feat(agent): job runner, compose override executor and swarm executor (#7)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/shcizo/package-updater/internal/protocol"
|
||||
)
|
||||
|
||||
type fakeCompose struct {
|
||||
fakeSelf
|
||||
containers []container.Summary
|
||||
}
|
||||
|
||||
func (f fakeCompose) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
|
||||
return f.containers, nil
|
||||
}
|
||||
|
||||
type ranCmd struct {
|
||||
dir string
|
||||
args []string
|
||||
}
|
||||
|
||||
type recorder struct {
|
||||
cmds []ranCmd
|
||||
failOn string // second arg after -f list, e.g. "pull"
|
||||
failOutput string
|
||||
onFirst func()
|
||||
}
|
||||
|
||||
func (r *recorder) run(_ context.Context, dir string, out io.Writer, args ...string) error {
|
||||
if len(r.cmds) == 0 && r.onFirst != nil {
|
||||
r.onFirst()
|
||||
}
|
||||
r.cmds = append(r.cmds, ranCmd{dir, args})
|
||||
if r.failOn != "" && strings.Contains(strings.Join(args, " "), " "+r.failOn+" ") {
|
||||
io.WriteString(out, r.failOutput)
|
||||
return errors.New("exit status 1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestComposeExecutor(t *testing.T) {
|
||||
const d = "sha256:d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1"
|
||||
root, _ := filepath.EvalSymlinks(t.TempDir())
|
||||
stack := filepath.Join(root, "slusk")
|
||||
require.NoError(t, os.MkdirAll(stack, 0o755))
|
||||
job := protocol.JobStart{Kind: protocol.KindCompose, ComposeProject: "slusk", ComposeService: "web", Ref: "gitea.shcizo.se/shcizo/slusk:1.4"}
|
||||
pinned := job.Ref + "@" + d
|
||||
|
||||
ct := container.Summary{Labels: map[string]string{
|
||||
labelWorkingDir: stack,
|
||||
labelConfigFiles: filepath.Join(stack, "compose.yaml"),
|
||||
}}
|
||||
rw := fakeSelf{mounts: []container.MountPoint{{Type: mount.TypeBind, Source: root, Destination: root, RW: true}}}
|
||||
ro := fakeSelf{mounts: []container.MountPoint{{Type: mount.TypeBind, Source: root, Destination: root, RW: false}}}
|
||||
wantFiles := []string{"-f", filepath.Join(stack, "compose.yaml"), "-f", filepath.Join(stack, OverrideName)}
|
||||
cmd := func(rest ...string) []string {
|
||||
return append(append([]string{"compose", "-p", "slusk"}, wantFiles...), rest...)
|
||||
}
|
||||
overridePath := filepath.Join(stack, OverrideName)
|
||||
cleanOverride := func() { os.Remove(overridePath) }
|
||||
|
||||
setup := func(cli ComposeDocker, dist *fakeDist, rec *recorder) *ComposeExecutor {
|
||||
return NewComposeExecutor(cli, NewResolver(dist, nil), "me", rec.run)
|
||||
}
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
rec := &recorder{onFirst: func() {
|
||||
_, err := os.Stat(overridePath)
|
||||
assert.NoError(t, err, "override must exist before the first command")
|
||||
}}
|
||||
var out strings.Builder
|
||||
got, err := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, &out)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pinned, got)
|
||||
require.Len(t, rec.cmds, 2)
|
||||
assert.Equal(t, ranCmd{stack, cmd("pull", "web")}, rec.cmds[0])
|
||||
assert.Equal(t, ranCmd{stack, cmd("up", "-d", "web")}, rec.cmds[1])
|
||||
_, doc := readOverride(t, stack)
|
||||
assert.Equal(t, pinned, serviceImage(doc, "web"))
|
||||
assert.Contains(t, out.String(), "resolved "+pinned)
|
||||
})
|
||||
t.Run("no matching container", func(t *testing.T) {
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{rw, nil}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "no container for compose service slusk/web on this machine")
|
||||
assert.Empty(t, rec.cmds)
|
||||
})
|
||||
t.Run("resolve fails", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{err: errors.New("denied")}, rec).Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "denied")
|
||||
assert.Empty(t, rec.cmds)
|
||||
assert.NoFileExists(t, overridePath)
|
||||
})
|
||||
t.Run("pull fails", func(t *testing.T) {
|
||||
t.Cleanup(cleanOverride)
|
||||
rec := &recorder{failOn: "pull", failOutput: "pull access denied\n"}
|
||||
_, err := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "pull access denied")
|
||||
require.Len(t, rec.cmds, 1, "up must not run")
|
||||
})
|
||||
t.Run("override refused", func(t *testing.T) {
|
||||
other := filepath.Join(stack, "docker-compose.override.yml")
|
||||
require.NoError(t, os.WriteFile(other, []byte("services: {}\n"), 0o644))
|
||||
t.Cleanup(func() { os.Remove(other) })
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{rw, []container.Summary{ct}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "refused")
|
||||
assert.Empty(t, rec.cmds)
|
||||
})
|
||||
t.Run("read-only mount", func(t *testing.T) {
|
||||
rec := &recorder{}
|
||||
_, err := setup(fakeCompose{ro, []container.Summary{ct}}, &fakeDist{digest: d}, rec).Execute(context.Background(), job, io.Discard)
|
||||
assert.ErrorContains(t, err, "no stack directory is mounted")
|
||||
assert.Empty(t, rec.cmds)
|
||||
assert.NoFileExists(t, overridePath)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user