fix: Equals and Clone for configs (#235)

This commit is contained in:
Anton Ovchinnikov
2025-12-30 20:32:19 +10:00
committed by GitHub
parent 878f96a4b7
commit 9a1d2e0d46
3 changed files with 172 additions and 0 deletions
+71
View File
@@ -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{})
+18
View File
@@ -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
}
+83
View File
@@ -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")
}