package deploy import ( "testing" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/psviderski/uncloud/pkg/api" "github.com/stretchr/testify/assert" ) func TestEvalContainerSpecChange_ContainerCapAdd(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", CapAdd: []string{"NET_ADMIN"}, }, } assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) } func TestEvalContainerSpecChange_ContainerCapDrop(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", CapDrop: []string{"ALL"}, }, } assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) } func TestEvalContainerSpecChange_ContainerPidMode(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", PidMode: "host", }, } assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) } func TestEvalContainerSpecChange_ContainerTty(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Tty: true, OpenStdin: true, }, } assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) } func TestEvalContainerSpecChange_ContainerLabels(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Labels: map[string]string{"com.example.team": "web"}, }, } emptySpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Labels: map[string]string{}, }, } assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(newSpec, newSpec)) assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(currentSpec, emptySpec)) } func TestEvalContainerSpecChange_ServiceLabels(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, Labels: map[string]string{"tier": "frontend"}, } emptySpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, Labels: map[string]string{}, } assert.Equal(t, ContainerNeedsUpdate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsUpdate, EvalContainerSpecChange(newSpec, currentSpec)) assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(newSpec, newSpec)) assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(currentSpec, emptySpec)) // Changes that require recreating the container take precedence over the in-place update. newSpecWithImage := newSpec.Clone() newSpecWithImage.Container.Image = "nginx:alpine" assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpecWithImage)) } func TestEvalContainerChange(t *testing.T) { t.Parallel() spec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: api.ContainerResources{CPU: api.Core, Memory: 256 * 1024 * 1024}, }, } appliedSpec := container.Resources{NanoCPUs: api.Core, Memory: 256 * 1024 * 1024} tests := []struct { name string hostConfig *container.HostConfig change func(spec *api.ServiceSpec) expected ContainerSpecStatus }{ { name: "applied limits match spec", hostConfig: &container.HostConfig{Resources: appliedSpec}, change: func(*api.ServiceSpec) {}, expected: ContainerUpToDate, }, { name: "unknown host config falls back to spec limits", change: func(*api.ServiceSpec) {}, expected: ContainerUpToDate, }, { name: "applied memory limit drifted from spec", hostConfig: &container.HostConfig{Resources: container.Resources{NanoCPUs: api.Core, Memory: 512 * 1024 * 1024}}, change: func(*api.ServiceSpec) {}, expected: ContainerNeedsUpdate, }, { name: "applied limits already match new spec but stored spec doesn't", hostConfig: &container.HostConfig{Resources: container.Resources{NanoCPUs: api.Core, Memory: 512 * 1024 * 1024}}, change: func(spec *api.ServiceSpec) { spec.Container.Resources.Memory = 512 * 1024 * 1024 }, expected: ContainerNeedsUpdate, }, { name: "applied limit not in spec can't be removed in place", hostConfig: &container.HostConfig{Resources: container.Resources{ NanoCPUs: api.Core, Memory: 256 * 1024 * 1024, MemoryReservation: 128 * 1024 * 1024, }}, change: func(*api.ServiceSpec) {}, expected: ContainerNeedsRecreate, }, { name: "spec change that requires recreating", hostConfig: &container.HostConfig{Resources: appliedSpec}, change: func(spec *api.ServiceSpec) { spec.Container.Image = "nginx:alpine" }, expected: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() newSpec := spec.Clone() tt.change(&newSpec) assert.Equal(t, tt.expected, EvalContainerChange(serviceContainer(spec, tt.hostConfig), newSpec)) }) } } func TestContainerUpdateOptions(t *testing.T) { t.Parallel() current := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: api.ContainerResources{CPU: api.Core, Memory: 256 * 1024 * 1024}, }, Labels: map[string]string{"tier": "frontend"}, } applied := &container.HostConfig{Resources: container.Resources{NanoCPUs: api.Core, Memory: 256 * 1024 * 1024}} tests := []struct { name string hostConfig *container.HostConfig change func(spec *api.ServiceSpec) expected api.UpdateContainerOptions }{ { name: "no changes", hostConfig: applied, change: func(*api.ServiceSpec) {}, }, { name: "CPU only", hostConfig: applied, change: func(spec *api.ServiceSpec) { spec.Container.Resources.CPU = 2 * api.Core }, expected: api.UpdateContainerOptions{CPU: new(int64(2 * api.Core))}, }, { name: "memory and reservation", hostConfig: applied, change: func(spec *api.ServiceSpec) { spec.Container.Resources.Memory = 512 * 1024 * 1024 spec.Container.Resources.MemoryReservation = 128 * 1024 * 1024 }, expected: api.UpdateContainerOptions{ Memory: new(int64(512 * 1024 * 1024)), MemoryReservation: new(int64(128 * 1024 * 1024)), }, }, { name: "applied memory limit drifted from spec", hostConfig: &container.HostConfig{Resources: container.Resources{ NanoCPUs: api.Core, Memory: 512 * 1024 * 1024, }}, change: func(*api.ServiceSpec) {}, expected: api.UpdateContainerOptions{Memory: new(int64(256 * 1024 * 1024))}, }, { name: "applied memory limit already matches new spec", hostConfig: &container.HostConfig{Resources: container.Resources{ NanoCPUs: api.Core, Memory: 512 * 1024 * 1024, }}, change: func(spec *api.ServiceSpec) { spec.Container.Resources.Memory = 512 * 1024 * 1024 }, expected: api.UpdateContainerOptions{Memory: new(int64(512 * 1024 * 1024))}, }, { name: "change service labels", hostConfig: applied, change: func(spec *api.ServiceSpec) { spec.Labels = map[string]string{"tier": "backend"} }, expected: api.UpdateContainerOptions{ServiceLabels: &map[string]string{"tier": "backend"}}, }, { name: "remove all service labels", hostConfig: applied, change: func(spec *api.ServiceSpec) { spec.Labels = nil }, expected: api.UpdateContainerOptions{ServiceLabels: &map[string]string{}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() newSpec := current.Clone() tt.change(&newSpec) assert.Equal(t, tt.expected, ContainerUpdateOptions(serviceContainer(current, tt.hostConfig), newSpec)) }) } } func serviceContainer(spec api.ServiceSpec, hostConfig *container.HostConfig) api.ServiceContainer { return api.ServiceContainer{ Container: api.Container{InspectResponse: container.InspectResponse{ ContainerJSONBase: &container.ContainerJSONBase{HostConfig: hostConfig}, }}, ServiceSpec: spec, } } func TestEvalContainerSpecChange_ContainerResources(t *testing.T) { t.Parallel() tests := []struct { name string current api.ContainerResources new api.ContainerResources want ContainerSpecStatus }{ { name: "empty", current: api.ContainerResources{}, new: api.ContainerResources{}, want: ContainerUpToDate, }, // CPU { name: "set CPU", current: api.ContainerResources{}, new: api.ContainerResources{ CPU: 1000000000, }, want: ContainerNeedsUpdate, }, { name: "change CPU", current: api.ContainerResources{ CPU: 1000000000, }, new: api.ContainerResources{ CPU: 2000000000, }, want: ContainerNeedsUpdate, }, { name: "unset CPU", current: api.ContainerResources{ CPU: 1000000000, }, new: api.ContainerResources{}, want: ContainerNeedsRecreate, }, // Memory { name: "equal memory", current: api.ContainerResources{Memory: 100 * 1024 * 1024}, new: api.ContainerResources{Memory: 100 * 1024 * 1024}, want: ContainerUpToDate, }, { name: "equal reservation", current: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024}, want: ContainerUpToDate, }, { name: "equal memory and reservation", current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, want: ContainerUpToDate, }, { name: "set memory", current: api.ContainerResources{}, new: api.ContainerResources{Memory: 100 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "set reservation", current: api.ContainerResources{}, new: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "set memory and reservation", current: api.ContainerResources{}, new: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "change memory", current: api.ContainerResources{Memory: 100 * 1024 * 1024}, new: api.ContainerResources{Memory: 200 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "change reservation", current: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{MemoryReservation: 100 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "change memory and reservation", current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{Memory: 200 * 1024 * 1024, MemoryReservation: 100 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "unset memory", current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024}, want: ContainerNeedsRecreate, }, { name: "unset reservation", current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{Memory: 100 * 1024 * 1024}, want: ContainerNeedsRecreate, }, { name: "unset memory and reservation", current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{}, want: ContainerNeedsRecreate, }, // Shared memory { name: "change shared memory", current: api.ContainerResources{SharedMemory: 64 * 1024 * 1024}, new: api.ContainerResources{SharedMemory: 128 * 1024 * 1024}, want: ContainerNeedsRecreate, }, { name: "change memory and shared memory", current: api.ContainerResources{Memory: 100 * 1024 * 1024}, new: api.ContainerResources{Memory: 200 * 1024 * 1024, SharedMemory: 64 * 1024 * 1024}, want: ContainerNeedsRecreate, }, // Memory and CPU { name: "set CPU and memory", current: api.ContainerResources{}, new: api.ContainerResources{ CPU: 1000000000, Memory: 100 * 1024 * 1024, }, want: ContainerNeedsUpdate, }, { name: "update CPU and memory", current: api.ContainerResources{ CPU: 1000000000, Memory: 100 * 1024 * 1024, }, new: api.ContainerResources{ CPU: 2000000000, Memory: 200 * 1024 * 1024, MemoryReservation: 100 * 1024 * 1024, }, want: ContainerNeedsUpdate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: tt.new, }, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_ContainerImage(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(currentSpec, newSpec)) newSpec.Container.Image = "nginx:1.19" assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) } func TestEvalContainerSpecChange_ContainerLogDriver(t *testing.T) { t.Parallel() tests := []struct { name string current *api.LogDriver new *api.LogDriver want ContainerSpecStatus }{ { name: "set driver", current: nil, new: &api.LogDriver{ Name: "json-file", }, want: ContainerNeedsRecreate, }, { name: "change driver", current: &api.LogDriver{ Name: "json-file", }, new: &api.LogDriver{ Name: "syslog", }, want: ContainerNeedsRecreate, }, { name: "set empty options", current: &api.LogDriver{ Name: "json-file", }, new: &api.LogDriver{ Name: "json-file", Options: map[string]string{}, }, want: ContainerUpToDate, }, { name: "set options", current: &api.LogDriver{ Name: "json-file", }, new: &api.LogDriver{ Name: "json-file", Options: map[string]string{ "max-size": "10m", }, }, want: ContainerNeedsRecreate, }, { name: "change options", current: &api.LogDriver{ Name: "json-file", Options: map[string]string{ "max-size": "10m", }, }, new: &api.LogDriver{ Name: "json-file", Options: map[string]string{ "max-size": "20m", }, }, want: ContainerNeedsRecreate, }, { name: "unset options", current: &api.LogDriver{ Name: "json-file", Options: map[string]string{ "max-size": "10m", }, }, new: &api.LogDriver{ Name: "json-file", }, want: ContainerNeedsRecreate, }, { name: "unset driver", current: &api.LogDriver{ Name: "json-file", Options: map[string]string{ "max-size": "10m", }, }, new: nil, want: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", LogDriver: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", LogDriver: tt.new, }, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_ContainerPrivileged(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Privileged: true, }, } assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) } func TestEvalContainerSpecChange_ContainerSysctls(t *testing.T) { t.Parallel() currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Sysctls: map[string]string{ "net.ipv4.ip_forward": "1", }, }, } assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) } func TestEvalContainerSpecChange_PullPolicy(t *testing.T) { t.Parallel() tests := []struct { name string current string new string want ContainerSpecStatus }{ { name: "non-always to always", current: api.PullPolicyMissing, new: api.PullPolicyAlways, want: ContainerNeedsRecreate, }, { name: "always to always", current: api.PullPolicyAlways, new: api.PullPolicyAlways, want: ContainerNeedsRecreate, }, { name: "always to non-always", current: api.PullPolicyAlways, new: api.PullPolicyMissing, want: ContainerUpToDate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ PullPolicy: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ PullPolicy: tt.new, }, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_ContainerUser(t *testing.T) { t.Parallel() tests := []struct { name string current string new string want ContainerSpecStatus }{ { name: "empty", current: "", new: "", want: ContainerUpToDate, }, { name: "equal user", current: "user", new: "user", want: ContainerUpToDate, }, { name: "equal UID", current: "1000", new: "1000", want: ContainerUpToDate, }, { name: "equal user group", current: "user:group", new: "user:group", want: ContainerUpToDate, }, { name: "equal UID GID", current: "1000:1000", new: "1000:1000", want: ContainerUpToDate, }, { name: "set user", current: "", new: "user", want: ContainerNeedsRecreate, }, { name: "set UID", current: "", new: "1000", want: ContainerNeedsRecreate, }, { name: "set user group", current: "", new: "user:group", want: ContainerNeedsRecreate, }, { name: "set UID GID", current: "", new: "1000:1000", want: ContainerNeedsRecreate, }, { name: "change user", current: "user", new: "another_user", want: ContainerNeedsRecreate, }, { name: "change group", current: "user:group", new: "user:another_group", want: ContainerNeedsRecreate, }, { name: "change user group", current: "user:group", new: "another_user:another_group", want: ContainerNeedsRecreate, }, { name: "unser user", current: "user", new: "", want: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", User: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", User: tt.new, }, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_Placement(t *testing.T) { t.Parallel() tests := []struct { name string current api.Placement new api.Placement want ContainerSpecStatus }{ { name: "empty", current: api.Placement{}, new: api.Placement{}, want: ContainerUpToDate, }, { name: "empty machines", current: api.Placement{Machines: nil}, new: api.Placement{Machines: []string{}}, want: ContainerUpToDate, }, { name: "set machines", current: api.Placement{}, new: api.Placement{Machines: []string{"machine1"}}, want: ContainerNeedsRecreate, }, { name: "unset", current: api.Placement{Machines: []string{"machine1"}}, new: api.Placement{}, want: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, Placement: tt.current, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, Placement: tt.new, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_Volumes(t *testing.T) { t.Parallel() tests := []struct { name string current api.ServiceSpec new api.ServiceSpec want ContainerSpecStatus }{ // TODO: should all volumes that are defined but not used (no corresponding mounts) be simply ignored? { name: "identical volumes", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, want: ContainerUpToDate, }, { name: "volumes in different order but identical", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data1", Type: api.VolumeTypeVolume, }, { Name: "data2", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data2", Type: api.VolumeTypeVolume, }, { Name: "data1", Type: api.VolumeTypeVolume, }, }, }, want: ContainerUpToDate, }, { name: "different number of volumes", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, { Name: "config", Type: api.VolumeTypeVolume, }, }, }, want: ContainerNeedsRecreate, }, { name: "no volumes to one volume", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{}, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, want: ContainerNeedsRecreate, }, { name: "one volume to no volumes", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{}, }, want: ContainerNeedsRecreate, }, { name: "change volume type", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/host/path", }, }, }, }, want: ContainerNeedsRecreate, }, { name: "change volume options", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, VolumeOptions: &api.VolumeOptions{ NoCopy: false, }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, VolumeOptions: &api.VolumeOptions{ NoCopy: true, }, }, }, }, want: ContainerNeedsRecreate, }, { name: "change volume options to defaults", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, VolumeOptions: &api.VolumeOptions{ Name: "data", }, }, }, }, want: ContainerUpToDate, }, { name: "change volume driver to local", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, VolumeOptions: &api.VolumeOptions{ Name: "data", Driver: &mount.Driver{ Name: api.VolumeDriverLocal, }, }, }, }, }, // TODO: this doesn't really require a recreate, only a spec update would be sufficient. want: ContainerNeedsRecreate, }, { name: "change volume driver to custom", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, VolumeOptions: &api.VolumeOptions{ Name: "data", Driver: &mount.Driver{ Name: "custom", }, }, }, }, }, // TODO: this doesn't really require a recreate, only a spec update would be sufficient. want: ContainerNeedsRecreate, }, { name: "change bind option CreateHostPath", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "config", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/host/path", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "config", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/host/path", CreateHostPath: true, }, }, }, }, // TODO: this doesn't really require a recreate, only a spec update would be sufficient. want: ContainerNeedsRecreate, }, { name: "change bind option propagation", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "config", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/host/path", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "config", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/host/path", Propagation: mount.PropagationRPrivate, }, }, }, }, // TODO: should we handle rprivate the same as empty propagation, hence up-to-date? want: ContainerNeedsRecreate, }, { name: "change tmpfs options", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "temp", Type: api.VolumeTypeTmpfs, TmpfsOptions: &mount.TmpfsOptions{ SizeBytes: 1024 * 1024, }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "temp", Type: api.VolumeTypeTmpfs, TmpfsOptions: &mount.TmpfsOptions{ SizeBytes: 2 * 1024 * 1024, }, }, }, }, want: ContainerNeedsRecreate, }, { name: "changing volume name", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data-new", Type: api.VolumeTypeVolume, }, }, }, want: ContainerNeedsRecreate, }, // Tests for volume mounts { name: "identical volume mounts", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, want: ContainerUpToDate, }, { name: "volume mounts in different order but identical", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data1", Type: api.VolumeTypeVolume, }, { Name: "data2", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data1", ContainerPath: "/data1", }, { VolumeName: "data2", ContainerPath: "/data2", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data1", Type: api.VolumeTypeVolume, }, { Name: "data2", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data2", ContainerPath: "/data2", }, { VolumeName: "data1", ContainerPath: "/data1", }, }, }, }, want: ContainerUpToDate, }, { name: "volumes and volume mounts in different order but identical", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data1", Type: api.VolumeTypeVolume, }, { Name: "data2", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data1", ContainerPath: "/data1", }, { VolumeName: "data2", ContainerPath: "/data2", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data2", Type: api.VolumeTypeVolume, }, { Name: "data1", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data2", ContainerPath: "/data2", }, { VolumeName: "data1", ContainerPath: "/data1", }, }, }, }, want: ContainerUpToDate, }, { name: "added volume mount", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, { Name: "config", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, { Name: "config", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, { VolumeName: "config", ContainerPath: "/config", }, }, }, }, want: ContainerNeedsRecreate, }, { name: "removed volume mount", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, { Name: "config", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, { VolumeName: "config", ContainerPath: "/config", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, { Name: "config", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, want: ContainerNeedsRecreate, }, { name: "changed volume mount container path", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/new/data/path", }, }, }, }, want: ContainerNeedsRecreate, }, { name: "changed volume mount read-only flag", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", ReadOnly: false, }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", ReadOnly: true, }, }, }, }, want: ContainerNeedsRecreate, }, { name: "changed mounted volume type", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/host/path", }, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, want: ContainerNeedsRecreate, }, { name: "changed reference name in spec but preserved original volume name", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "new_name", Type: api.VolumeTypeVolume, VolumeOptions: &api.VolumeOptions{ Name: "data", }, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "new_name", ContainerPath: "/data", }, }, }, }, // TODO: this doesn't really require a recreate, only a spec update would be sufficient. want: ContainerNeedsRecreate, }, { name: "changed volumes, mounts and paths", current: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, }, { Name: "config", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/etc/app/config", }, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/data", }, { VolumeName: "config", ContainerPath: "/app/config", ReadOnly: true, }, }, }, }, new: api.ServiceSpec{ Volumes: []api.VolumeSpec{ { Name: "data", Type: api.VolumeTypeVolume, VolumeOptions: &api.VolumeOptions{ NoCopy: true, }, }, { Name: "config", Type: api.VolumeTypeBind, BindOptions: &api.BindOptions{ HostPath: "/etc/app/new-config", CreateHostPath: true, }, }, { Name: "logs", Type: api.VolumeTypeVolume, }, }, Container: api.ContainerSpec{ VolumeMounts: []api.VolumeMount{ { VolumeName: "data", ContainerPath: "/var/lib/app/data", }, { VolumeName: "config", ContainerPath: "/app/config", ReadOnly: true, }, { VolumeName: "logs", ContainerPath: "/var/log/app", }, }, }, }, want: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := EvalContainerSpecChange(tt.current, tt.new) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_Devices(t *testing.T) { t.Parallel() tests := []struct { name string current api.ContainerResources new api.ContainerResources want ContainerSpecStatus }{ { name: "empty", current: api.ContainerResources{}, new: api.ContainerResources{}, want: ContainerUpToDate, }, { name: "identical mapping", current: api.ContainerResources{Devices: []api.DeviceMapping{{HostPath: "/dev/foo", ContainerPath: "/dev/foo", CgroupPermissions: "rwm"}}}, new: api.ContainerResources{Devices: []api.DeviceMapping{{HostPath: "/dev/foo", ContainerPath: "/dev/foo", CgroupPermissions: "rwm"}}}, want: ContainerUpToDate, }, { name: "add mapping", current: api.ContainerResources{}, new: api.ContainerResources{Devices: []api.DeviceMapping{{HostPath: "/dev/foo", ContainerPath: "/dev/foo", CgroupPermissions: "rwm"}}}, want: ContainerNeedsRecreate, }, { name: "remove mapping", current: api.ContainerResources{Devices: []api.DeviceMapping{{HostPath: "/dev/foo", ContainerPath: "/dev/foo", CgroupPermissions: "rwm"}}}, new: api.ContainerResources{}, want: ContainerNeedsRecreate, }, { name: "change mapping path", current: api.ContainerResources{Devices: []api.DeviceMapping{{HostPath: "/dev/foo", ContainerPath: "/dev/foo", CgroupPermissions: "rwm"}}}, new: api.ContainerResources{Devices: []api.DeviceMapping{{HostPath: "/dev/foo", ContainerPath: "/dev/bar", CgroupPermissions: "rwm"}}}, want: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: tt.new, }, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_DeviceReservations(t *testing.T) { t.Parallel() tests := []struct { name string current api.ContainerResources new api.ContainerResources want ContainerSpecStatus }{ { name: "empty", current: api.ContainerResources{}, new: api.ContainerResources{}, want: ContainerUpToDate, }, { name: "both nil", current: api.ContainerResources{DeviceReservations: nil}, new: api.ContainerResources{DeviceReservations: nil}, want: ContainerUpToDate, }, { name: "both empty", current: api.ContainerResources{DeviceReservations: []container.DeviceRequest{}}, new: api.ContainerResources{DeviceReservations: []container.DeviceRequest{}}, want: ContainerUpToDate, }, { name: "set GPU request", current: api.ContainerResources{}, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: -1, // all Capabilities: [][]string{{"gpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "unset GPU request", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: -1, Capabilities: [][]string{{"gpu"}}, }, }, }, new: api.ContainerResources{}, want: ContainerNeedsRecreate, }, { name: "identical GPU request", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 2, Capabilities: [][]string{{"gpu", "compute"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 2, Capabilities: [][]string{{"gpu", "compute"}}, }, }, }, want: ContainerUpToDate, }, { name: "change GPU count", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 2, Capabilities: [][]string{{"gpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "change GPU driver", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "amd", Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "change device IDs", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { DeviceIDs: []string{"0"}, Capabilities: [][]string{{"gpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { DeviceIDs: []string{"0", "1"}, Capabilities: [][]string{{"gpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "change capabilities", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu", "compute"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "set options", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, Options: map[string]string{ "key": "value", }, }, }, }, want: ContainerNeedsRecreate, }, { name: "change options", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, Options: map[string]string{ "key": "value1", }, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, Options: map[string]string{ "key": "value2", }, }, }, }, want: ContainerNeedsRecreate, }, { name: "unset options", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, Options: map[string]string{ "key": "value", }, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "add device request", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, { Count: 1, Capabilities: [][]string{{"tpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "remove device request", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, { Count: 1, Capabilities: [][]string{{"tpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "multiple identical device requests", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 2, Capabilities: [][]string{{"gpu", "compute"}}, }, { Count: 1, Capabilities: [][]string{{"tpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 2, Capabilities: [][]string{{"gpu", "compute"}}, }, { Count: 1, Capabilities: [][]string{{"tpu"}}, }, }, }, want: ContainerUpToDate, }, { name: "reordered device requests", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 2, Capabilities: [][]string{{"gpu"}}, }, { Count: 1, Capabilities: [][]string{{"tpu"}}, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"tpu"}}, }, { Driver: "nvidia", Count: 2, Capabilities: [][]string{{"gpu"}}, }, }, }, want: ContainerNeedsRecreate, }, { name: "complex GPU configuration identical", current: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 2, DeviceIDs: []string{"0", "1"}, Capabilities: [][]string{{"gpu", "compute", "utility"}}, Options: map[string]string{ "runtime": "nvidia", "compute": "exclusive", }, }, }, }, new: api.ContainerResources{ DeviceReservations: []container.DeviceRequest{ { Driver: "nvidia", Count: 2, DeviceIDs: []string{"0", "1"}, Capabilities: [][]string{{"gpu", "compute", "utility"}}, Options: map[string]string{ "runtime": "nvidia", "compute": "exclusive", }, }, }, }, want: ContainerUpToDate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nvidia/cuda:latest", Resources: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nvidia/cuda:latest", Resources: tt.new, }, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_Ulimits(t *testing.T) { t.Parallel() tests := []struct { name string current api.ContainerResources new api.ContainerResources want ContainerSpecStatus }{ { name: "empty", current: api.ContainerResources{}, new: api.ContainerResources{}, want: ContainerUpToDate, }, { name: "identical single ulimit", current: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 20000, Hard: 40000}}}, new: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 20000, Hard: 40000}}}, want: ContainerUpToDate, }, { name: "set ulimit", current: api.ContainerResources{}, new: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 20000, Hard: 40000}}}, want: ContainerNeedsRecreate, }, { name: "remove ulimit", current: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 20000, Hard: 40000}}}, new: api.ContainerResources{}, want: ContainerNeedsRecreate, }, { name: "change ulimit soft value", current: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 20000, Hard: 40000}}}, new: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 30000, Hard: 40000}}}, want: ContainerNeedsRecreate, }, { name: "change ulimit hard value", current: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 20000, Hard: 40000}}}, new: api.ContainerResources{Ulimits: map[string]api.Ulimit{"nofile": {Soft: 20000, Hard: 80000}}}, want: ContainerNeedsRecreate, }, { name: "add ulimit", current: api.ContainerResources{Ulimits: map[string]api.Ulimit{ "nofile": {Soft: 20000, Hard: 40000}, }}, new: api.ContainerResources{Ulimits: map[string]api.Ulimit{ "nofile": {Soft: 20000, Hard: 40000}, "nproc": {Soft: 65535, Hard: 65535}, }}, want: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: tt.new, }, } result := EvalContainerSpecChange(currentSpec, newSpec) assert.Equal(t, tt.want, result) }) } } func TestEvalContainerSpecChange_Mixed(t *testing.T) { t.Parallel() tests := []struct { name string current api.ServiceSpec new api.ServiceSpec want ContainerSpecStatus }{ { name: "mutable changes", current: api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: api.ContainerResources{ Memory: 100 * 1024 * 1024, }, }, }, new: api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: api.ContainerResources{ CPU: 1000000000, Memory: 200 * 1024 * 1024, MemoryReservation: 100 * 1024 * 1024, }, }, }, want: ContainerNeedsUpdate, }, { name: "mutable and immutable changes", current: api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", }, }, new: api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Resources: api.ContainerResources{ CPU: 1000000000, }, User: "root", }, }, want: ContainerNeedsRecreate, }, { name: "mutable memory change with immutable device reservation change", current: api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nvidia/cuda:latest", Resources: api.ContainerResources{ Memory: 100 * 1024 * 1024, }, }, }, new: api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nvidia/cuda:latest", Resources: api.ContainerResources{ Memory: 200 * 1024 * 1024, DeviceReservations: []container.DeviceRequest{ { Count: 1, Capabilities: [][]string{{"gpu"}}, }, }, }, }, }, want: ContainerNeedsRecreate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := EvalContainerSpecChange(tt.current, tt.new) assert.Equal(t, tt.want, result) }) } }