200 lines
7.7 KiB
Go
200 lines
7.7 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"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"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func readOverride(t *testing.T, dir string) (string, map[string]any) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join(dir, OverrideName))
|
|
require.NoError(t, err)
|
|
var doc map[string]any
|
|
require.NoError(t, yaml.Unmarshal(raw, &doc))
|
|
return string(raw), doc
|
|
}
|
|
|
|
func serviceImage(doc map[string]any, name string) any {
|
|
svcs, _ := doc["services"].(map[string]any)
|
|
entry, _ := svcs[name].(map[string]any)
|
|
return entry["image"]
|
|
}
|
|
|
|
func TestWriteOverride(t *testing.T) {
|
|
t.Run("new file", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path, err := WriteOverride(dir, "web", "a:1@sha256:aa")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, filepath.Join(dir, OverrideName), path)
|
|
raw, doc := readOverride(t, dir)
|
|
assert.True(t, len(raw) > len(overrideHeader) && raw[:len(overrideHeader)] == overrideHeader)
|
|
assert.Equal(t, "a:1@sha256:aa", serviceImage(doc, "web"))
|
|
})
|
|
t.Run("keeps other services", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, OverrideName), []byte(overrideHeader+"services:\n db:\n image: db:1\n"), 0o644))
|
|
_, err := WriteOverride(dir, "web", "a:1@sha256:aa")
|
|
require.NoError(t, err)
|
|
_, doc := readOverride(t, dir)
|
|
assert.Equal(t, "db:1", serviceImage(doc, "db"))
|
|
assert.Equal(t, "a:1@sha256:aa", serviceImage(doc, "web"))
|
|
})
|
|
t.Run("replaces own entry", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, OverrideName), []byte(overrideHeader+"services:\n web:\n image: old\n"), 0o644))
|
|
_, err := WriteOverride(dir, "web", "a:2@sha256:bb")
|
|
require.NoError(t, err)
|
|
_, doc := readOverride(t, dir)
|
|
assert.Equal(t, "a:2@sha256:bb", serviceImage(doc, "web"))
|
|
})
|
|
t.Run("operator file without header is never touched", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
const body = "services:\n web:\n environment: [A=1]\n"
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, OverrideName), []byte(body), 0o644))
|
|
_, err := WriteOverride(dir, "web", "a:1@sha256:aa")
|
|
assert.ErrorContains(t, err, "refused")
|
|
got, _ := os.ReadFile(filepath.Join(dir, OverrideName))
|
|
assert.Equal(t, body, string(got))
|
|
})
|
|
for _, other := range []string{"compose.override.yml", "docker-compose.override.yml", "docker-compose.override.yaml"} {
|
|
t.Run("refuses next to "+other, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, other), []byte("services: {}\n"), 0o644))
|
|
_, err := WriteOverride(dir, "web", "a:1@sha256:aa")
|
|
assert.ErrorContains(t, err, other)
|
|
_, statErr := os.Stat(filepath.Join(dir, OverrideName))
|
|
assert.True(t, errors.Is(statErr, os.ErrNotExist))
|
|
})
|
|
}
|
|
t.Run("leaves no temp files", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := WriteOverride(dir, "web", "a:1@sha256:aa")
|
|
require.NoError(t, err)
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 1)
|
|
assert.Equal(t, OverrideName, entries[0].Name())
|
|
})
|
|
}
|
|
|
|
type fakeSelf struct {
|
|
mounts []container.MountPoint
|
|
err error
|
|
}
|
|
|
|
func (f fakeSelf) ContainerInspect(context.Context, string) (container.InspectResponse, error) {
|
|
return container.InspectResponse{Mounts: f.mounts}, f.err
|
|
}
|
|
|
|
func TestMountRoots(t *testing.T) {
|
|
bind := func(src, dst string, rw bool) container.MountPoint {
|
|
return container.MountPoint{Type: mount.TypeBind, Source: src, Destination: dst, RW: rw}
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
self fakeSelf
|
|
selfID string
|
|
want []string
|
|
wantErr string
|
|
}{
|
|
{"rw same path", fakeSelf{mounts: []container.MountPoint{bind("/srv/stacks", "/srv/stacks", true)}}, "me", []string{"/srv/stacks"}, ""},
|
|
{"read-only", fakeSelf{mounts: []container.MountPoint{bind("/srv/stacks", "/srv/stacks", false)}}, "me", nil, ""},
|
|
{"path translated", fakeSelf{mounts: []container.MountPoint{bind("/home/x/stacks", "/stacks", true)}}, "me", nil, ""},
|
|
{"named volume", fakeSelf{mounts: []container.MountPoint{{Type: mount.TypeVolume, Source: "/v", Destination: "/v", RW: true}}}, "me", nil, ""},
|
|
{"inspect error", fakeSelf{err: errors.New("no such container")}, "me", nil, "inspect own container"},
|
|
{"no self id", fakeSelf{}, "", nil, "no stack directory is mounted"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got, err := MountRoots(context.Background(), c.self, c.selfID)
|
|
if c.wantErr != "" {
|
|
assert.ErrorContains(t, err, c.wantErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, c.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComposeFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
root, err := filepath.EvalSymlinks(root) // macOS temp dirs are symlinked
|
|
require.NoError(t, err)
|
|
mk := func(p ...string) string {
|
|
dir := filepath.Join(append([]string{root}, p...)...)
|
|
require.NoError(t, os.MkdirAll(dir, 0o755))
|
|
return dir
|
|
}
|
|
a := mk("a")
|
|
nested := mk("apps", "x", "y")
|
|
second := t.TempDir()
|
|
second, _ = filepath.EvalSymlinks(second)
|
|
secondDir := filepath.Join(second, "s")
|
|
require.NoError(t, os.MkdirAll(secondDir, 0o755))
|
|
|
|
outside, _ := filepath.EvalSymlinks(t.TempDir())
|
|
require.NoError(t, os.Symlink(outside, filepath.Join(root, "evil")))
|
|
|
|
sock := filepath.Join(root, "docker.sock")
|
|
require.NoError(t, os.WriteFile(sock, nil, 0o644))
|
|
|
|
t.Run("adds override last", func(t *testing.T) {
|
|
files, dir, err := composeFiles([]string{root}, filepath.Join(a, "compose.yaml"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{filepath.Join(a, "compose.yaml"), filepath.Join(a, OverrideName)}, files)
|
|
assert.Equal(t, a, dir)
|
|
})
|
|
t.Run("override in label listed once, last", func(t *testing.T) {
|
|
label := filepath.Join(a, OverrideName) + "," + filepath.Join(a, "compose.yaml") + "," + filepath.Join(a, "extra.yaml")
|
|
files, _, err := composeFiles([]string{root}, label)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{filepath.Join(a, "compose.yaml"), filepath.Join(a, "extra.yaml"), filepath.Join(a, OverrideName)}, files)
|
|
})
|
|
t.Run("nested", func(t *testing.T) {
|
|
_, dir, err := composeFiles([]string{root}, filepath.Join(nested, "compose.yaml"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, nested, dir)
|
|
})
|
|
t.Run("second root", func(t *testing.T) {
|
|
_, _, err := composeFiles([]string{root, second}, filepath.Join(secondDir, "compose.yaml"))
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("empty label", func(t *testing.T) {
|
|
_, _, err := composeFiles([]string{root}, "")
|
|
assert.ErrorContains(t, err, "config_files")
|
|
})
|
|
t.Run("no roots", func(t *testing.T) {
|
|
_, _, err := composeFiles(nil, filepath.Join(a, "compose.yaml"))
|
|
assert.ErrorContains(t, err, "no stack directory is mounted")
|
|
})
|
|
t.Run("outside every root", func(t *testing.T) {
|
|
_, _, err := composeFiles([]string{root}, filepath.Join(second, "s", "compose.yaml"))
|
|
assert.EqualError(t, err, "stack directory "+filepath.Join(second, "s")+" is not mounted into the agent at the same path (read-write)")
|
|
})
|
|
t.Run("symlink escaping the root", func(t *testing.T) {
|
|
_, _, err := composeFiles([]string{root}, filepath.Join(root, "evil", "compose.yaml"))
|
|
assert.EqualError(t, err, "stack directory "+filepath.Join(root, "evil")+" is not mounted into the agent at the same path (read-write)")
|
|
})
|
|
t.Run("prefix trick", func(t *testing.T) {
|
|
evil := root + "-evil"
|
|
require.NoError(t, os.MkdirAll(evil, 0o755))
|
|
t.Cleanup(func() { os.RemoveAll(evil) })
|
|
_, _, err := composeFiles([]string{root}, filepath.Join(evil, "compose.yaml"))
|
|
assert.ErrorContains(t, err, "not mounted")
|
|
})
|
|
t.Run("a file root allows nothing", func(t *testing.T) {
|
|
_, _, err := composeFiles([]string{sock}, filepath.Join(sock, "compose.yaml"))
|
|
assert.Error(t, err)
|
|
})
|
|
}
|