Initial push
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShellQuote(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"simple": "simple",
|
||||
"a/b-c_1.2": "a/b-c_1.2",
|
||||
"": "''",
|
||||
"has space": "'has space'",
|
||||
"it's": `'it'\''s'`,
|
||||
"$(rm -rf /)": "'$(rm -rf /)'",
|
||||
"a;b": "'a;b'",
|
||||
"KEY=value": "KEY=value",
|
||||
"tag:1.0@sha256:ab": "tag:1.0@sha256:ab",
|
||||
"back`tick`": "'back`tick`'",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := ShellQuote(in); got != want {
|
||||
t.Errorf("ShellQuote(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateArgsFull checks that a container using most of the surface area of
|
||||
// docker run is rendered back into an equivalent create command.
|
||||
func TestCreateArgsFull(t *testing.T) {
|
||||
stopTimeout := 15
|
||||
initTrue := true
|
||||
c := &Container{
|
||||
Name: "web",
|
||||
Image: "nginx:1.27",
|
||||
Hostname: "web-1",
|
||||
User: "101:101",
|
||||
WorkingDir: "/srv",
|
||||
Env: []string{"TZ=Europe/Paris", "SECRET=a b"},
|
||||
Labels: map[string]string{"team": "infra", "com.docker.compose.project": "shop"},
|
||||
Cmd: []string{"nginx", "-g", "daemon off;"},
|
||||
CmdSet: true,
|
||||
Entrypoint: []string{"/entry.sh", "--flag"},
|
||||
EntrypointSet: true,
|
||||
RestartPolicy: "on-failure",
|
||||
RestartMaxRetries: 3,
|
||||
StopSignal: "SIGQUIT",
|
||||
StopTimeout: &stopTimeout,
|
||||
Init: &initTrue,
|
||||
Privileged: true,
|
||||
CapAdd: []string{"NET_ADMIN"},
|
||||
CapDrop: []string{"MKNOD"},
|
||||
Sysctls: map[string]string{"net.core.somaxconn": "1024"},
|
||||
DNS: []string{"1.1.1.1"},
|
||||
ExtraHosts: []string{"db:10.0.0.5"},
|
||||
NetworkMode: "frontend",
|
||||
Endpoints: []Endpoint{
|
||||
{Network: "frontend", Aliases: []string{"web", "www"}, IPv4Address: "172.20.0.9"},
|
||||
{Network: "backend", Aliases: []string{"web"}},
|
||||
},
|
||||
Ports: []PortBinding{
|
||||
{ContainerPort: "80/tcp", HostIP: "0.0.0.0", HostPort: "8080"},
|
||||
{ContainerPort: "53/udp", HostIP: "127.0.0.1", HostPort: "5353"},
|
||||
},
|
||||
ExposedPorts: []string{"80/tcp", "9000/tcp"},
|
||||
Mounts: []Mount{
|
||||
{Kind: MountVolume, Name: "html", Destination: "/usr/share/nginx/html", ReadOnly: true},
|
||||
{Kind: MountBind, Source: "/etc/nginx/conf.d", Destination: "/etc/nginx/conf.d"},
|
||||
{Kind: MountAnonymous, Name: strings.Repeat("a", 64), Destination: "/cache"},
|
||||
{Kind: MountTmpfs, Destination: "/run", TmpfsOpts: "size=64m"},
|
||||
},
|
||||
LogDriver: "json-file",
|
||||
LogOptions: map[string]string{"max-size": "10m"},
|
||||
Resources: Resources{Memory: 536870912, NanoCPUs: 1500000000, ShmSize: 67108864},
|
||||
}
|
||||
|
||||
got := strings.Join(c.CreateArgs(RenderOptions{}), " ")
|
||||
|
||||
mustContain := []string{
|
||||
"create --name web",
|
||||
"--hostname web-1",
|
||||
"--user 101:101",
|
||||
"--env TZ=Europe/Paris",
|
||||
"--label team=infra",
|
||||
"--restart on-failure:3",
|
||||
"--stop-timeout 15",
|
||||
"--init",
|
||||
"--privileged",
|
||||
"--cap-add NET_ADMIN",
|
||||
"--sysctl net.core.somaxconn=1024",
|
||||
"--add-host db:10.0.0.5",
|
||||
"--network frontend",
|
||||
"--network-alias web",
|
||||
"--publish 8080:80",
|
||||
"--publish 127.0.0.1:5353:53/udp",
|
||||
"--expose 9000/tcp",
|
||||
"--volume html:/usr/share/nginx/html:ro",
|
||||
"--volume /etc/nginx/conf.d:/etc/nginx/conf.d",
|
||||
"--tmpfs /run:size=64m",
|
||||
"--log-opt max-size=10m",
|
||||
"--memory 536870912",
|
||||
"--cpus 1.5",
|
||||
"--entrypoint /entry.sh",
|
||||
"nginx:1.27",
|
||||
}
|
||||
for _, want := range mustContain {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("create args missing %q\ngot: %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Labels docker or compose manage themselves must not be re-applied.
|
||||
if strings.Contains(got, "com.docker.compose.project") {
|
||||
t.Errorf("compose-managed label was re-applied:\n%s", got)
|
||||
}
|
||||
// A port already published must not also be re-exposed.
|
||||
if strings.Contains(got, "--expose 80/tcp") {
|
||||
t.Errorf("published port was also exposed:\n%s", got)
|
||||
}
|
||||
// The default shm size carries no information and should be omitted.
|
||||
if strings.Contains(got, "--shm-size") {
|
||||
t.Errorf("default shm size was emitted:\n%s", got)
|
||||
}
|
||||
// The image must be the last flag-free token before the command.
|
||||
idx := strings.Index(got, "nginx:1.27")
|
||||
if idx < 0 || !strings.Contains(got[idx:], "--flag") {
|
||||
t.Errorf("entrypoint remainder and command must follow the image:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateArgsSecondNetworkNeedsConnect(t *testing.T) {
|
||||
c := &Container{
|
||||
Name: "app", Image: "app:1", NetworkMode: "a",
|
||||
Endpoints: []Endpoint{
|
||||
{Network: "a"},
|
||||
{Network: "b", Aliases: []string{"app-b"}, IPv4Address: "10.1.2.3"},
|
||||
},
|
||||
}
|
||||
args := c.CreateArgs(RenderOptions{})
|
||||
if n := strings.Count(strings.Join(args, " "), "--network "); n != 1 {
|
||||
t.Fatalf("docker create accepts one --network, got %d in %v", n, args)
|
||||
}
|
||||
connects := c.NetworkConnectArgs(RenderOptions{})
|
||||
if len(connects) != 1 {
|
||||
t.Fatalf("expected 1 network connect, got %d", len(connects))
|
||||
}
|
||||
joined := strings.Join(connects[0], " ")
|
||||
if !strings.Contains(joined, "network connect --alias app-b b app") {
|
||||
t.Errorf("unexpected connect args: %s", joined)
|
||||
}
|
||||
if strings.Contains(joined, "--ip ") {
|
||||
t.Errorf("static IP must not be applied unless requested: %s", joined)
|
||||
}
|
||||
|
||||
withIP := strings.Join(c.NetworkConnectArgs(RenderOptions{KeepStaticIPs: true})[0], " ")
|
||||
if !strings.Contains(withIP, "--ip 10.1.2.3") {
|
||||
t.Errorf("static IP was requested but not applied: %s", withIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderOptionsDropAndRename(t *testing.T) {
|
||||
c := &Container{
|
||||
Name: "db", Image: "postgres:16",
|
||||
Ports: []PortBinding{{ContainerPort: "5432/tcp", HostPort: "5432"}},
|
||||
Mounts: []Mount{{Kind: MountVolume, Name: "pgdata", Destination: "/var/lib/postgresql/data"}},
|
||||
Endpoints: []Endpoint{{Network: "backend"}},
|
||||
}
|
||||
got := strings.Join(c.CreateArgs(RenderOptions{
|
||||
NameOverride: "db-new",
|
||||
SkipPorts: true,
|
||||
SkipNetworks: true,
|
||||
DropMounts: map[string]bool{"/var/lib/postgresql/data": true},
|
||||
}), " ")
|
||||
|
||||
if !strings.Contains(got, "--name db-new") {
|
||||
t.Errorf("name override not applied: %s", got)
|
||||
}
|
||||
for _, unwanted := range []string{"--publish", "--network", "--volume"} {
|
||||
if strings.Contains(got, unwanted) {
|
||||
t.Errorf("expected %s to be dropped: %s", unwanted, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCgroupnsPrivateIsNotCarried guards a cross-host hazard: "private" is
|
||||
// simply what a cgroup v2 host reports, and passing it explicitly makes the
|
||||
// create fail on a target whose kernel only has cgroup v1.
|
||||
func TestCgroupnsPrivateIsNotCarried(t *testing.T) {
|
||||
private := &Container{Name: "a", Image: "img", CgroupnsMode: "private"}
|
||||
if got := strings.Join(private.CreateArgs(RenderOptions{}), " "); strings.Contains(got, "--cgroupns") {
|
||||
t.Errorf("the default cgroup namespace must not be pinned: %s", got)
|
||||
}
|
||||
host := &Container{Name: "a", Image: "img", CgroupnsMode: "host"}
|
||||
if got := strings.Join(host.CreateArgs(RenderOptions{}), " "); !strings.Contains(got, "--cgroupns host") {
|
||||
t.Errorf("an explicit host cgroup namespace must be carried across: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoRemoveIsNeverReapplied(t *testing.T) {
|
||||
c := &Container{Name: "job", Image: "busybox", AutoRemove: true}
|
||||
if strings.Contains(strings.Join(c.CreateArgs(RenderOptions{}), " "), "--rm") {
|
||||
t.Error("--rm must not be reapplied; the migrated container would delete itself")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeAndNetworkCreateArgs(t *testing.T) {
|
||||
v := Volume{
|
||||
Name: "pgdata", Driver: "local",
|
||||
DriverOpts: map[string]string{"type": "nfs", "device": ":/exports/pg"},
|
||||
Labels: map[string]string{"app": "shop", "com.docker.compose.project": "x"},
|
||||
}
|
||||
got := strings.Join(v.CreateArgs(), " ")
|
||||
for _, want := range []string{"volume create", "--opt device=:/exports/pg", "--opt type=nfs", "--label app=shop", "pgdata"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("volume args missing %q: %s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "--driver local") {
|
||||
t.Errorf("the default driver should be omitted: %s", got)
|
||||
}
|
||||
if strings.Contains(got, "compose.project") {
|
||||
t.Errorf("compose label must not be reapplied: %s", got)
|
||||
}
|
||||
|
||||
n := Network{
|
||||
Name: "backend", Driver: "bridge", Internal: true, Attachable: true,
|
||||
IPAMPools: []IPAMPool{{Subnet: "172.28.0.0/16", Gateway: "172.28.0.1"}},
|
||||
Options: map[string]string{"com.docker.network.bridge.name": "br-backend"},
|
||||
}
|
||||
gotNet := strings.Join(n.CreateArgs(), " ")
|
||||
for _, want := range []string{"network create", "--driver bridge", "--internal", "--attachable", "--subnet 172.28.0.0/16", "--gateway 172.28.0.1", "backend"} {
|
||||
if !strings.Contains(gotNet, want) {
|
||||
t.Errorf("network args missing %q: %s", want, gotNet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortBindingString(t *testing.T) {
|
||||
cases := []struct {
|
||||
in PortBinding
|
||||
want string
|
||||
}{
|
||||
{PortBinding{ContainerPort: "80/tcp", HostPort: "8080"}, "8080:80"},
|
||||
{PortBinding{ContainerPort: "80/tcp", HostIP: "0.0.0.0", HostPort: "80"}, "80:80"},
|
||||
{PortBinding{ContainerPort: "53/udp", HostIP: "127.0.0.1", HostPort: "5353"}, "127.0.0.1:5353:53/udp"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.in.String(); got != c.want {
|
||||
t.Errorf("PortBinding%+v = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user