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")) }