package deploy import ( "testing" "github.com/docker/docker/api/types/mount" "github.com/psviderski/uncloud/pkg/api" "github.com/stretchr/testify/assert" ) func TestEvalContainerSpecChange_ContainerCPU(t *testing.T) { t.Parallel() tests := []struct { name string current api.CPUResources new api.CPUResources want ContainerSpecStatus }{ { name: "set limit", current: api.CPUResources{}, new: api.CPUResources{ Limit: 1000000000, }, want: ContainerNeedsUpdate, }, { name: "change limit", current: api.CPUResources{ Limit: 1000000000, }, new: api.CPUResources{ Limit: 2000000000, }, want: ContainerNeedsUpdate, }, { name: "unset limit", current: api.CPUResources{ Limit: 1000000000, }, new: api.CPUResources{}, want: ContainerNeedsUpdate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", CPU: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", CPU: 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_ContainerMemory(t *testing.T) { t.Parallel() tests := []struct { name string current api.MemoryResources new api.MemoryResources want ContainerSpecStatus }{ { name: "empty", current: api.MemoryResources{}, new: api.MemoryResources{}, want: ContainerUpToDate, }, { name: "equal limit", current: api.MemoryResources{Limit: 100 * 1024 * 1024}, new: api.MemoryResources{Limit: 100 * 1024 * 1024}, want: ContainerUpToDate, }, { name: "equal reservation", current: api.MemoryResources{Reservation: 50 * 1024 * 1024}, new: api.MemoryResources{Reservation: 50 * 1024 * 1024}, want: ContainerUpToDate, }, { name: "equal limit and reservation", current: api.MemoryResources{Limit: 100 * 1024 * 1024, Reservation: 50 * 1024 * 1024}, new: api.MemoryResources{Limit: 100 * 1024 * 1024, Reservation: 50 * 1024 * 1024}, want: ContainerUpToDate, }, { name: "set limit", current: api.MemoryResources{}, new: api.MemoryResources{Limit: 100 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "set reservation", current: api.MemoryResources{}, new: api.MemoryResources{Reservation: 50 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "set limit and reservation", current: api.MemoryResources{}, new: api.MemoryResources{Limit: 100 * 1024 * 1024, Reservation: 50 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "change limit", current: api.MemoryResources{Limit: 100 * 1024 * 1024}, new: api.MemoryResources{Limit: 200 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "change reservation", current: api.MemoryResources{Reservation: 50 * 1024 * 1024}, new: api.MemoryResources{Reservation: 100 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "change limit and reservation", current: api.MemoryResources{Limit: 100 * 1024 * 1024, Reservation: 50 * 1024 * 1024}, new: api.MemoryResources{Limit: 200 * 1024 * 1024, Reservation: 100 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "unset limit", current: api.MemoryResources{Limit: 100 * 1024 * 1024, Reservation: 50 * 1024 * 1024}, new: api.MemoryResources{Reservation: 50 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "unset reservation", current: api.MemoryResources{Limit: 100 * 1024 * 1024, Reservation: 50 * 1024 * 1024}, new: api.MemoryResources{Limit: 100 * 1024 * 1024}, want: ContainerNeedsUpdate, }, { name: "unset limit and reservation", current: api.MemoryResources{Limit: 100 * 1024 * 1024, Reservation: 50 * 1024 * 1024}, new: api.MemoryResources{}, want: ContainerNeedsUpdate, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { currentSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Memory: tt.current, }, } newSpec := api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", Memory: 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_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_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", Memory: api.MemoryResources{ Limit: 100 * 1024 * 1024, }, }, }, new: api.ServiceSpec{ Container: api.ContainerSpec{ Image: "nginx:latest", CPU: api.CPUResources{ Limit: 1000000000, }, Memory: api.MemoryResources{ Limit: 200 * 1024 * 1024, Reservation: 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", CPU: api.CPUResources{ Limit: 1000000000, }, User: "root", }, }, 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) }) } }