/* 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", Short: "Convert a helmfile to a set of argocd applications", Long: `Takes a helmfile .yaml or .yaml.gotmpl file as input and outputs a set of .yaml files describing argocd Applications that are built from the releases section.`, Run: func(cmd *cobra.Command, args []string) { 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) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // convertCmd.PersistentFlags().String("foo", "", "A help for foo") // 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`) }