package api import ( "os" "testing" "github.com/docker/docker/api/types/container" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) { tests := []struct { name string spec ServiceSpec wantErr string }{ { name: "valid with neither Caddy nor Ports", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, }, wantErr: "", }, { name: "valid with Caddy only", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Caddy: &CaddySpec{ Config: "example.com {\n reverse_proxy :8080\n}", }, }, wantErr: "", }, { name: "valid with Ports only", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Ports: []PortSpec{ { ContainerPort: 80, Protocol: ProtocolHTTP, }, }, }, wantErr: "", }, { name: "valid with empty Caddy config and Ports", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Caddy: &CaddySpec{ Config: "", }, Ports: []PortSpec{ { ContainerPort: 80, Protocol: ProtocolHTTP, }, }, }, wantErr: "", }, { name: "invalid with Caddy and Ports (default mode is ingress)", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Caddy: &CaddySpec{ Config: "example.com {\n reverse_proxy :8080\n}", }, Ports: []PortSpec{ { ContainerPort: 80, Protocol: ProtocolHTTP, // Mode is empty, defaults to ingress }, }, }, wantErr: "ingress ports and Caddy configuration cannot be specified simultaneously", }, { name: "invalid with both Caddy and ingress Ports", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Caddy: &CaddySpec{ Config: "example.com {\n reverse_proxy :8080\n}", }, Ports: []PortSpec{ { ContainerPort: 80, Protocol: ProtocolHTTP, Mode: PortModeIngress, }, }, }, wantErr: "ingress ports and Caddy configuration cannot be specified simultaneously", }, { name: "valid with Caddy and host mode Ports", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Caddy: &CaddySpec{ Config: "example.com {\n reverse_proxy :8080\n}", }, Ports: []PortSpec{ { ContainerPort: 3306, PublishedPort: 3306, Protocol: ProtocolTCP, Mode: PortModeHost, }, }, }, wantErr: "", }, { name: "invalid with Caddy and mixed mode Ports", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Caddy: &CaddySpec{ Config: "example.com {\n reverse_proxy :8080\n}", }, Ports: []PortSpec{ { ContainerPort: 3306, PublishedPort: 3306, Protocol: ProtocolTCP, Mode: PortModeHost, }, { ContainerPort: 80, Protocol: ProtocolHTTP, Mode: PortModeIngress, }, }, }, wantErr: "ingress ports and Caddy configuration cannot be specified simultaneously", }, { name: "valid with Caddy and multiple host mode Ports", spec: ServiceSpec{ Name: "test", Container: ContainerSpec{ Image: "nginx:latest", }, Caddy: &CaddySpec{ Config: "example.com {\n reverse_proxy :8080\n}", }, Ports: []PortSpec{ { ContainerPort: 3306, PublishedPort: 3306, Protocol: ProtocolTCP, Mode: PortModeHost, }, { ContainerPort: 5432, PublishedPort: 5432, Protocol: ProtocolTCP, Mode: PortModeHost, }, }, }, wantErr: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.spec.Validate() if tt.wantErr == "" { require.NoError(t, err) } else { require.ErrorContains(t, err, tt.wantErr) } }) } } func TestContainerSpec_Clone(t *testing.T) { mode := os.FileMode(0o644) original := ContainerSpec{ CapAdd: []string{"NET_ADMIN"}, CapDrop: []string{"ALL"}, Pid: container.PidMode("host"), Command: []string{"sh", "-c", "echo hello"}, Entrypoint: []string{"/bin/bash"}, Env: EnvVars{ "FOO": "bar", "BAZ": "qux", }, Image: "nginx:latest", Init: new(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, Devices: []DeviceMapping{ {HostPath: "/dev/sda", ContainerPath: "/dev/xvda", CgroupPermissions: "rwm"}, }, DeviceReservations: []container.DeviceRequest{ {Count: 1, Capabilities: [][]string{{"gpu"}}, Driver: "nvidia"}, }, }, Sysctls: map[string]string{ "net.ipv4.ip_forward": "1", }, 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.CapAdd[0] = stringModified original.CapDrop[0] = stringModified 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 original.Sysctls["net.ipv4.ip_forward"] = stringModified original.Resources.Devices[0].HostPath = stringModified original.Resources.DeviceReservations[0].Count = 2 original.Resources.DeviceReservations[0].Driver = stringModified assert.False(t, original.Equals(cloned)) // Assert cloned values are unchanged assert.Equal(t, "NET_ADMIN", cloned.CapAdd[0]) assert.Equal(t, "ALL", cloned.CapDrop[0]) 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, "/dev/sda", cloned.Resources.Devices[0].HostPath) assert.Equal(t, 1, cloned.Resources.DeviceReservations[0].Count) assert.Equal(t, "nvidia", cloned.Resources.DeviceReservations[0].Driver) 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") assert.Equal(t, "1", cloned.Sysctls["net.ipv4.ip_forward"]) }