Files
uncloud/pkg/api/service_test.go
T

356 lines
10 KiB
Go

package api
import (
"os"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServiceSpec_Validate_Name(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
wantErr string
}{
{name: "empty allows generated name"},
{name: "single character", input: "a"},
{name: "single digit", input: "1"},
{name: "two characters", input: "a1"},
{name: "hyphens and digits", input: "1-web-2"},
{name: "consecutive hyphens", input: "web--1"},
{name: "maximum length", input: strings.Repeat("a", 63)},
{name: "maximum length with hyphens", input: "a" + strings.Repeat("-", 61) + "1"},
{name: "round-robin mode is a valid service name", input: "rr"},
{name: "nearest mode is a valid service name", input: "nearest"},
{name: "machine namespace prefix", input: "m-service"},
{name: "short hexadecimal name", input: strings.Repeat("a", 31)},
{name: "long hexadecimal name", input: strings.Repeat("a", 33)},
{name: "non-hexadecimal 32 characters", input: strings.Repeat("g", 32)},
{name: "too long", input: strings.Repeat("a", 64), wantErr: "must be 1-63 characters"},
{name: "uppercase", input: "WEB1", wantErr: "lowercase letters"},
{name: "leading hyphen", input: "-web", wantErr: "starting and ending"},
{name: "trailing hyphen", input: "web-", wantErr: "starting and ending"},
{name: "hyphen only", input: "-", wantErr: "starting and ending"},
{name: "underscore", input: "web_1", wantErr: "hyphens only"},
{name: "dot", input: "web.example", wantErr: "hyphens only"},
{name: "slash", input: "web/api", wantErr: "hyphens only"},
{name: "space", input: "web 1", wantErr: "hyphens only"},
{name: "leading whitespace", input: " web", wantErr: "hyphens only"},
{name: "trailing whitespace", input: "web ", wantErr: "hyphens only"},
{name: "tab", input: "web\t1", wantErr: "hyphens only"},
{name: "newline", input: "web\n", wantErr: "hyphens only"},
{name: "non-ASCII", input: "wéb", wantErr: "lowercase letters"},
{name: "machine DNS namespace", input: "m", wantErr: "reserved for the machine DNS namespace"},
{name: "service ID", input: "c337f00600de51ef4375c9a9a267dba5", wantErr: "service ID format"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
spec := ServiceSpec{Name: tt.input, Container: ContainerSpec{Image: "nginx:latest"}}
err := spec.Validate()
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
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"},
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"])
}