mirror of
https://github.com/psviderski/uncloud.git
synced 2026-10-07 05:39:01 +00:00
356 lines
10 KiB
Go
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"])
|
|
}
|