From 42fe3534445ccb77a1258dd02af49c8bc5cc2cf9 Mon Sep 17 00:00:00 2001 From: Martyn Ranyard Date: Tue, 5 May 2026 21:47:02 +0200 Subject: [PATCH] Working but no recursion --- cmd/convert.go | 300 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 298 insertions(+), 2 deletions(-) diff --git a/cmd/convert.go b/cmd/convert.go index 0b91006..af7c9fd 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -1,15 +1,125 @@ /* Copyright © 2026 NAME HERE - */ 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`) + }