Support template placeholders and annotations for PVCs

This commit is contained in:
Prathamesh Musale 2025-12-03 15:26:52 +05:30
parent 0d6934e81c
commit 003a6d8d52
7 changed files with 173 additions and 41 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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