diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 9c8ccc07..2c59830a 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -102,133 +102,49 @@ func splitImage(image string) (string, string) { return image, "latest" } -// extractImageValues extracts image information from Deployment and StatefulSet objects -func extractImageValues(objects []runtime.Object) map[string]ServiceValues { +// unquoteHelmTemplates removes quotes around Helm template syntax that yaml.Marshal adds +func unquoteHelmTemplates(yamlBytes []byte) []byte { + yamlStr := string(yamlBytes) + + // Remove quotes around any string containing Helm templates + // Handles both single templates and multiple templates in one value (e.g., image: repo:tag) + re := regexp.MustCompile(`['"]([^'"]*\{\{.+?\}\}[^'"]*?)['"]`) + yamlStr = re.ReplaceAllString(yamlStr, "$1") + + return []byte(yamlStr) +} + +// extractValuesFromKomposeObject extracts values from KomposeObject for values.yaml +func extractValuesFromKomposeObject(komposeObject kobject.KomposeObject) map[string]ServiceValues { values := make(map[string]ServiceValues) - for _, obj := range objects { - switch o := obj.(type) { - case *appsv1.Deployment: - serviceName := o.ObjectMeta.Name - if len(o.Spec.Template.Spec.Containers) > 0 { - container := o.Spec.Template.Spec.Containers[0] + for serviceName, service := range komposeObject.ServiceConfigs { + svcValues := ServiceValues{} - repo, tag := splitImage(container.Image) - pullPolicy := string(container.ImagePullPolicy) - if pullPolicy == "" { - pullPolicy = "IfNotPresent" - } + // Extract image + repo, tag := splitImage(service.Image) + svcValues.Image.Repository = repo + svcValues.Image.Tag = tag - svcValues := ServiceValues{} - svcValues.Image.Repository = repo - svcValues.Image.Tag = tag - svcValues.Image.PullPolicy = pullPolicy - - // Extract env vars - svcValues.Env = make(map[string]string) - for _, envVar := range container.Env { - svcValues.Env[envVar.Name] = envVar.Value - } - - values[serviceName] = svcValues - } - - case *appsv1.StatefulSet: - serviceName := o.ObjectMeta.Name - if len(o.Spec.Template.Spec.Containers) > 0 { - container := o.Spec.Template.Spec.Containers[0] - - repo, tag := splitImage(container.Image) - pullPolicy := string(container.ImagePullPolicy) - if pullPolicy == "" { - pullPolicy = "IfNotPresent" - } - - svcValues := ServiceValues{} - svcValues.Image.Repository = repo - svcValues.Image.Tag = tag - svcValues.Image.PullPolicy = pullPolicy - - // Extract env vars - svcValues.Env = make(map[string]string) - for _, envVar := range container.Env { - svcValues.Env[envVar.Name] = envVar.Value - } - - values[serviceName] = svcValues - } + // Extract pull policy + pullPolicy := service.ImagePullPolicy + if pullPolicy == "" { + pullPolicy = "IfNotPresent" } + svcValues.Image.PullPolicy = pullPolicy + + // Extract env vars + svcValues.Env = make(map[string]string) + for _, envVar := range service.Environment { + svcValues.Env[envVar.Name] = envVar.Value + } + + values[serviceName] = svcValues } return values } -// templatizeImage replaces image and imagePullPolicy with Helm template syntax -func templatizeImage(yamlBytes []byte, serviceName string, values map[string]ServiceValues) []byte { - if svcValues, ok := values[serviceName]; ok { - yamlStr := string(yamlBytes) - - // Replace image line - originalImage := svcValues.Image.Repository + ":" + svcValues.Image.Tag - templatedImage := "{{ .Values." + serviceName + ".image.repository }}:{{ .Values." + serviceName + ".image.tag }}" - yamlStr = strings.Replace(yamlStr, "image: "+originalImage, "image: "+templatedImage, 1) - - // Replace imagePullPolicy line - if svcValues.Image.PullPolicy != "" { - originalPolicy := "imagePullPolicy: " + svcValues.Image.PullPolicy - templatedPolicy := "imagePullPolicy: {{ .Values." + serviceName + ".image.pullPolicy }}" - yamlStr = strings.Replace(yamlStr, originalPolicy, templatedPolicy, 1) - } - - return []byte(yamlStr) - } - return yamlBytes -} - -// templatizeEnv replaces env var values with Helm template syntax -func templatizeEnv(yamlBytes []byte, serviceName string, values map[string]ServiceValues) []byte { - if svcValues, ok := values[serviceName]; ok { - yamlStr := string(yamlBytes) - - for envName, envValue := range svcValues.Env { - template := "{{ .Values." + serviceName + ".env." + envName + " | quote }}" - - if envValue != "" { - // Match: name: ENVNAME\nvalue: "VALUE" or value: VALUE - // Replace with: name: ENVNAME\nvalue: TEMPLATE - pattern := `(name: ` + regexp.QuoteMeta(envName) + `)\n(\s+)value: (?:"` + regexp.QuoteMeta(envValue) + `"|` + regexp.QuoteMeta(envValue) + `)` - replacement := `${1}` + "\n" + `${2}value: ` + template - re := regexp.MustCompile(pattern) - yamlStr = re.ReplaceAllString(yamlStr, replacement) - } else { - // For empty values, handle two cases: - // 1. name: ENVNAME\nvalue: "" - // 2. name: ENVNAME\n - name: (no value field) - - // Try to replace existing empty value first - pattern1 := `(name: ` + regexp.QuoteMeta(envName) + `)\n(\s+)value: ""` - replacement1 := `${1}` + "\n" + `${2}value: ` + template - re1 := regexp.MustCompile(pattern1) - newYamlStr := re1.ReplaceAllString(yamlStr, replacement1) - - // If nothing was replaced, insert value field after name - if newYamlStr == yamlStr { - // Match the current list item with its indentation - pattern2 := `(\s+)- (name: ` + regexp.QuoteMeta(envName) + `)\n` - replacement2 := "${1}- ${2}${1} value: " + template + "\n" - re2 := regexp.MustCompile(pattern2) - yamlStr = re2.ReplaceAllString(yamlStr, replacement2) - } else { - yamlStr = newYamlStr - } - } - } - - return []byte(yamlStr) - } - return yamlBytes -} - /** * Generate Helm Chart configuration */ @@ -415,9 +331,9 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje var files []string var imageValues map[string]ServiceValues - // Extract image values for chart templating (before processing) + // Extract values from KomposeObject for values.yaml if opt.CreateChart { - imageValues = extractImageValues(objects) + imageValues = extractValuesFromKomposeObject(komposeObject) } // if asked to print to stdout or to put in single file @@ -437,6 +353,12 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje if err != nil { return fmt.Errorf("error in marshalling the List: %v", err) } + + // Unquote Helm templates if generating chart + if opt.CreateChart { + data = unquoteHelmTemplates(data) + } + // this part add --- which unifies the file data = []byte(fmt.Sprintf("---\n%s", data)) printVal, err := transformer.Print("", dirName, "", data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider) @@ -485,10 +407,9 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje objectMeta = val.FieldByName("ObjectMeta").Interface().(metav1.ObjectMeta) } - // Templatize YAML if generating chart - if opt.CreateChart && imageValues != nil { - data = templatizeEnv(data, objectMeta.Name, imageValues) - data = templatizeImage(data, objectMeta.Name, imageValues) + // Unquote Helm templates if generating chart + if opt.CreateChart { + data = unquoteHelmTemplates(data) } file, err = transformer.Print(objectMeta.Name, finalDirName, strings.ToLower(typeMeta.Kind), data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider) diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 66b44b4b..ee30c990 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -109,10 +109,17 @@ func (k *Kubernetes) CheckUnsupportedKey(komposeObject *kobject.KomposeObject, u } // InitPodSpec creates the pod specification -func (k *Kubernetes) InitPodSpec(name string, image string, pullSecret string) api.PodSpec { +func (k *Kubernetes) InitPodSpec(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) api.PodSpec { + image := service.Image if image == "" { image = name } + + // Inject Helm template for chart generation + if opt.CreateChart { + image = "{{ .Values." + name + ".image.repository }}:{{ .Values." + name + ".image.tag }}" + } + pod := api.PodSpec{ Containers: []api.Container{ { @@ -121,10 +128,11 @@ func (k *Kubernetes) InitPodSpec(name string, image string, pullSecret string) a }, }, } - if pullSecret != "" { + + if service.ImagePullSecret != "" { pod.ImagePullSecrets = []api.LocalObjectReference{ { - Name: pullSecret, + Name: service.ImagePullSecret, }, } } @@ -132,7 +140,7 @@ func (k *Kubernetes) InitPodSpec(name string, image string, pullSecret string) a } // InitPodSpecWithConfigMap creates the pod specification -func (k *Kubernetes) InitPodSpecWithConfigMap(name string, image string, service kobject.ServiceConfig) api.PodSpec { +func (k *Kubernetes) InitPodSpecWithConfigMap(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) api.PodSpec { var volumeMounts []api.VolumeMount var volumes []api.Volume @@ -177,6 +185,16 @@ func (k *Kubernetes) InitPodSpecWithConfigMap(name string, image string, service volumes = append(volumes, cmVol) } + image := service.Image + if image == "" { + image = name + } + + // Inject Helm template for chart generation + if opt.CreateChart { + image = "{{ .Values." + name + ".image.repository }}:{{ .Values." + name + ".image.tag }}" + } + pod := api.PodSpec{ Containers: []api.Container{ { @@ -412,12 +430,12 @@ func (k *Kubernetes) InitConfigMapFromFile(name string, service kobject.ServiceC } // InitD initializes Kubernetes Deployment object -func (k *Kubernetes) InitD(name string, service kobject.ServiceConfig, replicas int) *appsv1.Deployment { +func (k *Kubernetes) InitD(name string, service kobject.ServiceConfig, replicas int, opt kobject.ConvertOptions) *appsv1.Deployment { var podSpec api.PodSpec if len(service.Configs) > 0 { - podSpec = k.InitPodSpecWithConfigMap(name, service.Image, service) + podSpec = k.InitPodSpecWithConfigMap(name, service, opt) } else { - podSpec = k.InitPodSpec(name, service.Image, service.ImagePullSecret) + podSpec = k.InitPodSpec(name, service, opt) } rp := int32(replicas) @@ -468,7 +486,7 @@ func (k *Kubernetes) InitD(name string, service kobject.ServiceConfig, replicas } // InitDS initializes Kubernetes DaemonSet object -func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig) *appsv1.DaemonSet { +func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) *appsv1.DaemonSet { ds := &appsv1.DaemonSet{ TypeMeta: metav1.TypeMeta{ Kind: "DaemonSet", @@ -483,7 +501,7 @@ func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig) *appsv1. MatchLabels: transformer.ConfigLabels(name), }, Template: api.PodTemplateSpec{ - Spec: k.InitPodSpec(name, service.Image, service.ImagePullSecret), + Spec: k.InitPodSpec(name, service, opt), }, }, } @@ -491,12 +509,12 @@ func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig) *appsv1. } // InitSS method initialize a stateful set -func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas int) *appsv1.StatefulSet { +func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas int, opt kobject.ConvertOptions) *appsv1.StatefulSet { var podSpec api.PodSpec if len(service.Configs) > 0 { - podSpec = k.InitPodSpecWithConfigMap(name, service.Image, service) + podSpec = k.InitPodSpecWithConfigMap(name, service, opt) } else { - podSpec = k.InitPodSpec(name, service.Image, service.ImagePullSecret) + podSpec = k.InitPodSpec(name, service, opt) } rp := int32(replicas) ds := &appsv1.StatefulSet{ @@ -523,7 +541,7 @@ func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas } // InitCJ initializes Kubernetes CronJob object -func (k *Kubernetes) InitCJ(name string, service kobject.ServiceConfig, schedule string, concurrencyPolicy batchv1.ConcurrencyPolicy, backoffLimit *int32) *batchv1.CronJob { +func (k *Kubernetes) InitCJ(name string, service kobject.ServiceConfig, schedule string, concurrencyPolicy batchv1.ConcurrencyPolicy, backoffLimit *int32, opt kobject.ConvertOptions) *batchv1.CronJob { cj := &batchv1.CronJob{ TypeMeta: metav1.TypeMeta{ Kind: "CronJob", @@ -540,7 +558,7 @@ func (k *Kubernetes) InitCJ(name string, service kobject.ServiceConfig, schedule Spec: batchv1.JobSpec{ BackoffLimit: backoffLimit, Template: api.PodTemplateSpec{ - Spec: k.InitPodSpec(name, service.Image, service.ImagePullSecret), + Spec: k.InitPodSpec(name, service, opt), }, }, }, @@ -1225,12 +1243,18 @@ func ConfigEnvs(service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]ap // Load up the environment variables for _, v := range service.Environment { if !keysFromEnvFile[v.Name] { - if strings.Contains(v.Value, "run/secrets") { - v.Value = FormatResourceName(v.Value) + value := v.Value + if opt.CreateChart { + // Inject Helm template syntax for chart generation + value = "{{ .Values." + service.Name + ".env." + v.Name + " | quote }}" + } else { + if strings.Contains(value, "run/secrets") { + value = FormatResourceName(value) + } } envs = append(envs, api.EnvVar{ Name: v.Name, - Value: v.Value, + Value: value, }) } } @@ -1347,15 +1371,15 @@ func (k *Kubernetes) CreateWorkloadAndConfigMapObjects(name string, service kobj } if opt.CreateD || opt.Controller == DeploymentController { - objects = append(objects, k.InitD(name, service, replica)) + objects = append(objects, k.InitD(name, service, replica, opt)) } if opt.CreateDS || opt.Controller == DaemonSetController { - objects = append(objects, k.InitDS(name, service)) + objects = append(objects, k.InitDS(name, service, opt)) } if opt.Controller == StatefulStateController { - objects = append(objects, k.InitSS(name, service, replica)) + objects = append(objects, k.InitSS(name, service, replica, opt)) } envConfigMaps := k.PargeEnvFiletoConfigMaps(name, service, opt) @@ -1392,7 +1416,7 @@ func (k *Kubernetes) createConfigMapFromComposeConfig(name string, service kobje } // InitPod initializes Kubernetes Pod object -func (k *Kubernetes) InitPod(name string, service kobject.ServiceConfig) *api.Pod { +func (k *Kubernetes) InitPod(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) *api.Pod { pod := api.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", @@ -1403,7 +1427,7 @@ func (k *Kubernetes) InitPod(name string, service kobject.ServiceConfig) *api.Po Labels: transformer.ConfigLabels(name), Annotations: transformer.ConfigAnnotations(service), }, - Spec: k.InitPodSpec(name, service.Image, service.ImagePullSecret), + Spec: k.InitPodSpec(name, service, opt), } return &pod } @@ -1692,10 +1716,10 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() { if service.CronJobSchedule != "" { log.Infof("Create kubernetes pod instead of pod controller due to restart policy: %s", service.Restart) - cronJob := k.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit) + cronJob := k.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit, opt) objects = append(objects, cronJob) } else { - pod := k.InitPod(name, service) + pod := k.InitPod(name, service, opt) objects = append(objects, pod) } envConfigMaps := k.PargeEnvFiletoConfigMaps(name, service, opt) diff --git a/pkg/transformer/kubernetes/kubernetes_test.go b/pkg/transformer/kubernetes/kubernetes_test.go index f2910821..99c58c8f 100644 --- a/pkg/transformer/kubernetes/kubernetes_test.go +++ b/pkg/transformer/kubernetes/kubernetes_test.go @@ -611,7 +611,9 @@ func TestRestartOnFailure(t *testing.T) { func TestInitPodSpec(t *testing.T) { name := "foo" k := Kubernetes{} - result := k.InitPodSpec(name, newServiceConfig().Image, "") + service := newServiceConfig() + opt := kobject.ConvertOptions{} + result := k.InitPodSpec(name, service, opt) if result.Containers[0].Name != "foo" && result.Containers[0].Image != "image" { t.Fatalf("Pod object not found") } diff --git a/pkg/transformer/openshift/openshift.go b/pkg/transformer/openshift/openshift.go index 2947de4e..efb08b56 100644 --- a/pkg/transformer/openshift/openshift.go +++ b/pkg/transformer/openshift/openshift.go @@ -151,7 +151,7 @@ func initBuildConfig(name string, service kobject.ServiceConfig, repo string, br } // initDeploymentConfig initializes OpenShifts DeploymentConfig object -func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceConfig, replicas int) *deployapi.DeploymentConfig { +func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceConfig, replicas int, opt kobject.ConvertOptions) *deployapi.DeploymentConfig { containerName := []string{name} // Properly add tags to the image name @@ -164,9 +164,9 @@ func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceCon var podSpec corev1.PodSpec if len(service.Configs) > 0 { - podSpec = o.InitPodSpecWithConfigMap(name, " ", service) + podSpec = o.InitPodSpecWithConfigMap(name, service, opt) } else { - podSpec = o.InitPodSpec(name, " ", "") + podSpec = o.InitPodSpec(name, service, opt) } dc := &deployapi.DeploymentConfig{ @@ -333,10 +333,10 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C } if service.CronJobSchedule != "" { - cronJob := o.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit) + cronJob := o.InitCJ(name, service, service.CronJobSchedule, service.CronJobConcurrencyPolicy, service.CronJobBackoffLimit, opt) objects = append(objects, cronJob) } else { - pod := o.InitPod(name, service) + pod := o.InitPod(name, service, opt) objects = append(objects, pod) } @@ -346,7 +346,7 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C objects = o.CreateWorkloadAndConfigMapObjects(name, service, opt) if opt.CreateDeploymentConfig { - objects = append(objects, o.initDeploymentConfig(name, service, replica)) // OpenShift DeploymentConfigs + objects = append(objects, o.initDeploymentConfig(name, service, replica, opt)) // OpenShift DeploymentConfigs // create ImageStream after deployment (creating IS will trigger new deployment) objects = append(objects, o.initImageStream(name, service, opt)) } diff --git a/pkg/transformer/openshift/openshift_test.go b/pkg/transformer/openshift/openshift_test.go index 57813ebf..e326ba5e 100644 --- a/pkg/transformer/openshift/openshift_test.go +++ b/pkg/transformer/openshift/openshift_test.go @@ -75,7 +75,7 @@ func TestOpenShiftUpdateKubernetesObjects(t *testing.T) { serviceConfig := newServiceConfig() opt := kobject.ConvertOptions{} - object = append(object, o.initDeploymentConfig("foobar", serviceConfig, 3)) + object = append(object, o.initDeploymentConfig("foobar", serviceConfig, 3, opt)) o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object) for _, obj := range object { @@ -95,7 +95,8 @@ func TestOpenShiftUpdateKubernetesObjects(t *testing.T) { func TestInitDeploymentConfig(t *testing.T) { o := OpenShift{} - spec := o.initDeploymentConfig("foobar", newServiceConfig(), 1) + opt := kobject.ConvertOptions{} + spec := o.initDeploymentConfig("foobar", newServiceConfig(), 1, opt) // Check that "foobar" is used correctly as a name if spec.Spec.Template.Spec.Containers[0].Name != "foobar" {