package convert import ( "fmt" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" "github.com/docker/helm-prototype/pkg/compose" "github.com/stretchr/testify/assert" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "os" "runtime" "testing" ) func loadYAML(yaml string) (*compose.Project, error) { dict, err := loader.ParseYAML([]byte(yaml)) if err != nil { return nil, err } workingDir, err := os.Getwd() if err != nil { panic(err) } return compose.NewProject(types.ConfigDetails{ WorkingDir: workingDir, ConfigFiles: []types.ConfigFile{ {Filename: "compose.yaml", Config: dict}, }, }, "test") } func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { res, err := podTemplateWithError(yaml) assert.NoError(t, err) return res } func podTemplateWithError(yaml string) (apiv1.PodTemplateSpec, error) { project, err := loadYAML(yaml) if err != nil { return apiv1.PodTemplateSpec{}, err } return toPodTemplate(project.Services[0], nil, project) } func TestToPodWithDockerSocket(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") return } podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" volumes: - "/var/run/docker.sock:/var/run/docker.sock" `) expectedVolume := apiv1.Volume{ Name: "mount-0", VolumeSource: apiv1.VolumeSource{ HostPath: &apiv1.HostPathVolumeSource{ Path: "/var/run", }, }, } expectedMount := apiv1.VolumeMount{ Name: "mount-0", MountPath: "/var/run/docker.sock", SubPath: "docker.sock", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func TestToPodWithFunkyCommand(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: basi/node-exporter command: ["-collector.procfs", "/host/proc", "-collector.sysfs", "/host/sys"] `) expectedArgs := []string{ `-collector.procfs`, `/host/proc`, // ? `-collector.sysfs`, `/host/sys`, // ? } assert.Equal(t, expectedArgs, podTemplate.Spec.Containers[0].Args) } func TestToPodWithGlobalVolume(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: db: image: "postgres:9.4" volumes: - dbdata:/var/lib/postgresql/data `) expectedMount := apiv1.VolumeMount{ Name: "dbdata", MountPath: "/var/lib/postgresql/data", } assert.Len(t, podTemplate.Spec.Volumes, 0) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func TestToPodWithResources(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: db: image: "postgres:9.4" deploy: resources: limits: cpus: "0.001" memory: 50Mb reservations: cpus: "0.0001" memory: 20Mb `) expectedResourceRequirements := apiv1.ResourceRequirements{ Limits: map[apiv1.ResourceName]resource.Quantity{ apiv1.ResourceCPU: resource.MustParse("0.001"), apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 50*1024*1024)), }, Requests: map[apiv1.ResourceName]resource.Quantity{ apiv1.ResourceCPU: resource.MustParse("0.0001"), apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 20*1024*1024)), }, } assert.Equal(t, expectedResourceRequirements, podTemplate.Spec.Containers[0].Resources) } func TestToPodWithCapabilities(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" cap_add: - ALL cap_drop: - NET_ADMIN - SYS_ADMIN `) expectedSecurityContext := &apiv1.SecurityContext{ Capabilities: &apiv1.Capabilities{ Add: []apiv1.Capability{"ALL"}, Drop: []apiv1.Capability{"NET_ADMIN", "SYS_ADMIN"}, }, } assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) } func TestToPodWithReadOnly(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" read_only: true `) yes := true expectedSecurityContext := &apiv1.SecurityContext{ ReadOnlyRootFilesystem: &yes, } assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) } func TestToPodWithPrivileged(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" privileged: true `) yes := true expectedSecurityContext := &apiv1.SecurityContext{ Privileged: &yes, } assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) } func TestToPodWithEnvNilShouldErrorOut(t *testing.T) { _, err := podTemplateWithError(` version: "3" services: redis: image: "redis:alpine" environment: - SESSION_SECRET `) assert.Error(t, err) } func TestToPodWithEnv(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" environment: - RACK_ENV=development - SHOW=true `) expectedEnv := []apiv1.EnvVar{ { Name: "RACK_ENV", Value: "development", }, { Name: "SHOW", Value: "true", }, } assert.Equal(t, expectedEnv, podTemplate.Spec.Containers[0].Env) } func TestToPodWithVolume(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") return } podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx volumes: - /ignore:/ignore - /opt/data:/var/lib/mysql:ro `) assert.Len(t, podTemplate.Spec.Volumes, 2) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 2) } func /*FIXME Test*/ToPodWithRelativeVolumes(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") return } _, err := podTemplateWithError(` version: "3" services: nginx: image: nginx volumes: - ./fail:/ignore `) assert.Error(t, err) } func TestToPodWithHealthCheck(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 90s timeout: 10s retries: 3 `) expectedLivenessProbe := &apiv1.Probe{ TimeoutSeconds: 10, PeriodSeconds: 90, FailureThreshold: 3, Handler: apiv1.Handler{ Exec: &apiv1.ExecAction{ Command: []string{"curl", "-f", "http://localhost"}, }, }, } assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) } func TestToPodWithShellHealthCheck(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx healthcheck: test: ["CMD-SHELL", "curl -f http://localhost"] `) expectedLivenessProbe := &apiv1.Probe{ TimeoutSeconds: 1, PeriodSeconds: 1, FailureThreshold: 3, Handler: apiv1.Handler{ Exec: &apiv1.ExecAction{ Command: []string{"sh", "-c", "curl -f http://localhost"}, }, }, } assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) } func TestToPodWithTargetlessExternalSecret(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx secrets: - my_secret `) expectedVolume := apiv1.Volume{ Name: "secret-0", VolumeSource: apiv1.VolumeSource{ Secret: &apiv1.SecretVolumeSource{ SecretName: "my_secret", Items: []apiv1.KeyToPath{ { Key: "file", // TODO: This is the key we assume external secrets use Path: "secret-0", }, }, }, }, } expectedMount := apiv1.VolumeMount{ Name: "secret-0", ReadOnly: true, MountPath: "/run/secrets/my_secret", SubPath: "secret-0", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func TestToPodWithExternalSecret(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx secrets: - source: my_secret target: nginx_secret `) expectedVolume := apiv1.Volume{ Name: "secret-0", VolumeSource: apiv1.VolumeSource{ Secret: &apiv1.SecretVolumeSource{ SecretName: "my_secret", Items: []apiv1.KeyToPath{ { Key: "file", // TODO: This is the key we assume external secrets use Path: "secret-0", }, }, }, }, } expectedMount := apiv1.VolumeMount{ Name: "secret-0", ReadOnly: true, MountPath: "/run/secrets/nginx_secret", SubPath: "secret-0", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func /*FIXME Test*/ToPodWithFileBasedSecret(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx secrets: - source: my_secret secrets: my_secret: file: ./secret.txt `) expectedVolume := apiv1.Volume{ Name: "secret-0", VolumeSource: apiv1.VolumeSource{ Secret: &apiv1.SecretVolumeSource{ SecretName: "my_secret", Items: []apiv1.KeyToPath{ { Key: "secret.txt", Path: "secret-0", }, }, }, }, } expectedMount := apiv1.VolumeMount{ Name: "secret-0", ReadOnly: true, MountPath: "/run/secrets/my_secret", SubPath: "secret-0", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func /*FIXME Test*/ToPodWithTwoFileBasedSecrets(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx secrets: - source: my_secret1 - source: my_secret2 target: secret2 secrets: my_secret1: file: ./secret1.txt my_secret2: file: ./secret2.txt `) expectedVolumes := []apiv1.Volume{ { Name: "secret-0", VolumeSource: apiv1.VolumeSource{ Secret: &apiv1.SecretVolumeSource{ SecretName: "my_secret1", Items: []apiv1.KeyToPath{ { Key: "secret1.txt", Path: "secret-0", }, }, }, }, }, { Name: "secret-1", VolumeSource: apiv1.VolumeSource{ Secret: &apiv1.SecretVolumeSource{ SecretName: "my_secret2", Items: []apiv1.KeyToPath{ { Key: "secret2.txt", Path: "secret-1", }, }, }, }, }, } expectedMounts := []apiv1.VolumeMount{ { Name: "secret-0", ReadOnly: true, MountPath: "/run/secrets/my_secret1", SubPath: "secret-0", }, { Name: "secret-1", ReadOnly: true, MountPath: "/run/secrets/secret2", SubPath: "secret-1", }, } assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) } func TestToPodWithTerminationGracePeriod(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" stop_grace_period: 100s `) expected := int64(100) assert.Equal(t, &expected, podTemplate.Spec.TerminationGracePeriodSeconds) } func TestToPodWithTmpfs(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" tmpfs: - /tmp `) expectedVolume := apiv1.Volume{ Name: "tmp-0", VolumeSource: apiv1.VolumeSource{ EmptyDir: &apiv1.EmptyDirVolumeSource{ Medium: "Memory", }, }, } expectedMount := apiv1.VolumeMount{ Name: "tmp-0", MountPath: "/tmp", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func TestToPodWithNumericalUser(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" user: "1000" `) userID := int64(1000) expectedSecurityContext := &apiv1.SecurityContext{ RunAsUser: &userID, } assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) } func TestToPodWithGitVolume(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" volumes: - source: "git@github.com:moby/moby.git" target: /sources type: git `) expectedVolume := apiv1.Volume{ Name: "mount-0", VolumeSource: apiv1.VolumeSource{ GitRepo: &apiv1.GitRepoVolumeSource{ Repository: "git@github.com:moby/moby.git", }, }, } expectedMount := apiv1.VolumeMount{ Name: "mount-0", ReadOnly: false, MountPath: "/sources", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func /*FIXME Test*/ToPodWithFileBasedConfig(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" configs: - source: my_config target: /usr/share/nginx/html/index.html uid: "103" gid: "103" mode: 0440 configs: my_config: file: ./file.html `) mode := int32(0440) expectedVolume := apiv1.Volume{ Name: "config-0", VolumeSource: apiv1.VolumeSource{ ConfigMap: &apiv1.ConfigMapVolumeSource{ LocalObjectReference: apiv1.LocalObjectReference{ Name: "my_config", }, Items: []apiv1.KeyToPath{ { Key: "file.html", Path: "config-0", Mode: &mode, }, }, }, }, } expectedMount := apiv1.VolumeMount{ Name: "config-0", ReadOnly: true, MountPath: "/usr/share/nginx/html/index.html", SubPath: "config-0", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func /*FIXME Test*/ToPodWithTargetlessFileBasedConfig(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" configs: - my_config configs: my_config: file: ./file.html `) expectedVolume := apiv1.Volume{ Name: "config-0", VolumeSource: apiv1.VolumeSource{ ConfigMap: &apiv1.ConfigMapVolumeSource{ LocalObjectReference: apiv1.LocalObjectReference{ Name: "myconfig", }, Items: []apiv1.KeyToPath{ { Key: "file.html", Path: "config-0", }, }, }, }, } expectedMount := apiv1.VolumeMount{ Name: "config-0", ReadOnly: true, MountPath: "/myconfig", SubPath: "config-0", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func TestToPodWithExternalConfig(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: redis: image: "redis:alpine" configs: - source: my_config target: /usr/share/nginx/html/index.html uid: "103" gid: "103" mode: 0440 configs: my_config: external: true `) mode := int32(0440) expectedVolume := apiv1.Volume{ Name: "config-0", VolumeSource: apiv1.VolumeSource{ ConfigMap: &apiv1.ConfigMapVolumeSource{ LocalObjectReference: apiv1.LocalObjectReference{ Name: "my_config", }, Items: []apiv1.KeyToPath{ { Key: "file", // TODO: This is the key we assume external config use Path: "config-0", Mode: &mode, }, }, }, }, } expectedMount := apiv1.VolumeMount{ Name: "config-0", ReadOnly: true, MountPath: "/usr/share/nginx/html/index.html", SubPath: "config-0", } assert.Len(t, podTemplate.Spec.Volumes, 1) assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } func /*FIXME Test*/ToPodWithTwoConfigsSameMountPoint(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx configs: - source: first target: /data/first.json mode: "0440" - source: second target: /data/second.json mode: "0550" configs: first: file: ./file1 secondv: file: ./file2 `) mode0440 := int32(0440) mode0550 := int32(0550) expectedVolumes := []apiv1.Volume{ { Name: "config-0", VolumeSource: apiv1.VolumeSource{ ConfigMap: &apiv1.ConfigMapVolumeSource{ LocalObjectReference: apiv1.LocalObjectReference{ Name: "first", }, Items: []apiv1.KeyToPath{ { Key: "file1", Path: "config-0", Mode: &mode0440, }, }, }, }, }, { Name: "config-1", VolumeSource: apiv1.VolumeSource{ ConfigMap: &apiv1.ConfigMapVolumeSource{ LocalObjectReference: apiv1.LocalObjectReference{ Name: "second", }, Items: []apiv1.KeyToPath{ { Key: "file2", Path: "config-1", Mode: &mode0550, }, }, }, }, }, } expectedMounts := []apiv1.VolumeMount{ { Name: "config-0", ReadOnly: true, MountPath: "/data/first.json", SubPath: "config-0", }, { Name: "config-1", ReadOnly: true, MountPath: "/data/second.json", SubPath: "config-1", }, } assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) } func TestToPodWithTwoExternalConfigsSameMountPoint(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: nginx: image: nginx configs: - source: first target: /data/first.json - source: second target: /data/second.json configs: first: file: ./file1 second: file: ./file2 `) expectedVolumes := []apiv1.Volume{ { Name: "config-0", VolumeSource: apiv1.VolumeSource{ ConfigMap: &apiv1.ConfigMapVolumeSource{ LocalObjectReference: apiv1.LocalObjectReference{ Name: "first", }, Items: []apiv1.KeyToPath{ { Key: "file", Path: "config-0", }, }, }, }, }, { Name: "config-1", VolumeSource: apiv1.VolumeSource{ ConfigMap: &apiv1.ConfigMapVolumeSource{ LocalObjectReference: apiv1.LocalObjectReference{ Name: "second", }, Items: []apiv1.KeyToPath{ { Key: "file", Path: "config-1", }, }, }, }, }, } expectedMounts := []apiv1.VolumeMount{ { Name: "config-0", ReadOnly: true, MountPath: "/data/first.json", SubPath: "config-0", }, { Name: "config-1", ReadOnly: true, MountPath: "/data/second.json", SubPath: "config-1", }, } assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) } func /*FIXME Test*/ToPodWithPullSecret(t *testing.T) { podTemplateWithSecret := podTemplate(t, ` version: "3" services: nginx: image: nginx x-kubernetes.pull-secret: test-pull-secret `) assert.Equal(t, 1, len(podTemplateWithSecret.Spec.ImagePullSecrets)) assert.Equal(t, "test-pull-secret", podTemplateWithSecret.Spec.ImagePullSecrets[0].Name) podTemplateNoSecret := podTemplate(t, ` version: "3" services: nginx: image: nginx `) assert.Nil(t, podTemplateNoSecret.Spec.ImagePullSecrets) } func /*FIXME Test*/ToPodWithPullPolicy(t *testing.T) { cases := []struct { name string stack string expectedPolicy apiv1.PullPolicy expectedError string }{ { name: "specific tag", stack: ` version: "3" services: nginx: image: nginx:specific `, expectedPolicy: apiv1.PullIfNotPresent, }, { name: "latest tag", stack: ` version: "3" services: nginx: image: nginx:latest `, expectedPolicy: apiv1.PullAlways, }, { name: "explicit policy", stack: ` version: "3" services: nginx: image: nginx:specific x-kubernetes.pull-policy: Never `, expectedPolicy: apiv1.PullNever, }, { name: "invalid policy", stack: ` version: "3" services: nginx: image: nginx:specific x-kubernetes.pull-policy: Invalid `, expectedError: `invalid pull policy "Invalid", must be "Always", "IfNotPresent" or "Never"`, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { pod, err := podTemplateWithError(c.stack) if c.expectedError != "" { assert.EqualError(t, err, c.expectedError) } else { assert.NoError(t, err) assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, c.expectedPolicy) } }) } }