forked from LaconicNetwork/kompose
Generate template placeholders for env vars and images in charts
This commit is contained in:
parent
fb0539e64c
commit
3074b5451d
@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user