diff --git a/docs/user-guide.md b/docs/user-guide.md index b833dc80..ae9c5a1a 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 | | kompose.hpa.replicas.min | defines Horizontal Pod Autoscaler minimum number of pod replicas | | kompose.hpa.replicas.max | defines Horizontal Pod Autoscaler maximum number of pod replicas | | kompose.hpa.cpu | defines Horizontal Pod Autoscaler cpu utilization trigger | @@ -472,6 +475,48 @@ services: 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 +``` + + - `kompose.hpa.replicas.min` defines the floor for the number of replicas that the HPA can scale down to during a scaling event. Default value is set to 1. This means that, regardless of the load on the system, the HPA will always maintain at least one replica. More info: [HPA Min Replicas](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). For example: diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index 8087ca54..92b4062c 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" // LabelHpaMinReplicas defines min pod replicas LabelHpaMinReplicas = "kompose.hpa.replicas.min" // LabelHpaMaxReplicas defines max pod replicas diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index e2796137..7dc0fe3b 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -678,7 +678,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 } @@ -1010,6 +1010,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 +} + // searchHPAValues is useful to check if labels // contains any labels related to Horizontal Pod Autoscaler func searchHPAValues(labels map[string]string) bool { diff --git a/pkg/transformer/kubernetes/k8sutils_test.go b/pkg/transformer/kubernetes/k8sutils_test.go index fb72183e..dfa4817a 100644 --- a/pkg/transformer/kubernetes/k8sutils_test.go +++ b/pkg/transformer/kubernetes/k8sutils_test.go @@ -29,6 +29,7 @@ import ( "github.com/kubernetes/kompose/pkg/testutils" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" + api "k8s.io/api/core/v1" hpa "k8s.io/api/autoscaling/v2beta2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -741,6 +742,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_getHpaValue(t *testing.T) { type args struct { service *kobject.ServiceConfig diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh index 3c3ac820..baeab0e9 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 HPA k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/hpa/compose.yaml convert --stdout --with-kompose-annotation=false" k8s_output="$KOMPOSE_ROOT/script/test/fixtures/hpa/output-k8s.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