package runner import ( "fmt" "os" "path/filepath" "strings" "github.com/go-cmd/cmd" "gopkg.in/yaml.v3" ) type HelmDependency struct { Name string Repository string } type HelmChart struct { Dependencies []HelmDependency } func (c CmdConfig) HelmDependencyBuild() error { paths, err := c.HelmChartsPaths() if err != nil { return err } c.Logger.Debug().Strs("paths", paths).Msg("found helm dependencies") for _, p := range paths { if err := c.HelmBuildDependency(p); err != nil { return err } } return nil } func (c CmdConfig) HelmBuildDependency(path string) error { args := []string{"dependency", "build", path} apiCmd := cmd.NewCmd(helmCmd, args...) stdOut, stdErr, err := RunCMD(apiCmd) if err != nil { c.Logger.Err(err). Str("command", helmCmd). Str("args", strings.Join(args, " ")). Str("sdtout", strings.Join(stdOut, "\n")). Str("stderr", strings.Join(stdErr, "\n")). Msg("failed to run command") // Error must be pretty printed to end users /!\ fmt.Printf("\n%s\n\n", strings.Join(stdErr, "\n")) return fmt.Errorf("failed to run command: %w", err) } c.Logger.Debug(). Strs("stdout", stdOut). Str("path", path). Msg("helm dependencies successfully built") return nil } func (c CmdConfig) HelmChartsPaths() ([]string, error) { var allPaths []string for name, chart := range c.Spec.Charts { if chart.Type == "helm" { c.Logger.Debug(). Str("chart", name). Str("type", chart.Type). Str("path", chart.Path). Msg("search helm dependencies for") paths, err := c.pathsByChart(chart.Path) if err != nil { return nil, err } for _, p := range paths { // Avoid infinite loop with circular dependencies. // Also improve the performance by templating only // once any given chart in case the dependency is // used multiple times. if !contains(allPaths, p) { allPaths = append(allPaths, p) } } } } return allPaths, nil } func (c CmdConfig) pathsByChart(path string) ([]string, error) { var allPaths []string helmChart, err := getHelmChart(path) if err != nil { return nil, err } for _, dependency := range helmChart.Dependencies { c.Logger.Debug(). Str("chart", dependency.Name). Str("repository", dependency.Repository). Msg("found helm dependency") if strings.HasPrefix(dependency.Repository, "file://") { subChartPath := filepath.Join( path, strings.TrimPrefix(dependency.Repository, "file://")) subChartsDependenciesPaths, err := c.pathsByChart(subChartPath) if err != nil { return nil, err } allPaths = append(allPaths, subChartsDependenciesPaths...) } } allPaths = append(allPaths, path) return allPaths, nil } func getHelmChart(path string) (*HelmChart, error) { helmChart := HelmChart{} for _, ext := range []string{"yaml", "yml"} { helmChartFile := filepath.Join(path, fmt.Sprintf("%s.%s", "Chart", ext)) fileInfo, err := os.Stat(helmChartFile) if err != nil || fileInfo.IsDir() { continue } helmChartContent, err := os.ReadFile(helmChartFile) if err != nil { return nil, err } err = yaml.Unmarshal(helmChartContent, &helmChart) if err != nil { return nil, err } return &helmChart, nil } return nil, fmt.Errorf("helm Chart.yaml file not found in %s", path) } func contains(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false }