Working but no recursion

This commit is contained in:
Martyn 2026-05-05 21:47:02 +02:00
parent 80d7bccbdd
commit 42fe353444

View file

@ -1,15 +1,125 @@
/*
Copyright © 2026 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"regexp"
"github.com/spf13/cobra"
"github.com/yookoala/realpath"
"gopkg.in/yaml.v3"
"internal/fileflag"
)
type helmfileStructure struct {
Repositories []helmfileRepository `json:"repositories"`
Releases []helmfileHelmRelease `json:"releases"`
HelmFiles []helmfileHemfileRef `json:"helmfiles"`
Environments map[string]interface{} `json:"environments"`
}
type helmfileRepository struct {
Name string `json:"name"`
Url string `json:"url"`
}
type helmfileHelmRelease struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Enabled bool `json:"enabled"`
Installed bool `json:"installed"`
Labels string `json:"labels"`
Chart string `json:"chart"`
Version string `json:"version"`
Values []map[string]interface{} `json:"values"`
}
type helmfileHemfileRef struct {
Path string `json:"path"`
Values []map[string]interface{} `json:"values"`
}
type argoProjectMeta struct {
Name string `json:"name"`
}
type argoProjectDestination struct {
Namespace string `json:"namespace"`
Server string `json:"server"`
}
type argoResourceWhitelistItem struct {
Group string `json:"group"`
Kind string `json:"kind"`
}
type argoProjectSpec struct {
SourceRepos []string `json:"sourceRepos"`
Destinations []argoProjectDestination `json:"Destinations"`
ClusterResourceWhitelist []argoResourceWhitelistItem `json:"clusterResourceWhitelist"`
}
type argoProject struct {
ApiVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata argoProjectMeta `json:"metadata"`
Spec argoProjectSpec `json:"spec"`
}
type argoAppMeta struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}
type argoAppSpecSourceHelm struct {
ReleaseName string `releaseName`
ValuesObject map[string]interface{} `valuesObject`
}
type argoAppSpecSource struct {
Chart string `json:"chart"`
RepoURL string `json:"repoURL"`
targetRevision string `json:"targetRevision"`
Helm argoAppSpecSourceHelm `json:"helm"`
}
type argoAppSpecDestination struct {
Server string `json:"server"`
Namespace string `json:"namespace"`
}
type argoAppSpec struct {
Project string `json:"project"`
Source argoAppSpecSource `json:"source"`
Destination argoAppSpecDestination `json: "destination"`
}
type argoApp struct {
ApiVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata argoAppMeta `json:"metadata"`
Spec argoAppSpec `json:"spec"`
}
var flagInputYaml fileflag.InputFileFlag = "helmfile.yaml"
var singleFile bool = true
var outputDir string = "-" //TODO: actually handle files
var appNamespace string = "argocd"
var projectName string = "helmfile-imported"
var createProject bool = true
var serverURL string = "https://kubernetes.default.svc"
var recursiveConvert bool = false
var loadEnvironmentValues string = ""
var reposFromHelmChart []helmfileRepository
var environmentValues map[string]interface{}
// convertCmd represents the convert command
var convertCmd = &cobra.Command{
Use: "convert",
@ -18,10 +128,186 @@ var convertCmd = &cobra.Command{
a set of .yaml files describing argocd Applications that are built from the
releases section.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("convert called")
helmfileContents := parseHelmfile(string(flagInputYaml))
fmt.Println("converting file:", flagInputYaml)
if (loadEnvironmentValues != "") {
environmentMap, ok := helmfileContents.Environments[loadEnvironmentValues].(map[string]interface{})
if !ok {
fmt.Println("Error, no enviroments defined in helmfile, but environment passed in command.")
os.Exit(1)
}
var valueFilesMap = environmentMap["values"].([]interface{})
var valueFiles []string
for i := range valueFilesMap {
valueFiles = append(valueFiles, valueFilesMap[i].(string))
}
environmentValues = fetchEnvironmentValues(valueFiles)
}
if createProject && (len(helmfileContents.Repositories) > 0) {
outputProject := baseProject(projectName)
for i := 0; i <= len(helmfileContents.Repositories)-1; i++ {
outputProject.Spec.SourceRepos = append(outputProject.Spec.SourceRepos, helmfileContents.Repositories[i].Url)
}
outputProjectYaml, _ := yaml.Marshal(outputProject)
outputYamlFile(outputProjectYaml)
}
reposFromHelmChart = helmfileContents.Repositories
if len(helmfileContents.Releases) > 0 {
for i := 0; i <= len(helmfileContents.Releases)-1; i++ {
thisRelease := helmfileContents.Releases[i]
outputApp := baseArgoApp(thisRelease.Name)
outputApp.Spec.Project = projectName
if thisRelease.Chart[0:4] == "http" {
outputApp.Spec.Source.Chart = "UNSUPPORTED_BY_ARGO"
outputApp.Spec.Source.RepoURL = thisRelease.Chart
} else {
var chartmatcher = regexp.MustCompile(`(^[^/]*)/(.*)`)
chart := chartmatcher.ReplaceAllString(thisRelease.Chart, `$2`)
repo := chartmatcher.ReplaceAllString(thisRelease.Chart, `$1`)
outputApp.Spec.Source.Chart = chart
var err error
err, outputApp.Spec.Source.RepoURL = repoURLbyName(repo)
if err != nil {
outputApp.Spec.Source.RepoURL = "UNDEFINED"
}
}
outputApp.Spec.Source.Helm.ReleaseName = thisRelease.Name
if (loadEnvironmentValues != "") {
//flattened := flattenHelmfileWeirdValues(thisRelease.Values)
if (environmentValues == nil) {
outputApp.Spec.Source.Helm.ValuesObject = flattenHelmfileWeirdValues(thisRelease.Values)
} else if (thisRelease.Values == nil) {
outputApp.Spec.Source.Helm.ValuesObject = environmentValues
} else {
merged := environmentValues
maps.Copy(merged, flattenHelmfileWeirdValues(thisRelease.Values))
outputApp.Spec.Source.Helm.ValuesObject = merged
}
} else {
outputApp.Spec.Source.Helm.ValuesObject = flattenHelmfileWeirdValues(thisRelease.Values)
}
outputApp.Spec.Destination.Server = serverURL
outputApp.Spec.Destination.Namespace = thisRelease.Namespace
outputAppYaml, _ := yaml.Marshal(outputApp)
outputYamlFile(outputAppYaml)
}
}
},
}
func flattenHelmfileWeirdValues(hfv []map[string]interface{}) map[string]interface{} {
var nv map[string]interface{}
for i := range hfv {
if i == 0 {
nv = hfv[i]
} else {
maps.Copy(nv,hfv[i])
}
}
return nv
}
func parseHelmfile(helmfileFilename string) helmfileStructure {
fmt.Println("parsing file:", flagInputYaml)
yamlFile, err := os.ReadFile(helmfileFilename)
if err != nil {
fmt.Printf("yamlFile.Get err #%v ", err)
}
templatingMatch := regexp.MustCompile(`{{[^{}]*}}`)
passOneRemoveTemplating := templatingMatch.ReplaceAllString(string(yamlFile), `UNDEFINED`)
passTwoRemoveTemplating := templatingMatch.ReplaceAllString(passOneRemoveTemplating, `UNDEFINED`)
multidocMatch := regexp.MustCompile(`\n---\n`)
passThreeRemoveMultiDoc := multidocMatch.ReplaceAllString(passTwoRemoveTemplating, "\n")
helmfileContents := &helmfileStructure{}
err = yaml.Unmarshal([]byte(passThreeRemoveMultiDoc), helmfileContents)
if err != nil {
fmt.Printf("Unmarshal: %v", err)
}
return *helmfileContents
}
func fetchEnvironmentValues(files []string) map[string]interface{} {
var allValues map[string]interface{}
for i := 0; i <= len(files)-1; i++ {
absolutePath, err := realpath.Realpath(string(flagInputYaml))
basePath := filepath.Dir(absolutePath)
finalPath, err := realpath.Realpath(basePath + string(os.PathSeparator) + files[i])
fmt.Print("Loading values from ")
fmt.Println(finalPath)
if err != nil {
panic(err)
}
yamlFile, err := os.ReadFile(finalPath)
if err != nil {
panic(err)
}
var thisFilesValues map[string]interface{}
if err := yaml.Unmarshal(yamlFile, &thisFilesValues); err != nil {
panic(err)
}
fmt.Println(thisFilesValues)
if i == 0 {
allValues = thisFilesValues
} else {
maps.Copy(allValues, thisFilesValues)
}
}
fmt.Println(allValues)
return allValues
}
func fetchRepos(helmfileFilename string) []helmfileRepository {
return nil
}
func repoURLbyName(repoName string) (error, string) {
for i := 0; i <= len(reposFromHelmChart)-1; i++ {
if reposFromHelmChart[i].Name == repoName {
return nil, reposFromHelmChart[i].Url
}
}
return errors.New("No repo by that name found!"), ""
}
func outputString(data string, filename string) {
if filename == "-" {
fmt.Println(data)
} else {
fmt.Println(filename + " is not -")
}
}
func outputYamlFile(bytes []byte) {
if singleFile || (string(outputDir) == "-") {
outputString("---", string(outputDir))
}
outputString(string(bytes), string(outputDir))
}
func baseProject(name string) argoProject {
outputProject := argoProject{}
outputProject.ApiVersion = "argoproj.io/v1alpha1"
outputProject.Kind = "AppProject"
outputProject.Spec.Destinations = append(outputProject.Spec.Destinations, argoProjectDestination{Namespace: "*", Server: "*"})
outputProject.Spec.ClusterResourceWhitelist = append(outputProject.Spec.ClusterResourceWhitelist, argoResourceWhitelistItem{Group: "*", Kind: "*"})
outputProject.Metadata.Name = name
return outputProject
}
func baseArgoApp(name string) argoApp {
outputApp := argoApp{}
outputApp.ApiVersion = "argoproj.io/v1alpha1"
outputApp.Kind = "Application"
outputApp.Metadata.Namespace = appNamespace
outputApp.Metadata.Name = name
return outputApp
}
func init() {
rootCmd.AddCommand(convertCmd)
@ -34,4 +320,14 @@ func init() {
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// convertCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
convertCmd.Flags().VarP(&flagInputYaml, "input", "i", `helmfile to convert`)
convertCmd.Flags().BoolVarP(&singleFile, "single-file", "1", true, `single output file (always true if output is "-")`)
convertCmd.Flags().StringVarP(&outputDir, "output", "o", "-", `output folder or "-" for stdout`)
convertCmd.Flags().StringVarP(&appNamespace, "appnamespace", "a", "argocd", `namespace for application objects`)
convertCmd.Flags().StringVarP(&projectName, "projectname", "p", "helmfile-imported", `project name for all apps`)
convertCmd.Flags().BoolVarP(&createProject, "create-project", "c", true, `create project with above projectname and all repositories from helmfile`)
convertCmd.Flags().StringVarP(&serverURL, "server-name", "n", "https://kubernetes.default.svc", `spec.destination.server value in the application objects`)
convertCmd.Flags().BoolVarP(&recursiveConvert, "recursive", "r", false, `recurse into sub helmfiles`)
convertCmd.Flags().StringVarP(&loadEnvironmentValues, "load-environment-values", "e", "", `set to an environment name to load environment-based values in the top-level helmfile`)
}