diff --git a/docs/user-guide.md b/docs/user-guide.md index 1b8e9ac2..be213f5b 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -211,6 +211,9 @@ The currently supported options are: | kompose.cronjob.schedule | kubernetes cronjob schedule (for example: '1 * * * *') | | kompose.cronjob.concurrency_policy | 'Forbid' / 'Allow' / 'Never' / '' | | kompose.cronjob.backoff_limit | kubernetes cronjob backoff limit (for example: '6') | +| kompose.init.containers.name | kubernetes init container name | +| kompose.init.containers.image | kubernetes init container image | +| kompose.init.containers.command | kubernetes init container commands | **Note**: `kompose.service.type` label should be defined with `ports` only (except for headless service), otherwise `kompose` will fail. @@ -467,6 +470,48 @@ services: labels: kompose.volume.sub-path: pg-data ``` + +- `kompose.init.containers.name` is used to specify the name of the Init Containers for a Pod [Init Container Name](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) + +For example: + +```yaml +version: '3' +services: + example-service: + image: example-image + labels: + kompose.init.containers.name: "initcontainername" +``` + +- `kompose.init.containers.image` defines image to use for the Init Containers [Init Container Image](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) + +For example: + +```yaml +version: '3' +services: + example-service: + image: example-image + labels: + kompose.init.containers.image: perl +``` + + +- `kompose.init.containers.command` defines the command that the Init Containers will run after they are started [Init Container Command](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) + +For example: + +```yaml +version: '3' +services: + example-service: + image: example-image + labels: + kompose.init.containers.command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + kompose.init.containers.image: perl +``` + ## Restart If you want to create normal pods without controller you can use `restart` construct of compose to define that. Follow table below to see what happens on the `restart` value. diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index a101458c..2bdccac1 100644 --- a/pkg/loader/compose/utils.go +++ b/pkg/loader/compose/utils.go @@ -87,6 +87,12 @@ const ( LabelCronJobConcurrencyPolicy = "kompose.cronjob.concurrency_policy" // LabelCronJobBackoffLimit defines the job backoff limit LabelCronJobBackoffLimit = "kompose.cronjob.backoff_limit" + // LabelInitContainerName defines name resource + LabelInitContainerName = "kompose.init.containers.name" + // LabelInitContainerImage defines image to pull + LabelInitContainerImage = "kompose.init.containers.image" + // LabelInitContainerCommand defines commands + LabelInitContainerCommand = "kompose.init.containers.command" ) // load environment variables from compose file diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index d0cd6968..b9cded79 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -654,7 +654,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic if serviceAccountName, ok := service.Labels[compose.LabelServiceAccountName]; ok { template.Spec.ServiceAccountName = serviceAccountName } - + fillInitContainers(template, service) return nil } @@ -986,6 +986,51 @@ func reformatSecretConfigUnderscoreWithDash(secretConfig types.ServiceSecretConf return newSecretConfig } +// fillInitContainers looks for an initContainer resources and its passed as labels +// if there is no image, it does not fill the initContainer +// https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ +func fillInitContainers(template *api.PodTemplateSpec, service kobject.ServiceConfig) { + resourceImage, exist := service.Labels[compose.LabelInitContainerImage] + if !exist || resourceImage == "" { + return + } + resourceName, exist := service.Labels[compose.LabelInitContainerName] + if !exist || resourceName == "" { + resourceName = "init-service" + } + + template.Spec.InitContainers = append(template.Spec.InitContainers, api.Container{ + Name: resourceName, + Command: parseContainerCommandsFromStr(service.Labels[compose.LabelInitContainerCommand]), + Image: resourceImage, + }) +} + +// parseContainerCommandsFromStr parses a string containing comma-separated commands +// returns a slice of strings or a single command +// example: +// [ "bundle", "exec", "thin", "-p", "3000" ] +// +// example: +// [ "bundle exec thin -p 3000" ] +func parseContainerCommandsFromStr(line string) []string { + if line == "" { + return []string{} + } + var commands []string + if strings.Contains(line, ",") { + line = strings.TrimSpace(strings.Trim(line, "[]")) + commands = strings.Split(line, ",") + // remove space "' + for i := range commands { + commands[i] = strings.TrimSpace(strings.Trim(commands[i], `"' `)) + } + } else { + commands = append(commands, line) + } + return commands +} + // isConfigFile checks if the given filePath should be used as a configMap // if dir is not empty, withindir are treated as cofigmaps // if it's configMap, mount readonly as default diff --git a/pkg/transformer/kubernetes/k8sutils_test.go b/pkg/transformer/kubernetes/k8sutils_test.go index c971df92..6bd4bc1b 100644 --- a/pkg/transformer/kubernetes/k8sutils_test.go +++ b/pkg/transformer/kubernetes/k8sutils_test.go @@ -740,6 +740,212 @@ func TestRemoveEmptyInterfaces(t *testing.T) { } } +func Test_parseContainerCommandsFromStr(t *testing.T) { + tests := []struct { + name string + line string + want []string + }{ + { + name: "line command without spaces in between", + line: `[ "bundle", "exec", "thin", "-p", "3000" ]`, + want: []string{ + "bundle", "exec", "thin", "-p", "3000", + }, + }, + { + name: `line command spaces inside ""`, + line: `[ " bundle ", " exec ", " thin ", " -p ", "3000" ]`, + want: []string{ + "bundle", "exec", "thin", "-p", "3000", + }, + }, + { + name: `more use cases for line command spaces inside ""`, + line: `[ " bundle ", "exec ", " thin ", " -p ", "3000 " ]`, + want: []string{ + "bundle", "exec", "thin", "-p", "3000", + }, + }, + { + name: `line command without [] and ""`, + line: `bundle exec thin -p 3000`, + want: []string{ + "bundle exec thin -p 3000", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseContainerCommandsFromStr(tt.line); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseContainerCommandsFromStr() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_fillInitContainers(t *testing.T) { + type args struct { + template *api.PodTemplateSpec + service kobject.ServiceConfig + } + tests := []struct { + name string + args args + want []corev1.Container + }{ + { + name: "Testing init container are generated from labels with ,", + args: args{ + template: &api.PodTemplateSpec{}, + service: kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelInitContainerName: "name", + compose.LabelInitContainerImage: "image", + compose.LabelInitContainerCommand: `[ "bundle", "exec", "thin", "-p", "3000" ]`, + }, + }, + }, + want: []corev1.Container{ + { + Name: "name", + Image: "image", + Command: []string{ + "bundle", "exec", "thin", "-p", "3000", + }, + }, + }, + }, + { + name: "Testing init container are generated from labels without ,", + args: args{ + template: &api.PodTemplateSpec{}, + service: kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelInitContainerName: "name", + compose.LabelInitContainerImage: "image", + compose.LabelInitContainerCommand: `bundle exec thin -p 3000`, + }, + }, + }, + want: []corev1.Container{ + { + Name: "name", + Image: "image", + Command: []string{ + `bundle exec thin -p 3000`, + }, + }, + }, + }, + { + name: `Testing init container with long command with vars inside and ''`, + args: args{ + template: &api.PodTemplateSpec{}, + service: kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelInitContainerName: "init-myservice", + compose.LabelInitContainerImage: "busybox:1.28", + compose.LabelInitContainerCommand: `['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]`, + }, + }, + }, + want: []corev1.Container{ + { + Name: "init-myservice", + Image: "busybox:1.28", + Command: []string{ + "sh", "-c", `until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done`, + }, + }, + }, + }, + { + name: `without image`, + args: args{ + template: &api.PodTemplateSpec{}, + service: kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelInitContainerName: "init-myservice", + compose.LabelInitContainerImage: "", + compose.LabelInitContainerCommand: `['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]`, + }, + }, + }, + want: nil, + }, + { + name: `Testing init container without name`, + args: args{ + template: &api.PodTemplateSpec{}, + service: kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelInitContainerName: "", + compose.LabelInitContainerImage: "busybox:1.28", + compose.LabelInitContainerCommand: `['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]`, + }, + }, + }, + want: []corev1.Container{ + { + Name: "init-service", + Image: "busybox:1.28", + Command: []string{ + "sh", "-c", `until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done`, + }, + }, + }, + }, + { + name: `Testing init container without command`, + args: args{ + template: &api.PodTemplateSpec{}, + service: kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelInitContainerName: "init-service", + compose.LabelInitContainerImage: "busybox:1.28", + compose.LabelInitContainerCommand: ``, + }, + }, + }, + want: []corev1.Container{ + { + Name: "init-service", + Image: "busybox:1.28", + Command: []string{}, + }, + }, + }, + { + name: `Testing init container without command`, + args: args{ + template: &api.PodTemplateSpec{}, + service: kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelInitContainerName: "init-service", + compose.LabelInitContainerImage: "busybox:1.28", + }, + }, + }, + want: []corev1.Container{ + { + Name: "init-service", + Image: "busybox:1.28", + Command: []string{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fillInitContainers(tt.args.template, tt.args.service) + if !reflect.DeepEqual(tt.args.template.Spec.InitContainers, tt.want) { + t.Errorf("Test_fillInitContainers Fail got %v, want %v", tt.args.template.Spec.InitContainers, tt.want) + } + }) + } +} + func Test_setVolumeAccessMode(t *testing.T) { type args struct { mode string diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh index 07a058d0..2037085f 100755 --- a/script/test/cmd/tests_new.sh +++ b/script/test/cmd/tests_new.sh @@ -340,6 +340,11 @@ os_output="$KOMPOSE_ROOT/script/test/fixtures/resources-lowercase/output-os.yaml convert::expect_success "$k8s_cmd" "$k8s_output" || exit 1 convert::expect_success "$os_cmd" "$os_output" || exit 1 +# Test resources to generate initcontainer +k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/initcontainer/compose.yaml convert --stdout --with-kompose-annotation=false" +k8s_output="$KOMPOSE_ROOT/script/test/fixtures/initcontainer/output-k8s.yaml" +convert::expect_success_and_warning "$k8s_cmd" "$k8s_output" || exit 1 + #Test auto configmaps from files/dir k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/configmap-file-configs/compose-1.yaml convert --stdout --with-kompose-annotation=false" k8s_output="$KOMPOSE_ROOT/script/test/fixtures/configmap-file-configs/output-k8s-1.yaml" diff --git a/script/test/fixtures/initcontainer/compose.yaml b/script/test/fixtures/initcontainer/compose.yaml new file mode 100644 index 00000000..dcc8f756 --- /dev/null +++ b/script/test/fixtures/initcontainer/compose.yaml @@ -0,0 +1,8 @@ +version: "3" +services: + web: + image: nginx + labels: + kompose.init.containers.name: "init-myservice" + kompose.init.containers.image: "busybox:1.28" + kompose.init.containers.command: '["sh", "-c", "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]' \ No newline at end of file diff --git a/script/test/fixtures/initcontainer/output-k8s.yaml b/script/test/fixtures/initcontainer/output-k8s.yaml new file mode 100644 index 00000000..68dd7d88 --- /dev/null +++ b/script/test/fixtures/initcontainer/output-k8s.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + io.kompose.service: web + name: web +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: web + template: + metadata: + labels: + io.kompose.network/initcontainer-default: "true" + io.kompose.service: web + spec: + containers: + - image: nginx + name: web + initContainers: + - command: + - sh + - -c + - until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done + image: busybox:1.28 + name: init-myservice + restartPolicy: Always \ No newline at end of file