diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go index fd2a1fdf..ffb1e42b 100644 --- a/pkg/loader/compose/compose.go +++ b/pkg/loader/compose/compose.go @@ -606,7 +606,13 @@ func dockerComposeToKomposeMapping(composeObject *types.Project) (kobject.Kompos komposeObject.ServiceConfigs[normalizeServiceNames(name)] = serviceConfig } - handleVolume(&komposeObject, &composeObject.Volumes) + // Normalize volume names in the volumes map so lookups work with normalized names + normalizedVolumes := make(types.Volumes) + for volName, volConfig := range composeObject.Volumes { + normalizedVolumes[normalizeVolumes(volName)] = volConfig + } + + handleVolume(&komposeObject, &normalizedVolumes) return komposeObject, nil } diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index dc720ea9..70a9c678 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -89,7 +89,8 @@ type ServiceValues struct { Tag string PullPolicy string } - Env map[string]string + Env map[string]string + Replicas int `yaml:"replicas,omitempty"` } type PersistenceValues struct { @@ -134,62 +135,79 @@ func unquoteHelmTemplates(yamlBytes []byte) []byte { } // templatePVCStorage replaces PVC storage values with Helm templates -// TODO: This post-processes YAML because resource.Quantity validates input and rejects template strings +// 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") { + // Only process PersistentVolumeClaim or StatefulSet with volumeClaimTemplates + if !strings.Contains(yamlStr, "kind: PersistentVolumeClaim") && + !(strings.Contains(yamlStr, "kind: StatefulSet") && strings.Contains(yamlStr, "volumeClaimTemplates:")) { return yamlBytes } - // Extract PVC name from metadata - var pvcName string lines := strings.Split(yamlStr, "\n") + var pvcName string + inVolumeClaimTemplates := false inMetadata := false + + // Extract PVC name from metadata for _, line := range lines { trimmed := strings.TrimSpace(line) - if trimmed == "metadata:" { + + if strings.Contains(trimmed, "volumeClaimTemplates:") { + inVolumeClaimTemplates = true + } else if (trimmed == "metadata:" || trimmed == "- metadata:") && (inVolumeClaimTemplates || !strings.Contains(yamlStr, "volumeClaimTemplates:")) { inMetadata = true - continue - } - if inMetadata && strings.HasPrefix(trimmed, "name:") { - parts := strings.SplitN(trimmed, ":", 2) - if len(parts) == 2 { + } else if inMetadata && strings.HasPrefix(trimmed, "name:") { + if parts := strings.SplitN(trimmed, ":", 2); 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 + // Template the storage value if PVC name is in persistence values 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) } +// templateReplicas replaces hardcoded replica values with Helm templates +// This post-processes YAML because Replicas is *int32 and rejects template strings +func templateReplicas(yamlBytes []byte, serviceName string) []byte { + yamlStr := string(yamlBytes) + + // Only process Deployment or StatefulSet (DaemonSet doesn't use replicas) + if !strings.Contains(yamlStr, "kind: Deployment") && !strings.Contains(yamlStr, "kind: StatefulSet") { + return yamlBytes + } + + lines := strings.Split(yamlStr, "\n") + templateStr := "{{ " + helmValuesPath(serviceName, "replicas") + " | default 1 }}" + + for i, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "replicas:") { + indentEnd := strings.Index(line, "replicas:") + lines[i] = line[:indentEnd] + "replicas: " + templateStr + break + } + } + + return []byte(strings.Join(lines, "\n")) +} + // 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) { @@ -216,6 +234,13 @@ func extractValuesFromKomposeObject(komposeObject kobject.KomposeObject) (map[st svcValues.Env[envVar.Name] = envVar.Value } + // Extract replicas (default to 1 if not set) + if service.Replicas > 0 { + svcValues.Replicas = service.Replicas + } else { + svcValues.Replicas = 1 + } + serviceValues[serviceName] = svcValues } @@ -324,6 +349,9 @@ func generateValuesYAML(serviceValues map[string]ServiceValues, persistenceValue serviceMap["env"] = svcValues.Env } + // Add replicas + serviceMap["replicas"] = svcValues.Replicas + valuesMap[serviceName] = serviceMap } @@ -517,6 +545,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions, komposeObje if opt.CreateChart { data = unquoteHelmTemplates(data) data = templatePVCStorage(data, persistenceValues) + data = templateReplicas(data, objectMeta.Name) } file, err = transformer.Print(objectMeta.Name, finalDirName, strings.ToLower(typeMeta.Kind), data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider) @@ -828,7 +857,15 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic volumesMount = append(volumesMount, TmpVolumesMount...) } - if pvc != nil && opt.Controller != StatefulStateController { + // Check if service uses StatefulSet controller (either from opt or from label) + isStatefulSet := opt.Controller == StatefulStateController + if !isStatefulSet { + if controllerType, ok := service.Labels[compose.LabelControllerType]; ok { + isStatefulSet = (controllerType == StatefulStateController) + } + } + + if pvc != nil && !isStatefulSet { // Looping on the slice pvc instead of `*objects = append(*objects, pvc...)` // because the type of objects and pvc is different, but when doing append // one element at a time it gets converted to runtime.Object for objects slice @@ -904,7 +941,8 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic } } - if initPvc != nil { + // Skip PVCs for init containers if parent service is StatefulSet + if initPvc != nil && !isStatefulSet { for _, p := range initPvc { if !existingObjectNames[p.Name] { *objects = append(*objects, p) @@ -1115,8 +1153,26 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic persistentVolumeClaims[i] = *persistentVolumeClaim persistentVolumeClaims[i].APIVersion = "" persistentVolumeClaims[i].Kind = "" + + // Store PVC size for helm chart values (for volumeClaimTemplates) + if opt.CreateChart { + pvcName := persistentVolumeClaim.Name + if storageQty, ok := persistentVolumeClaim.Spec.Resources.Requests[api.ResourceStorage]; ok { + k.PersistenceValues[pvcName] = storageQty.String() + } + } } objType.Spec.VolumeClaimTemplates = persistentVolumeClaims + + // Remove PVC volumes from pod spec since they come from volumeClaimTemplates + // Keep only non-PVC volumes (ConfigMaps, Secrets, EmptyDir, HostPath, etc.) + var filteredVolumes []api.Volume + for _, vol := range objType.Spec.Template.Spec.Volumes { + if vol.PersistentVolumeClaim == nil { + filteredVolumes = append(filteredVolumes, vol) + } + } + objType.Spec.Template.Spec.Volumes = filteredVolumes } } }