Generate template placeholders for env vars and images in charts

This commit is contained in:
Prathamesh Musale 2025-11-27 10:52:57 +05:30
parent fb0539e64c
commit 3074b5451d
2 changed files with 216 additions and 9 deletions

View File

@ -261,7 +261,7 @@ func Convert(opt kobject.ConvertOptions) ([]runtime.Object, error) {
} }
// Print output // Print output
err = kubernetes.PrintList(objects, opt) err = kubernetes.PrintList(objects, opt, komposeObject)
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }

View File

@ -82,10 +82,157 @@ type DeploymentMapping struct {
TargetDeploymentName string 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\n<spaces>value: "VALUE" or value: VALUE
// Replace with: name: ENVNAME\n<spaces>value: 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\n<spaces>value: ""
// 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 * Generate Helm Chart configuration
*/ */
func generateHelm(dirName string) error { func generateHelm(dirName string, values map[string]ServiceValues) error {
type ChartDetails struct { type ChartDetails struct {
Name string Name string
} }
@ -141,10 +288,61 @@ home:
return err 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)) log.Infof("chart created in %q\n", dirName+string(os.PathSeparator))
return nil 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 // Check if given path is a directory
func isDir(name string) (bool, error) { func isDir(name string) (bool, error) {
// Open file to get stat later // 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 // 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 var f *os.File
dirName := getDirName(opt) dirName := getDirName(opt)
log.Debugf("Target Dir: %s", dirName) log.Debugf("Target Dir: %s", dirName)
@ -215,6 +413,13 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error {
} }
var files []string 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 // if asked to print to stdout or to put in single file
// we will create a list // we will create a list
if opt.ToStdout || f != nil { if opt.ToStdout || f != nil {
@ -265,6 +470,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error {
var typeMeta metav1.TypeMeta var typeMeta metav1.TypeMeta
var objectMeta metav1.ObjectMeta var objectMeta metav1.ObjectMeta
// Get object metadata first for templating
if us, ok := v.(*unstructured.Unstructured); ok { if us, ok := v.(*unstructured.Unstructured); ok {
typeMeta = metav1.TypeMeta{ typeMeta = metav1.TypeMeta{
Kind: us.GetKind(), Kind: us.GetKind(),
@ -275,15 +481,16 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error {
} }
} else { } else {
val := reflect.ValueOf(v).Elem() 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) 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) 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) file, err = transformer.Print(objectMeta.Name, finalDirName, strings.ToLower(typeMeta.Kind), data, opt.ToStdout, opt.GenerateJSON, f, opt.Provider)
if err != nil { if err != nil {
return errors.Wrap(err, "transformer.Print failed") return errors.Wrap(err, "transformer.Print failed")
@ -293,7 +500,7 @@ func PrintList(objects []runtime.Object, opt kobject.ConvertOptions) error {
} }
} }
if opt.CreateChart { if opt.CreateChart {
err = generateHelm(dirName) err = generateHelm(dirName, imageValues)
if err != nil { if err != nil {
return errors.Wrap(err, "generateHelm failed") return errors.Wrap(err, "generateHelm failed")
} }