Fix PVC handling for StatefulSets

This commit is contained in:
Prathamesh Musale 2025-12-15 19:38:52 +05:30
parent e6b323eedb
commit f2688ca2cf
2 changed files with 88 additions and 26 deletions

View File

@ -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
}

View File

@ -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
}
}
}