diff --git a/pkg/api/config.go b/pkg/api/config.go index 411313bd..ae6b4162 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strconv" ) @@ -98,6 +99,76 @@ func (c *ConfigMount) Validate() error { return nil } +// Compare compares this ConfigMount with another. +// Returns: +// +// -1 if c < other +// 0 if c == other +// +1 if c > other +func (c *ConfigMount) Compare(other *ConfigMount) int { + if c.ConfigName != other.ConfigName { + if c.ConfigName < other.ConfigName { + return -1 + } + return 1 + } + if c.ContainerPath != other.ContainerPath { + if c.ContainerPath < other.ContainerPath { + return -1 + } + return 1 + } + if c.Uid != other.Uid { + if c.Uid < other.Uid { + return -1 + } + return 1 + } + if c.Gid != other.Gid { + if c.Gid < other.Gid { + return -1 + } + return 1 + } + // Compare Mode (handle nil cases) + if c.Mode == nil && other.Mode != nil { + return -1 + } + if c.Mode != nil && other.Mode == nil { + return 1 + } + if c.Mode != nil && other.Mode != nil { + if *c.Mode < *other.Mode { + return -1 + } + if *c.Mode > *other.Mode { + return 1 + } + } + return 0 +} + +// Equals compares two ConfigMount instances for equality. +func (c *ConfigMount) Equals(other *ConfigMount) bool { + return c.Compare(other) == 0 +} + +func (c *ConfigMount) Clone() ConfigMount { + clone := *c + if c.Mode != nil { + mode := *c.Mode + clone.Mode = &mode + } + return clone +} + +// sortConfigMounts sorts a slice of ConfigMount instances. +func sortConfigMounts(mounts []ConfigMount) { + sort.Slice(mounts, func(i, j int) bool { + return mounts[i].Compare(&mounts[j]) < 0 + }) +} + // ValidateConfigsAndMounts takes config specs and config mounts and validates that all mounts refer to existing specs func ValidateConfigsAndMounts(configs []ConfigSpec, mounts []ConfigMount) error { configMap := make(map[string]struct{}) diff --git a/pkg/api/service.go b/pkg/api/service.go index 6075cf36..409807f1 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -290,12 +290,18 @@ func (s *ContainerSpec) Equals(spec ContainerSpec) bool { orig := s.SetDefaults() spec = spec.SetDefaults() + // Volumes slices.Sort(orig.Volumes) slices.Sort(spec.Volumes) + // Volume mounts sortVolumeMounts(orig.VolumeMounts) sortVolumeMounts(spec.VolumeMounts) + // Config mounts + sortConfigMounts(orig.ConfigMounts) + sortConfigMounts(spec.ConfigMounts) + return cmp.Equal(orig, spec, cmpopts.EquateEmpty()) } @@ -317,6 +323,12 @@ func (s *ContainerSpec) Clone() ContainerSpec { } spec.LogDriver = &logDriver } + if s.Env != nil { + spec.Env = make(EnvVars, len(s.Env)) + for k, v := range s.Env { + spec.Env[k] = v + } + } if s.Volumes != nil { spec.Volumes = make([]string, len(s.Volumes)) copy(spec.Volumes, s.Volumes) @@ -325,6 +337,12 @@ func (s *ContainerSpec) Clone() ContainerSpec { spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts)) copy(spec.VolumeMounts, s.VolumeMounts) } + if s.ConfigMounts != nil { + spec.ConfigMounts = make([]ConfigMount, len(s.ConfigMounts)) + for i, cm := range s.ConfigMounts { + spec.ConfigMounts[i] = cm.Clone() + } + } return spec } diff --git a/pkg/api/service_test.go b/pkg/api/service_test.go index 5b530290..79508333 100644 --- a/pkg/api/service_test.go +++ b/pkg/api/service_test.go @@ -1,11 +1,19 @@ package api import ( + "os" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// boolPtr is a convenience function to create a pointer to a uint64 value +// TODO: Make this a generic function that works for any type +func boolPtr(b bool) *bool { + return &b +} + func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) { tests := []struct { name string @@ -197,3 +205,78 @@ func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) { }) } } + +func TestContainerSpec_Clone(t *testing.T) { + mode := os.FileMode(0o644) + original := ContainerSpec{ + Command: []string{"sh", "-c", "echo hello"}, + Entrypoint: []string{"/bin/bash"}, + Env: EnvVars{ + "FOO": "bar", + "BAZ": "qux", + }, + Image: "nginx:latest", + Init: boolPtr(true), + LogDriver: &LogDriver{ + Name: "json-file", + Options: map[string]string{ + "max-size": "10m", + }, + }, + Privileged: true, + PullPolicy: PullPolicyAlways, + Resources: ContainerResources{ + CPU: 1234, + Memory: 2345, + MemoryReservation: 3456, + }, + User: "1000:1000", + Volumes: []string{"/data", "/config"}, + VolumeMounts: []VolumeMount{ + {VolumeName: "data", ContainerPath: "/data"}, + }, + ConfigMounts: []ConfigMount{ + {ConfigName: "app-config", ContainerPath: "/etc/config", Mode: &mode}, + }, + } + + cloned := original.Clone() + + // Check ContainerSpec equality + assert.True(t, original.Equals(cloned)) + + // Verify deep copy by modifying the original + stringModified := "modified" + original.Command[0] = stringModified + original.Entrypoint[0] = stringModified + original.Env["FOO"] = stringModified + original.LogDriver.Options["max-size"] = stringModified + original.Volumes[0] = stringModified + original.VolumeMounts[0].ContainerPath = stringModified + original.ConfigMounts[0].ContainerPath = stringModified + *original.ConfigMounts[0].Mode = 0o755 // Modify the Mode pointer value + + assert.False(t, original.Equals(cloned)) + // Assert cloned values are unchanged + assert.Equal(t, "sh", cloned.Command[0]) + assert.Equal(t, "/bin/bash", cloned.Entrypoint[0]) + assert.Equal(t, "bar", cloned.Env["FOO"]) + assert.Equal(t, "qux", cloned.Env["BAZ"]) + assert.Equal(t, "nginx:latest", cloned.Image) + assert.NotNil(t, cloned.Init) + assert.Equal(t, true, *cloned.Init) + assert.NotNil(t, cloned.LogDriver) + assert.Equal(t, "json-file", cloned.LogDriver.Name) + assert.Equal(t, "10m", cloned.LogDriver.Options["max-size"]) + assert.Equal(t, true, cloned.Privileged) + assert.Equal(t, PullPolicyAlways, cloned.PullPolicy) + assert.Equal(t, int64(1234), cloned.Resources.CPU) + assert.Equal(t, int64(2345), cloned.Resources.Memory) + assert.Equal(t, int64(3456), cloned.Resources.MemoryReservation) + assert.Equal(t, "1000:1000", cloned.User) + assert.Equal(t, "/data", cloned.Volumes[0]) + assert.Equal(t, "/data", cloned.VolumeMounts[0].ContainerPath) + assert.Equal(t, "/etc/config", cloned.ConfigMounts[0].ContainerPath) + assert.NotNil(t, cloned.ConfigMounts[0].Mode) + assert.Equal(t, os.FileMode(0o644), *cloned.ConfigMounts[0].Mode, "Mode should be deep copied") +}