From 3074b5451db53bd1d019e370d407db5d0a729f65 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 27 Nov 2025 10:52:57 +0530 Subject: [PATCH 01/10] 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") } -- 2.45.2 From 408cd36ad30e2d06d911cfd00c6250b081b6c5f4 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 27 Nov 2025 12:23:38 +0530 Subject: [PATCH 02/10] Inject templates while building the chart --- pkg/transformer/kubernetes/k8sutils.go | 167 +++++------------- pkg/transformer/kubernetes/kubernetes.go | 72 +++++--- pkg/transformer/kubernetes/kubernetes_test.go | 4 +- pkg/transformer/openshift/openshift.go | 12 +- pkg/transformer/openshift/openshift_test.go | 5 +- 5 files changed, 104 insertions(+), 156 deletions(-) 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" { -- 2.45.2 From 83fe6f7cb3b87532e990cc99760917339582db34 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 27 Nov 2025 14:52:56 +0530 Subject: [PATCH 03/10] Mount executables with required mode --- pkg/transformer/kubernetes/kubernetes.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index ee30c990..cb7883ad 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -347,6 +347,10 @@ func (k *Kubernetes) IntiConfigMapFromFileOrDir(name, cmName, filePath string, s configMap.Annotations = map[string]string{ "use-subpath": "true", } + // Check if the file is executable and store that info + if mode&0111 != 0 { // Check if any execute bit is set + configMap.Annotations["executable"] = "true" + } } return configMap, nil @@ -1154,6 +1158,13 @@ func (k *Kubernetes) ConfigEmptyVolumeSource(key string) *api.VolumeSource { func (k *Kubernetes) ConfigConfigMapVolumeSource(cmName string, targetPath string, cm *api.ConfigMap) *api.VolumeSource { s := api.ConfigMapVolumeSource{} s.Name = cmName + + // Set default mode to 0755 if the file is executable + if cm.Annotations != nil && cm.Annotations["executable"] == "true" { + mode := int32(0755) + s.DefaultMode = &mode + } + if useSubPathMount(cm) { var keys []string for k := range cm.Data { -- 2.45.2 From 11bed62f52667310294ccf2e572e2062fdfa7c6f Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Thu, 27 Nov 2025 16:34:48 +0530 Subject: [PATCH 04/10] Fix service name handling in generated templates --- pkg/transformer/kubernetes/k8sutils.go | 25 +++++++++++++++++++----- pkg/transformer/kubernetes/kubernetes.go | 17 +++++++++++++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 2c59830a..5a48049f 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -105,13 +105,28 @@ func splitImage(image string) (string, string) { // unquoteHelmTemplates removes quotes around Helm template syntax that yaml.Marshal adds func unquoteHelmTemplates(yamlBytes []byte) []byte { yamlStr := string(yamlBytes) + lines := strings.Split(yamlStr, "\n") - // 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") + for i, line := range lines { + // If line contains Helm template syntax {{...}} + if strings.Contains(line, "{{") && strings.Contains(line, "}}") { + // Remove outer quotes only - find the first and last quote on the line + // that enclose the entire value (after the colon for YAML) + if idx := strings.Index(line, ": "); idx != -1 { + prefix := line[:idx+2] // Keep "key: " + value := line[idx+2:] // Get the value part - return []byte(yamlStr) + // Remove surrounding quotes from value if present + if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) || + (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { + value = value[1 : len(value)-1] + } + lines[i] = prefix + value + } + } + } + + return []byte(strings.Join(lines, "\n")) } // extractValuesFromKomposeObject extracts values from KomposeObject for values.yaml diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index cb7883ad..2f6f828a 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -57,6 +57,17 @@ type Kubernetes struct { Opt kobject.ConvertOptions } +// helmValuesPath generates a Helm template path using index notation +// This works for all service names including those with hyphens +func helmValuesPath(parts ...string) string { + quotedParts := make([]string, len(parts)+1) + quotedParts[0] = ".Values" + for i, part := range parts { + quotedParts[i+1] = `"` + part + `"` + } + return "index " + strings.Join(quotedParts, " ") +} + // PVCRequestSize (Persistent Volume Claim) has default size const PVCRequestSize = "100Mi" @@ -117,7 +128,7 @@ func (k *Kubernetes) InitPodSpec(name string, service kobject.ServiceConfig, opt // Inject Helm template for chart generation if opt.CreateChart { - image = "{{ .Values." + name + ".image.repository }}:{{ .Values." + name + ".image.tag }}" + image = "{{ " + helmValuesPath(name, "image", "repository") + " }}:{{ " + helmValuesPath(name, "image", "tag") + " }}" } pod := api.PodSpec{ @@ -192,7 +203,7 @@ func (k *Kubernetes) InitPodSpecWithConfigMap(name string, service kobject.Servi // Inject Helm template for chart generation if opt.CreateChart { - image = "{{ .Values." + name + ".image.repository }}:{{ .Values." + name + ".image.tag }}" + image = "{{ " + helmValuesPath(name, "image", "repository") + " }}:{{ " + helmValuesPath(name, "image", "tag") + " }}" } pod := api.PodSpec{ @@ -1257,7 +1268,7 @@ func ConfigEnvs(service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]ap value := v.Value if opt.CreateChart { // Inject Helm template syntax for chart generation - value = "{{ .Values." + service.Name + ".env." + v.Name + " | quote }}" + value = "{{ " + helmValuesPath(service.Name, "env", v.Name) + " | quote }}" } else { if strings.Contains(value, "run/secrets") { value = FormatResourceName(value) -- 2.45.2 From c82b499b010db3cc562ecfb650b2fcc9a2eeeae9 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Mon, 1 Dec 2025 14:07:38 +0530 Subject: [PATCH 05/10] Skip docs workflow --- .github/workflows/go.yml | 2 +- .github/workflows/test.yml | 42 +++++++++++++------------- pkg/transformer/kubernetes/k8sutils.go | 4 +-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 18acebc1..92ba708f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,7 +29,7 @@ jobs: run: make bin - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: "kompose" path: "kompose" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e178aaf3..9b209208 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Kompose CI on: push: branches: - - main + - main pull_request: env: # Avoid noisy outputs like "tput: No value for $TERM and no -T specified" @@ -27,23 +27,23 @@ jobs: - name: Perform cross compile if: ${{ matrix.cross_compile }} run: make cross - docs: - name: Build docs and Coveralls integration - runs-on: ubuntu-latest - needs: test - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ^1.21 - - name: Install dyff - run: go install github.com/homeport/dyff/cmd/dyff@v1.5.8 - - name: Create .coverprofile for each targeted directory by re:running tests - run: make test - - name: Collect all .coverprofile files and save it to one file gover.coverprofile - run: gover - - name: Send coverage - run: goveralls -coverprofile=gover.coverprofile -service=github - env: - # As per https://github.com/mattn/goveralls#github-actions - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # docs: + # name: Build docs and Coveralls integration + # runs-on: ubuntu-latest + # needs: test + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-go@v5 + # with: + # go-version: ^1.21 + # - name: Install dyff + # run: go install github.com/homeport/dyff/cmd/dyff@v1.5.8 + # - name: Create .coverprofile for each targeted directory by re:running tests + # run: make test + # - name: Collect all .coverprofile files and save it to one file gover.coverprofile + # run: gover + # - name: Send coverage + # run: goveralls -coverprofile=gover.coverprofile -service=github + # env: + # # As per https://github.com/mattn/goveralls#github-actions + # COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 5a48049f..aad645d0 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -114,11 +114,11 @@ func unquoteHelmTemplates(yamlBytes []byte) []byte { // that enclose the entire value (after the colon for YAML) if idx := strings.Index(line, ": "); idx != -1 { prefix := line[:idx+2] // Keep "key: " - value := line[idx+2:] // Get the value part + value := line[idx+2:] // Get the value part // Remove surrounding quotes from value if present if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) || - (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { + (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { value = value[1 : len(value)-1] } lines[i] = prefix + value -- 2.45.2 From 386e2b9ec39a3d5f49e88a5ca11b44f93ba681bc Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Mon, 1 Dec 2025 14:28:07 +0530 Subject: [PATCH 06/10] Fix failing openshift tests --- pkg/transformer/openshift/openshift.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/transformer/openshift/openshift.go b/pkg/transformer/openshift/openshift.go index efb08b56..17f7d620 100644 --- a/pkg/transformer/openshift/openshift.go +++ b/pkg/transformer/openshift/openshift.go @@ -169,6 +169,11 @@ func (o *OpenShift) initDeploymentConfig(name string, service kobject.ServiceCon podSpec = o.InitPodSpec(name, service, opt) } + // OpenShift: Set image to a space - actual image comes from ImageStream trigger + for i := range podSpec.Containers { + podSpec.Containers[i].Image = " " + } + dc := &deployapi.DeploymentConfig{ TypeMeta: kapi.TypeMeta{ Kind: "DeploymentConfig", -- 2.45.2 From 1ff8edfaf063d07c917bb3d0190f3a8a41bf932c Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Fri, 28 Nov 2025 14:17:46 +0530 Subject: [PATCH 07/10] Add support for init containers and mounting secrets --- pkg/kobject/kobject.go | 2 + pkg/loader/compose/compose.go | 22 +++- pkg/loader/compose/utils.go | 6 ++ pkg/transformer/kubernetes/k8sutils.go | 102 +++++++++++++++++- pkg/transformer/kubernetes/kubernetes.go | 112 ++++++++++++++++---- pkg/transformer/openshift/openshift.go | 2 +- pkg/transformer/openshift/openshift_test.go | 5 +- 7 files changed, 223 insertions(+), 28 deletions(-) diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index 18c431eb..eb8a4aae 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -233,6 +233,8 @@ type Volumes struct { PVCName string // name of PVC PVCSize string // PVC size SelectorValue string // Value of the label selector + VolumeType string // Type of volume (e.g., "secret") + SecretName string // Name of Kubernetes Secret (if VolumeType is "secret") } // Placement holds the placement struct of container diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go index ca1f77ab..546fe6ef 100644 --- a/pkg/loader/compose/compose.go +++ b/pkg/loader/compose/compose.go @@ -839,12 +839,14 @@ func handleVolume(komposeObject *kobject.KomposeObject, volumes *types.Volumes) errors.Wrap(err, "could not retrieve vvolume") } for volName, vol := range vols { - size, selector := getVolumeLabels(vol.VolumeName, volumes) - if len(size) > 0 || len(selector) > 0 { + size, selector, volumeType, secretName := getVolumeLabels(vol.VolumeName, volumes) + if len(size) > 0 || len(selector) > 0 || len(volumeType) > 0 || len(secretName) > 0 { // We can't assign value to struct field in map while iterating over it, so temporary variable `temp` is used here var temp = vols[volName] temp.PVCSize = size temp.SelectorValue = selector + temp.VolumeType = volumeType + temp.SecretName = secretName vols[volName] = temp } } @@ -949,20 +951,30 @@ func getVol(toFind kobject.Volumes, Vols []kobject.Volumes) (bool, kobject.Volum return false, kobject.Volumes{} } -func getVolumeLabels(name string, volumes *types.Volumes) (string, string) { - size, selector := "", "" +func getVolumeLabels(name string, volumes *types.Volumes) (string, string, string, string) { + size, selector, volumeType, secretName := "", "", "", "" if volume, ok := (*volumes)[name]; ok { + log.Debugf("Getting labels for volume %s, labels: %v", name, volume.Labels) for key, value := range volume.Labels { if key == "kompose.volume.size" { size = value } else if key == "kompose.volume.selector" { selector = value + } else if key == "kompose.volume.type" { + volumeType = value + } else if key == "kompose.volume.secret-name" { + secretName = value } } + if volumeType != "" { + log.Infof("Volume %s has type %s, secret name: %s", name, volumeType, secretName) + } + } else { + log.Debugf("Volume %s not found in volumes map", name) } - return size, selector + return size, selector, volumeType, secretName } // getGroupAdd will return group in int64 format diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index 38712ea2..4c562cae 100644 --- a/pkg/loader/compose/utils.go +++ b/pkg/loader/compose/utils.go @@ -94,6 +94,12 @@ const ( LabelInitContainerImage = "kompose.init.containers.image" // LabelInitContainerCommand defines commands LabelInitContainerCommand = "kompose.init.containers.command" + // LabelInitContainerService defines service(s) to use as init containers (comma-separated) + LabelInitContainerService = "kompose.init.containers.service" + // LabelVolumeType defines the type of volume (e.g., "secret" for Kubernetes Secret volumes) + LabelVolumeType = "kompose.volume.type" + // LabelVolumeSecretName defines the name of the Kubernetes Secret to use + LabelVolumeSecretName = "kompose.volume.secret-name" // 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 aad645d0..fb4802d1 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -689,7 +689,7 @@ func (k *Kubernetes) UpdateKubernetesObjectsMultipleContainers(name string, serv } // UpdateKubernetesObjects loads configurations to k8s objects -func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object) error { +func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object, komposeObject *kobject.KomposeObject) error { // Configure the environment variables. envs, envsFrom, err := ConfigEnvs(service, opt) if err != nil { @@ -729,6 +729,101 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic // Configure annotations annotations := transformer.ConfigAnnotations(service) + // Process init container services if specified + var initContainers []api.Container + if initServicesStr, ok := service.Labels[compose.LabelInitContainerService]; ok && initServicesStr != "" && komposeObject != nil { + initServiceNames := strings.Split(initServicesStr, ",") + for _, initSvcName := range initServiceNames { + initSvcName = strings.TrimSpace(initSvcName) + if initSvcName == "" { + continue + } + + // Find the init container service in the kompose object + initService, exists := komposeObject.ServiceConfigs[initSvcName] + if !exists { + log.Warnf("Init container service %s not found for service %s", initSvcName, name) + continue + } + + log.Infof("Adding init container %s to service %s", initSvcName, name) + + // Configure init container environment variables + initEnvs, initEnvsFrom, err := ConfigEnvs(initService, opt) + if err != nil { + return errors.Wrapf(err, "Unable to load env variables for init container %s", initSvcName) + } + + // Configure init container volumes + initVolumeMounts, initVolumes, initPvc, initCms, err := k.ConfigVolumes(initSvcName, initService) + if err != nil { + return errors.Wrapf(err, "k.ConfigVolumes failed for init container %s", initSvcName) + } + + // Add init container volumes to the main volumes list (deduplicate by name) + existingVolumeNames := make(map[string]bool) + for _, v := range volumes { + existingVolumeNames[v.Name] = true + } + for _, initVol := range initVolumes { + if !existingVolumeNames[initVol.Name] { + volumes = append(volumes, initVol) + existingVolumeNames[initVol.Name] = true + } + } + + // Add init container PVCs and ConfigMaps to objects (deduplicate by name) + // Build a set of existing object names + existingObjectNames := make(map[string]bool) + for _, obj := range *objects { + switch o := obj.(type) { + case *api.PersistentVolumeClaim: + existingObjectNames[o.Name] = true + case *api.ConfigMap: + existingObjectNames[o.Name] = true + } + } + + if initPvc != nil { + for _, p := range initPvc { + if !existingObjectNames[p.Name] { + *objects = append(*objects, p) + existingObjectNames[p.Name] = true + } + } + } + for _, c := range initCms { + if !existingObjectNames[c.Name] { + *objects = append(*objects, c) + existingObjectNames[c.Name] = true + } + } + + // Get init container image + initImage := initService.Image + if initImage == "" { + initImage = initSvcName + } + if opt.CreateChart { + initImage = "{{ " + helmValuesPath(initSvcName, "image", "repository") + " }}:{{ " + helmValuesPath(initSvcName, "image", "tag") + " }}" + } + + // Create init container spec + initContainer := api.Container{ + Name: FormatContainerName(initSvcName), + Image: initImage, + Command: initService.Command, + Args: GetContainerArgs(initService), + Env: initEnvs, + EnvFrom: initEnvsFrom, + VolumeMounts: initVolumeMounts, + WorkingDir: initService.WorkingDir, + } + + initContainers = append(initContainers, initContainer) + } + } + // fillTemplate fills the pod template with the value calculated from config fillTemplate := func(template *api.PodTemplateSpec) error { template.Spec.Containers[0].Name = GetContainerName(service) @@ -865,6 +960,11 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic template.Spec.ServiceAccountName = serviceAccountName } fillInitContainers(template, service) + + // Add init containers from referenced services + if len(initContainers) > 0 { + template.Spec.InitContainers = append(template.Spec.InitContainers, initContainers...) + } return nil } diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 2f6f828a..c674b34d 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -72,7 +72,7 @@ func helmValuesPath(parts ...string) string { const PVCRequestSize = "100Mi" // ValidVolumeSet has the different types of valid volumes -var ValidVolumeSet = map[string]struct{}{"emptyDir": {}, "hostPath": {}, "configMap": {}, "persistentVolumeClaim": {}} +var ValidVolumeSet = map[string]struct{}{"emptyDir": {}, "hostPath": {}, "configMap": {}, "persistentVolumeClaim": {}, "secret": {}} const ( // DeploymentController is controller type for Deployment @@ -1022,13 +1022,15 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( } // Override volume type if specified in service labels. - if vt, ok := service.Labels["kompose.volume.type"]; ok { + var useSecret bool + if vt, ok := service.Labels[compose.LabelVolumeType]; ok { if _, okk := ValidVolumeSet[vt]; !okk { - return nil, nil, nil, nil, fmt.Errorf("invalid volume type %s specified in label 'kompose.volume.type' in service %s", vt, service.Name) + return nil, nil, nil, nil, fmt.Errorf("invalid volume type %s specified in label '%s' in service %s", vt, compose.LabelVolumeType, service.Name) } useEmptyVolumes = vt == "emptyDir" useHostPath = vt == "hostPath" useConfigMap = vt == "configMap" + useSecret = vt == "secret" } // config volumes from secret if present @@ -1042,23 +1044,52 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( for _, volume := range service.Volumes { // check if ro/rw mode is defined, default rw readonly := len(volume.Mode) > 0 && (volume.Mode == "ro" || volume.Mode == "rox") - mountHost := volume.Host - if mountHost == "" { - mountHost = volume.MountPath - } - // return useconfigmap and readonly, - // not used asigned readonly because dont break e2e - useConfigMap, _, skip = isConfigFile(mountHost) - if skip { - log.Warnf("Skip file in path %s ", volume.Host) - continue + + // Reset volume type flags for each volume (service-level flags don't apply to individual volumes) + volumeUseSecret := useSecret + volumeUseConfigMap := useConfigMap + volumeUseHostPath := useHostPath + volumeUseEmpty := useEmptyVolumes + + // Check volume type from volume labels first, then fall back to file/path detection + if volume.VolumeType == "secret" { + // Explicit secret volume from volume labels + volumeUseSecret = true + volumeUseConfigMap = false + volumeUseHostPath = false + volumeUseEmpty = false + } else if volume.Host != "" { + // Bind mount: detect if it's a config file, otherwise treat as hostPath + volumeUseConfigMap, _, skip = isConfigFile(volume.Host) + if skip { + log.Warnf("Skip file in path %s ", volume.Host) + continue + } + if volumeUseConfigMap { + // It's a config file, use ConfigMap + volumeUseSecret = false + volumeUseHostPath = false + volumeUseEmpty = false + } else { + // It's a regular bind mount, use hostPath + volumeUseConfigMap = false + volumeUseSecret = false + volumeUseHostPath = true + volumeUseEmpty = false + } + } else { + // Named volume with no host path + // Keep volumeUseSecret if set, otherwise will fall through to PVC + volumeUseConfigMap = false + volumeUseHostPath = false + volumeUseEmpty = false } if volume.VolumeName == "" { - if useEmptyVolumes { + if volumeUseEmpty { volumeName = strings.Replace(volume.PVCName, "claim", "empty", 1) - } else if useHostPath { + } else if volumeUseHostPath { volumeName = strings.Replace(volume.PVCName, "claim", "hostpath", 1) - } else if useConfigMap { + } else if volumeUseConfigMap { volumeName = strings.Replace(volume.PVCName, "claim", "cm", 1) } else { volumeName = volume.PVCName @@ -1080,15 +1111,15 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( // For PVC we will also create a PVC object and add to list var volsource *api.VolumeSource - if useEmptyVolumes { + if volumeUseEmpty { volsource = k.ConfigEmptyVolumeSource("volume") - } else if useHostPath { + } else if volumeUseHostPath { source, err := k.ConfigHostPathVolumeSource(volume.Host) if err != nil { return nil, nil, nil, nil, errors.Wrap(err, "k.ConfigHostPathVolumeSource failed") } volsource = source - } else if useConfigMap { + } else if volumeUseConfigMap { log.Debugf("Use configmap volume") cm, err := k.IntiConfigMapFromFileOrDir(name, volumeName, volume.Host, service) if err != nil { @@ -1100,6 +1131,25 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( if useSubPathMount(cm) { volMount.SubPath = volsource.ConfigMap.Items[0].Path } + } else if volumeUseSecret { + // Get secret name from volume config or service label, or default to volume name + secretName := volume.SecretName + if secretName == "" { + secretName = service.Labels[compose.LabelVolumeSecretName] + } + if secretName == "" { + secretName = volumeName + } + log.Infof("Creating Secret volume %s with secret %s", volumeName, secretName) + + // Create secret volume source (all secrets are optional by default) + optional := true + volsource = &api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: secretName, + Optional: &optional, + }, + } } else { volsource = k.ConfigPVCVolumeSource(volumeName, readonly) if volume.VFrom == "" { @@ -1717,6 +1767,22 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. allobjects = append(allobjects, objects...) } } + // Track services used as init containers (to skip creating deployments for them) + initContainerServices := make(map[string]bool) + for _, service := range komposeObject.ServiceConfigs { + if initServicesStr, ok := service.Labels[compose.LabelInitContainerService]; ok && initServicesStr != "" { + // Parse comma-separated list of init container service names + initServices := strings.Split(initServicesStr, ",") + for _, initSvc := range initServices { + initSvc = strings.TrimSpace(initSvc) + if initSvc != "" { + initContainerServices[initSvc] = true + log.Infof("Service %s will be used as init container for service %s", initSvc, service.Name) + } + } + } + } + sortedKeys := SortedKeys(komposeObject.ServiceConfigs) for _, name := range sortedKeys { service := komposeObject.ServiceConfigs[name] @@ -1726,6 +1792,12 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. continue } + // Skip services that are used as init containers for other services + if initContainerServices[name] { + log.Infof("Skipping deployment creation for service %s (used as init container)", name) + continue + } + var objects []runtime.Object service.WithKomposeAnnotation = opt.WithKomposeAnnotation @@ -1753,7 +1825,7 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. service.ServiceType = "Headless" } k.configKubeServiceAndIngressForService(service, name, &objects) - err := k.UpdateKubernetesObjects(name, service, opt, &objects) + err := k.UpdateKubernetesObjects(name, service, opt, &objects, &komposeObject) if err != nil { return nil, errors.Wrap(err, "Error transforming Kubernetes objects") } diff --git a/pkg/transformer/openshift/openshift.go b/pkg/transformer/openshift/openshift.go index 17f7d620..2aa54762 100644 --- a/pkg/transformer/openshift/openshift.go +++ b/pkg/transformer/openshift/openshift.go @@ -431,7 +431,7 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C } } - err := o.UpdateKubernetesObjects(name, service, opt, &objects) + err := o.UpdateKubernetesObjects(name, service, opt, &objects, &komposeObject) if err != nil { return nil, errors.Wrap(err, "Error transforming Kubernetes objects") } diff --git a/pkg/transformer/openshift/openshift_test.go b/pkg/transformer/openshift/openshift_test.go index e326ba5e..9f38114c 100644 --- a/pkg/transformer/openshift/openshift_test.go +++ b/pkg/transformer/openshift/openshift_test.go @@ -74,9 +74,12 @@ func TestOpenShiftUpdateKubernetesObjects(t *testing.T) { o := OpenShift{} serviceConfig := newServiceConfig() opt := kobject.ConvertOptions{} + komposeObject := kobject.KomposeObject{ + ServiceConfigs: map[string]kobject.ServiceConfig{"foobar": serviceConfig}, + } object = append(object, o.initDeploymentConfig("foobar", serviceConfig, 3, opt)) - o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object) + o.UpdateKubernetesObjects("foobar", serviceConfig, opt, &object, &komposeObject) for _, obj := range object { switch tobj := obj.(type) { -- 2.45.2 From 0d6934e81c762c11575ad3c9eb3720519863f073 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Mon, 1 Dec 2025 20:02:59 +0530 Subject: [PATCH 08/10] Support Job type controllers --- pkg/transformer/kubernetes/kubernetes.go | 63 +++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index c674b34d..f26689ba 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -81,6 +81,8 @@ const ( DaemonSetController = "daemonset" // StatefulStateController is controller type for StatefulSet StatefulStateController = "statefulset" + // JobController is controller type for Job + JobController = "job" ) // CheckUnsupportedKey checks if given komposeObject contains @@ -523,6 +525,52 @@ func (k *Kubernetes) InitDS(name string, service kobject.ServiceConfig, opt kobj return ds } +// InitJob initializes Kubernetes Job object +func (k *Kubernetes) InitJob(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) *batchv1.Job { + var podSpec api.PodSpec + if len(service.Configs) > 0 { + podSpec = k.InitPodSpecWithConfigMap(name, service, opt) + } else { + podSpec = k.InitPodSpec(name, service, opt) + } + + // Jobs need RestartPolicy set (default is Always which is invalid for Jobs) + // Use Never for jobs unless explicitly set + if podSpec.RestartPolicy == "" || podSpec.RestartPolicy == api.RestartPolicyAlways { + podSpec.RestartPolicy = api.RestartPolicyNever + } + + job := &batchv1.Job{ + TypeMeta: metav1.TypeMeta{ + Kind: "Job", + APIVersion: "batch/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: transformer.ConfigAllLabels(name, &service), + }, + Spec: batchv1.JobSpec{ + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: transformer.ConfigLabels(name), + }, + Spec: podSpec, + }, + }, + } + + // Set backoffLimit from label if specified + if backoffLimit, ok := service.Labels["kompose.job.backoff-limit"]; ok { + limit, err := strconv.ParseInt(backoffLimit, 10, 32) + if err == nil { + limit32 := int32(limit) + job.Spec.BackoffLimit = &limit32 + } + } + + return job +} + // InitSS method initialize a stateful set func (k *Kubernetes) InitSS(name string, service kobject.ServiceConfig, replicas int, opt kobject.ConvertOptions) *appsv1.StatefulSet { var podSpec api.PodSpec @@ -1454,6 +1502,10 @@ func (k *Kubernetes) CreateWorkloadAndConfigMapObjects(name string, service kobj objects = append(objects, k.InitSS(name, service, replica, opt)) } + if opt.Controller == JobController { + objects = append(objects, k.InitJob(name, service, opt)) + } + envConfigMaps := k.PargeEnvFiletoConfigMaps(name, service, opt) objects = append(objects, envConfigMaps...) return objects @@ -1807,7 +1859,10 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. } // Generate pod or cronjob and configmap objects - if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() { + // Check if service has explicit controller type label + _, hasControllerLabel := service.Labels[compose.LabelControllerType] + + if (service.Restart == "no" || service.Restart == "on-failure") && !opt.IsPodController() && !hasControllerLabel { 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, opt) @@ -1881,6 +1936,12 @@ func (k *Kubernetes) UpdateController(obj runtime.Object, updateTemplate func(*a return errors.Wrap(err, "updateTemplate failed") } updateMeta(&t.ObjectMeta) + case *batchv1.Job: + err = updateTemplate(&t.Spec.Template) + if err != nil { + return errors.Wrap(err, "updateTemplate failed") + } + updateMeta(&t.ObjectMeta) case *deployapi.DeploymentConfig: err = updateTemplate(t.Spec.Template) if err != nil { -- 2.45.2 From 003a6d8d52e5bd30137a3a22c9f1526578708957 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Wed, 3 Dec 2025 15:26:52 +0530 Subject: [PATCH 09/10] Support template placeholders and annotations for PVCs --- pkg/app/app.go | 7 +- pkg/kobject/kobject.go | 25 ++-- pkg/loader/compose/compose.go | 15 +- pkg/loader/compose/utils.go | 2 + pkg/transformer/kubernetes/k8sutils.go | 128 +++++++++++++++--- pkg/transformer/kubernetes/kubernetes.go | 35 ++++- pkg/transformer/kubernetes/kubernetes_test.go | 2 +- 7 files changed, 173 insertions(+), 41 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 1a77a811..d01ff4fa 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -261,7 +261,12 @@ func Convert(opt kobject.ConvertOptions) ([]runtime.Object, error) { } // Print output - err = kubernetes.PrintList(objects, opt, komposeObject) + // Extract kubernetes transformer for PVC values if applicable + var k8sTransformer *kubernetes.Kubernetes + if k8s, ok := t.(*kubernetes.Kubernetes); ok { + k8sTransformer = k8s + } + err = kubernetes.PrintList(objects, opt, komposeObject, k8sTransformer) if err != nil { log.Fatalf(err.Error()) } diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index eb8a4aae..b57cf9d4 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -223,18 +223,19 @@ func (port *Ports) ID() string { // Volumes holds the volume struct of container type Volumes struct { - SvcName string // Service name to which volume is linked - MountPath string // Mountpath extracted from docker-compose file - VFrom string // denotes service name from which volume is coming - VolumeName string // name of volume if provided explicitly - Host string // host machine address - Container string // Mountpath - Mode string // access mode for volume - PVCName string // name of PVC - PVCSize string // PVC size - SelectorValue string // Value of the label selector - VolumeType string // Type of volume (e.g., "secret") - SecretName string // Name of Kubernetes Secret (if VolumeType is "secret") + SvcName string // Service name to which volume is linked + MountPath string // Mountpath extracted from docker-compose file + VFrom string // denotes service name from which volume is coming + VolumeName string // name of volume if provided explicitly + Host string // host machine address + Container string // Mountpath + Mode string // access mode for volume + PVCName string // name of PVC + PVCSize string // PVC size + SelectorValue string // Value of the label selector + VolumeType string // Type of volume (e.g., "secret") + SecretName string // Name of Kubernetes Secret (if VolumeType is "secret") + Annotations map[string]string // Annotations to add to PVC (e.g., "helm.sh/resource-policy": "keep") } // Placement holds the placement struct of container diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go index 546fe6ef..fd2a1fdf 100644 --- a/pkg/loader/compose/compose.go +++ b/pkg/loader/compose/compose.go @@ -839,14 +839,15 @@ func handleVolume(komposeObject *kobject.KomposeObject, volumes *types.Volumes) errors.Wrap(err, "could not retrieve vvolume") } for volName, vol := range vols { - size, selector, volumeType, secretName := getVolumeLabels(vol.VolumeName, volumes) - if len(size) > 0 || len(selector) > 0 || len(volumeType) > 0 || len(secretName) > 0 { + size, selector, volumeType, secretName, annotations := getVolumeLabels(vol.VolumeName, volumes) + if len(size) > 0 || len(selector) > 0 || len(volumeType) > 0 || len(secretName) > 0 || len(annotations) > 0 { // We can't assign value to struct field in map while iterating over it, so temporary variable `temp` is used here var temp = vols[volName] temp.PVCSize = size temp.SelectorValue = selector temp.VolumeType = volumeType temp.SecretName = secretName + temp.Annotations = annotations vols[volName] = temp } } @@ -951,8 +952,9 @@ func getVol(toFind kobject.Volumes, Vols []kobject.Volumes) (bool, kobject.Volum return false, kobject.Volumes{} } -func getVolumeLabels(name string, volumes *types.Volumes) (string, string, string, string) { +func getVolumeLabels(name string, volumes *types.Volumes) (string, string, string, string, map[string]string) { size, selector, volumeType, secretName := "", "", "", "" + annotations := make(map[string]string) if volume, ok := (*volumes)[name]; ok { log.Debugf("Getting labels for volume %s, labels: %v", name, volume.Labels) @@ -965,6 +967,11 @@ func getVolumeLabels(name string, volumes *types.Volumes) (string, string, strin volumeType = value } else if key == "kompose.volume.secret-name" { secretName = value + } else if strings.HasPrefix(key, "kompose.volume.annotations/") { + // Extract annotation key by removing prefix and replacing ~ with / + annotationKey := strings.TrimPrefix(key, "kompose.volume.annotations/") + annotationKey = strings.ReplaceAll(annotationKey, "~", "/") + annotations[annotationKey] = value } } if volumeType != "" { @@ -974,7 +981,7 @@ func getVolumeLabels(name string, volumes *types.Volumes) (string, string, strin log.Debugf("Volume %s not found in volumes map", name) } - return size, selector, volumeType, secretName + return size, selector, volumeType, secretName, annotations } // getGroupAdd will return group in int64 format diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index 4c562cae..25f3c325 100644 --- a/pkg/loader/compose/utils.go +++ b/pkg/loader/compose/utils.go @@ -100,6 +100,8 @@ const ( LabelVolumeType = "kompose.volume.type" // LabelVolumeSecretName defines the name of the Kubernetes Secret to use LabelVolumeSecretName = "kompose.volume.secret-name" + // LabelVolumeAnnotationsPrefix is the prefix for volume annotations (e.g., kompose.volume.annotations/helm.sh~resource-policy: keep) + LabelVolumeAnnotationsPrefix = "kompose.volume.annotations/" // 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 fb4802d1..331d23a9 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -92,6 +92,10 @@ type ServiceValues struct { Env map[string]string } +type PersistenceValues struct { + Size string +} + // splitImage splits "repo:tag" or "repo" into repository and tag func splitImage(image string) (string, string) { parts := strings.Split(image, ":") @@ -129,9 +133,67 @@ func unquoteHelmTemplates(yamlBytes []byte) []byte { return []byte(strings.Join(lines, "\n")) } -// extractValuesFromKomposeObject extracts values from KomposeObject for values.yaml -func extractValuesFromKomposeObject(komposeObject kobject.KomposeObject) map[string]ServiceValues { - values := make(map[string]ServiceValues) +// templatePVCStorage replaces PVC storage values with Helm templates +// TODO: This post-processes YAML because resource.Quantity validates input and rejects template strings +func templatePVCStorage(yamlBytes []byte, persistenceValues map[string]PersistenceValues) []byte { + yamlStr := string(yamlBytes) + + // Check if this is a PersistentVolumeClaim + if !strings.Contains(yamlStr, "kind: PersistentVolumeClaim") { + return yamlBytes + } + + // Extract PVC name from metadata + var pvcName string + lines := strings.Split(yamlStr, "\n") + inMetadata := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "metadata:" { + inMetadata = true + continue + } + if inMetadata && strings.HasPrefix(trimmed, "name:") { + parts := strings.SplitN(trimmed, ":", 2) + if len(parts) == 2 { + pvcName = strings.TrimSpace(parts[1]) + break + } + } + // Exit metadata section if we hit another top-level key + if inMetadata && len(line) > 0 && line[0] != ' ' && line[0] != '\t' && !strings.HasPrefix(trimmed, "name:") { + break + } + } + + // If we found the PVC name and it's in our persistence values, template it + if pvcName != "" { + if _, exists := persistenceValues[pvcName]; exists { + templateStr := "{{ index .Values \"persistence\" \"" + pvcName + "\" \"size\" }}" + + // Replace storage value with template + for i, line := range lines { + if strings.Contains(line, "storage:") { + log.Debugf("Found storage line: %q", line) + prefix := line[:strings.Index(line, ":")+2] + log.Debugf("Prefix: %q, Template: %q", prefix, templateStr) + lines[i] = prefix + templateStr + log.Debugf("New line: %q", lines[i]) + break + } + } + yamlStr = strings.Join(lines, "\n") + log.Debugf("Final templated YAML:\n%s", yamlStr) + } + } + + return []byte(yamlStr) +} + +// extractValuesFromKomposeObject extracts service values from KomposeObject for values.yaml +// Note: PVC values are extracted during transformation and stored in Kubernetes.PersistenceValues +func extractValuesFromKomposeObject(komposeObject kobject.KomposeObject) (map[string]ServiceValues, map[string]PersistenceValues) { + serviceValues := make(map[string]ServiceValues) for serviceName, service := range komposeObject.ServiceConfigs { svcValues := ServiceValues{} @@ -154,16 +216,16 @@ func extractValuesFromKomposeObject(komposeObject kobject.KomposeObject) map[str svcValues.Env[envVar.Name] = envVar.Value } - values[serviceName] = svcValues + serviceValues[serviceName] = svcValues } - return values + return serviceValues, nil } /** * Generate Helm Chart configuration */ -func generateHelm(dirName string, values map[string]ServiceValues) error { +func generateHelm(dirName string, serviceValues map[string]ServiceValues, persistenceValues map[string]PersistenceValues) error { type ChartDetails struct { Name string } @@ -220,8 +282,8 @@ home: } /* Create the values.yaml file */ - if len(values) > 0 { - valuesYAML, err := generateValuesYAML(values) + if len(serviceValues) > 0 || len(persistenceValues) > 0 { + valuesYAML, err := generateValuesYAML(serviceValues, persistenceValues) if err != nil { return errors.Wrap(err, "Failed to generate values.yaml") } @@ -236,19 +298,19 @@ home: } // generateValuesYAML creates values.yaml content from extracted values -func generateValuesYAML(values map[string]ServiceValues) ([]byte, error) { +func generateValuesYAML(serviceValues map[string]ServiceValues, persistenceValues map[string]PersistenceValues) ([]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 := make([]string, 0, len(serviceValues)) + for name := range serviceValues { serviceNames = append(serviceNames, name) } sort.Strings(serviceNames) for _, serviceName := range serviceNames { - svcValues := values[serviceName] + svcValues := serviceValues[serviceName] serviceMap := map[string]interface{}{ "image": map[string]string{ "repository": svcValues.Image.Repository, @@ -265,6 +327,26 @@ func generateValuesYAML(values map[string]ServiceValues) ([]byte, error) { valuesMap[serviceName] = serviceMap } + // Add persistence values if present + if len(persistenceValues) > 0 { + // Sort PVC names for consistent output + pvcNames := make([]string, 0, len(persistenceValues)) + for name := range persistenceValues { + pvcNames = append(pvcNames, name) + } + sort.Strings(pvcNames) + + persistenceMap := make(map[string]interface{}) + for _, pvcName := range pvcNames { + pvcValues := persistenceValues[pvcName] + persistenceMap[pvcName] = map[string]string{ + "size": pvcValues.Size, + } + } + + valuesMap["persistence"] = persistenceMap + } + // Use marshalWithIndent for consistent 2-space indentation yamlBytes, err := marshalWithIndent(valuesMap, 2) if err != nil { @@ -312,7 +394,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, komposeObject kobject.KomposeObject) error { +func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObject kobject.KomposeObject, k8sTransformer *Kubernetes) error { var f *os.File dirName := getDirName(opt) log.Debugf("Target Dir: %s", dirName) @@ -344,11 +426,19 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje } var files []string - var imageValues map[string]ServiceValues + var serviceValues map[string]ServiceValues + var persistenceValues map[string]PersistenceValues // Extract values from KomposeObject for values.yaml if opt.CreateChart { - imageValues = extractValuesFromKomposeObject(komposeObject) + serviceValues, _ = extractValuesFromKomposeObject(komposeObject) + // Get persistence values from transformer (populated during PVC creation) + if k8sTransformer != nil && k8sTransformer.PersistenceValues != nil { + persistenceValues = make(map[string]PersistenceValues) + for name, size := range k8sTransformer.PersistenceValues { + persistenceValues[name] = PersistenceValues{Size: size} + } + } } // if asked to print to stdout or to put in single file @@ -372,6 +462,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje // Unquote Helm templates if generating chart if opt.CreateChart { data = unquoteHelmTemplates(data) + data = templatePVCStorage(data, persistenceValues) } // this part add --- which unifies the file @@ -425,6 +516,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje // Unquote Helm templates if generating chart if opt.CreateChart { data = unquoteHelmTemplates(data) + data = templatePVCStorage(data, persistenceValues) } file, err = transformer.Print(objectMeta.Name, finalDirName, strings.ToLower(typeMeta.Kind), data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider) @@ -436,7 +528,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje } } if opt.CreateChart { - err = generateHelm(dirName, imageValues) + err = generateHelm(dirName, serviceValues, persistenceValues) if err != nil { return errors.Wrap(err, "generateHelm failed") } @@ -697,7 +789,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic } // Configure the container volumes. - volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(name, service) + volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(name, service, opt) if err != nil { return errors.Wrap(err, "k.ConfigVolumes failed") } @@ -755,7 +847,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic } // Configure init container volumes - initVolumeMounts, initVolumes, initPvc, initCms, err := k.ConfigVolumes(initSvcName, initService) + initVolumeMounts, initVolumes, initPvc, initCms, err := k.ConfigVolumes(initSvcName, initService, opt) if err != nil { return errors.Wrapf(err, "k.ConfigVolumes failed for init container %s", initSvcName) } diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index f26689ba..baa7e9c7 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -55,6 +55,8 @@ import ( type Kubernetes struct { // the user provided options from the command line Opt kobject.ConvertOptions + // PersistenceValues stores PVC sizes for helm chart values.yaml + PersistenceValues map[string]string } // helmValuesPath generates a Helm template path using index notation @@ -736,8 +738,12 @@ func (k *Kubernetes) CreateSecrets(komposeObject kobject.KomposeObject) ([]*api. } // CreatePVC initializes PersistentVolumeClaim -func (k *Kubernetes) CreatePVC(name string, mode string, size string, selectorValue string, storageClassName string) (*api.PersistentVolumeClaim, error) { - volSize, err := resource.ParseQuantity(size) +func (k *Kubernetes) CreatePVC(name string, mode string, size string, selectorValue string, storageClassName string, annotations map[string]string, opt kobject.ConvertOptions) (*api.PersistentVolumeClaim, error) { + var volSize resource.Quantity + var err error + + // Parse the size value + volSize, err = resource.ParseQuantity(size) if err != nil { return nil, errors.Wrap(err, "resource.ParseQuantity failed, Error parsing size") } @@ -760,6 +766,16 @@ func (k *Kubernetes) CreatePVC(name string, mode string, size string, selectorVa }, } + // Add annotations if specified + if len(annotations) > 0 { + if pvc.ObjectMeta.Annotations == nil { + pvc.ObjectMeta.Annotations = make(map[string]string) + } + for key, value := range annotations { + pvc.ObjectMeta.Annotations[key] = value + } + } + if len(selectorValue) > 0 { pvc.Spec.Selector = &metav1.LabelSelector{ MatchLabels: transformer.ConfigLabels(selectorValue), @@ -1048,7 +1064,11 @@ func (k *Kubernetes) getSecretPathsLegacy(secretConfig types.ServiceSecretConfig } // ConfigVolumes configure the container volumes. -func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ([]api.VolumeMount, []api.Volume, []*api.PersistentVolumeClaim, []*api.ConfigMap, error) { +func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]api.VolumeMount, []api.Volume, []*api.PersistentVolumeClaim, []*api.ConfigMap, error) { + // Store PVC info for helm chart values + if opt.CreateChart && k.PersistenceValues == nil { + k.PersistenceValues = make(map[string]string) + } volumeMounts := []api.VolumeMount{} volumes := []api.Volume{} var PVCs []*api.PersistentVolumeClaim @@ -1218,12 +1238,17 @@ func (k *Kubernetes) ConfigVolumes(name string, service kobject.ServiceConfig) ( } } - createdPVC, err := k.CreatePVC(volumeName, volume.Mode, defaultSize, volume.SelectorValue, storageClassName) + createdPVC, err := k.CreatePVC(volumeName, volume.Mode, defaultSize, volume.SelectorValue, storageClassName, volume.Annotations, opt) if err != nil { return nil, nil, nil, nil, errors.Wrap(err, "k.CreatePVC failed") } + // Store PVC size for helm chart values + if opt.CreateChart { + k.PersistenceValues[volumeName] = defaultSize + } + PVCs = append(PVCs, createdPVC) } } @@ -1761,7 +1786,7 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. k.configKubeServiceAndIngressForService(service, groupName, &objects) // Configure the container volumes. - volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(groupName, service) + volumesMount, volumes, pvc, cms, err := k.ConfigVolumes(groupName, service, opt) if err != nil { return nil, errors.Wrap(err, "k.ConfigVolumes failed") } diff --git a/pkg/transformer/kubernetes/kubernetes_test.go b/pkg/transformer/kubernetes/kubernetes_test.go index 99c58c8f..e3d2d9ef 100644 --- a/pkg/transformer/kubernetes/kubernetes_test.go +++ b/pkg/transformer/kubernetes/kubernetes_test.go @@ -955,7 +955,7 @@ func TestHealthCheckOnMultipleContainers(t *testing.T) { func TestCreatePVC(t *testing.T) { storageClassName := "custom-storage-class-name" k := Kubernetes{} - result, err := k.CreatePVC("", "", PVCRequestSize, "", storageClassName) + result, err := k.CreatePVC("", "", PVCRequestSize, "", storageClassName, nil, kobject.ConvertOptions{}) if err != nil { t.Error(errors.Wrap(err, "k.CreatePVC failed")) } -- 2.45.2 From c6be5c3b58acb2a21acb4b52c2afd33f498157ec Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Wed, 3 Dec 2025 21:24:45 +0530 Subject: [PATCH 10/10] Update version and add a publish workflow --- .github/workflows/publish.yml | 49 +++++++++++++++++++++++++++++++++++ Makefile | 10 +++---- build/VERSION | 2 +- pkg/version/version.go | 2 +- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..b3bf2ec3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Release + +on: + release: + types: [created] + +env: + TERM: dumb + +jobs: + build-and-release: + name: Build and Upload Release Assets + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version: ^1.21 + id: go + + - name: Check out code + uses: actions/checkout@v4 + + - name: Build Linux AMD64 binary + run: make cross + + - name: Check binary permissions + run: | + cd bin + ls -la kompose-linux-amd64 + + - name: Get the version + id: vars + run: | + echo ::set-output name=tag::$(echo ${GITHUB_REF#refs/tags/}) + + - name: Upload binary to release assets + uses: https://gitea.com/cerc-io/action-gh-release@gitea-v2 + with: + files: ./bin/kompose-linux-amd64 + token: ${{ secrets.CICD_PUBLISH_TOKEN }} + + - name: Publish binary as generic package + run: | + curl --user "${{ github.repository_owner }}:${{ secrets.CICD_PUBLISH_TOKEN }}" \ + --upload-file ./bin/kompose-linux-amd64 \ + "https://git.vdb.to/api/packages/${{ github.repository_owner }}/generic/kompose/${{ steps.vars.outputs.tag }}/kompose-linux-amd64" diff --git a/Makefile b/Makefile index fdeb731d..e666b571 100644 --- a/Makefile +++ b/Makefile @@ -49,11 +49,11 @@ install: .PHONY: cross cross: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-amd64" main.go - GOOS=linux GOARCH=arm CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm" main.go - GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm64" main.go - GOOS=windows GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-windows-amd64.exe" main.go - GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-amd64" main.go - GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-arm64" main.go +# GOOS=linux GOARCH=arm CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm" main.go +# GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-linux-arm64" main.go +# GOOS=windows GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-windows-amd64.exe" main.go +# GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-amd64" main.go +# GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 GO111MODULE=on go build ${BUILD_FLAGS} -installsuffix cgo -o "bin/kompose-darwin-arm64" main.go .PHONY: clean clean: diff --git a/build/VERSION b/build/VERSION index bf50e910..b2822451 100644 --- a/build/VERSION +++ b/build/VERSION @@ -1 +1 @@ -1.37.0 +1.37.0-zenith-0.0.1 diff --git a/pkg/version/version.go b/pkg/version/version.go index ad27e80f..72e080e5 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -2,7 +2,7 @@ package version var ( // VERSION is version number that will be displayed when running ./kompose version - VERSION = "1.37.0" + VERSION = "1.37.0-zenith-0.0.1" // GITCOMMIT is hash of the commit that will be displayed when running ./kompose version // this will be overwritten when running build like this: go build -ldflags="-X github.com/kubernetes/kompose/pkg/version.GITCOMMIT=$(GITCOMMIT)" // HEAD is default indicating that this was not set during build -- 2.45.2