From 2940965e31fbf62a652da25515e8d862ec079721 Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Wed, 25 Feb 2026 16:38:44 +1000 Subject: [PATCH] fix: recreate container on ulimit changes, add test --- pkg/client/deploy/container.go | 4 ++ pkg/client/deploy/container_test.go | 79 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/pkg/client/deploy/container.go b/pkg/client/deploy/container.go index ca97ac9c..bbde5d70 100644 --- a/pkg/client/deploy/container.go +++ b/pkg/client/deploy/container.go @@ -90,6 +90,10 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta if !reflect.DeepEqual(current.Container.Resources.Devices, newResources.Devices) { return ContainerNeedsRecreate } + // Ulimits are immutable, so we'll need to recreate if any have changed. + if !reflect.DeepEqual(current.Container.Resources.Ulimits, newResources.Ulimits) { + return ContainerNeedsRecreate + } // Check if any mutable properties changed. if !current.Caddy.Equals(new.Caddy) { diff --git a/pkg/client/deploy/container_test.go b/pkg/client/deploy/container_test.go index bf819fbb..fff9c3ef 100644 --- a/pkg/client/deploy/container_test.go +++ b/pkg/client/deploy/container_test.go @@ -1864,6 +1864,85 @@ func TestEvalContainerSpecChange_DeviceReservations(t *testing.T) { } } +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()