From 3074b5451db53bd1d019e370d407db5d0a729f65 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 27 Nov 2025 10:52:57 +0530 Subject: [PATCH] Generate template placeholders for env vars and images in charts --- pkg/app/app.go | 2 +- pkg/transformer/kubernetes/k8sutils.go | 223 ++++++++++++++++++++++++- 2 files changed, 216 insertions(+), 9 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 64f1ba13..1a77a811 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -261,7 +261,7 @@ func Convert(opt kobject.ConvertOptions) ([]runtime.Object, error) { } // Print output - err = kubernetes.PrintList(objects, opt) + err = kubernetes.PrintList(objects, opt, komposeObject) if err != nil { log.Fatalf(err.Error()) } diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 82421630..9c8ccc07 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -82,10 +82,157 @@ type DeploymentMapping struct { TargetDeploymentName string } +// ServiceValues holds the extracted values for templating +type ServiceValues struct { + Image struct { + Repository string + Tag string + PullPolicy string + } + Env map[string]string +} + +// splitImage splits "repo:tag" or "repo" into repository and tag +func splitImage(image string) (string, string) { + parts := strings.Split(image, ":") + if len(parts) == 2 { + return parts[0], parts[1] + } + // No tag specified, use "latest" + return image, "latest" +} + +// extractImageValues extracts image information from Deployment and StatefulSet objects +func extractImageValues(objects []runtime.Object) 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] + + 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 + } + + 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 + } + } + } + + 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 */ -func generateHelm(dirName string) error { +func generateHelm(dirName string, values map[string]ServiceValues) error { type ChartDetails struct { Name string } @@ -141,10 +288,61 @@ home: return err } + /* Create the values.yaml file */ + if len(values) > 0 { + valuesYAML, err := generateValuesYAML(values) + if err != nil { + return errors.Wrap(err, "Failed to generate values.yaml") + } + err = os.WriteFile(dirName+string(os.PathSeparator)+"values.yaml", valuesYAML, 0644) + if err != nil { + return err + } + } + log.Infof("chart created in %q\n", dirName+string(os.PathSeparator)) return nil } +// generateValuesYAML creates values.yaml content from extracted values +func generateValuesYAML(values map[string]ServiceValues) ([]byte, error) { + // Build hierarchical structure: serviceName -> image -> {repository, tag, pullPolicy} + valuesMap := make(map[string]interface{}) + + // Sort service names for consistent output + serviceNames := make([]string, 0, len(values)) + for name := range values { + serviceNames = append(serviceNames, name) + } + sort.Strings(serviceNames) + + for _, serviceName := range serviceNames { + svcValues := values[serviceName] + serviceMap := map[string]interface{}{ + "image": map[string]string{ + "repository": svcValues.Image.Repository, + "tag": svcValues.Image.Tag, + "pullPolicy": svcValues.Image.PullPolicy, + }, + } + + // Add env vars if present + if len(svcValues.Env) > 0 { + serviceMap["env"] = svcValues.Env + } + + valuesMap[serviceName] = serviceMap + } + + // Use marshalWithIndent for consistent 2-space indentation + yamlBytes, err := marshalWithIndent(valuesMap, 2) + if err != nil { + return nil, err + } + + return yamlBytes, nil +} + // Check if given path is a directory func isDir(name string) (bool, error) { // Open file to get stat later @@ -183,7 +381,7 @@ func getDirName(opt kobject.ConvertOptions) string { } // PrintList will take the data converted and decide on the commandline attributes given -func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { +func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObject kobject.KomposeObject) error { var f *os.File dirName := getDirName(opt) log.Debugf("Target Dir: %s", dirName) @@ -215,6 +413,13 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { } var files []string + var imageValues map[string]ServiceValues + + // Extract image values for chart templating (before processing) + if opt.CreateChart { + imageValues = extractImageValues(objects) + } + // if asked to print to stdout or to put in single file // we will create a list if opt.ToStdout || f != nil { @@ -265,6 +470,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { var typeMeta metav1.TypeMeta var objectMeta metav1.ObjectMeta + // Get object metadata first for templating if us, ok := v.(*unstructured.Unstructured); ok { typeMeta = metav1.TypeMeta{ Kind: us.GetKind(), @@ -275,15 +481,16 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { } } else { val := reflect.ValueOf(v).Elem() - // Use reflect to access TypeMeta struct inside runtime.Object. - // cast it to correct type - metav1.TypeMeta typeMeta = val.FieldByName("TypeMeta").Interface().(metav1.TypeMeta) - - // Use reflect to access ObjectMeta struct inside runtime.Object. - // cast it to correct type - api.ObjectMeta 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) + } + file, err = transformer.Print(objectMeta.Name, finalDirName, strings.ToLower(typeMeta.Kind), data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider) if err != nil { return errors.Wrap(err, "transformer.Print failed") @@ -293,7 +500,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error { } } if opt.CreateChart { - err = generateHelm(dirName) + err = generateHelm(dirName, imageValues) if err != nil { return errors.Wrap(err, "generateHelm failed") }