diff --git a/runner/fixtures/f4/base/beaver.yaml b/runner/fixtures/f4/base/beaver.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvYmVhdmVyLnlhbWw= --- /dev/null +++ b/runner/fixtures/f4/base/beaver.yaml @@ -0,0 +1,4 @@ +charts: + hcl1: + type: helm + path: ./hcl1 diff --git a/runner/fixtures/f4/base/build.sh b/runner/fixtures/f4/base/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvYnVpbGQuc2g= --- /dev/null +++ b/runner/fixtures/f4/base/build.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Local build demo file +# Demonstrate that leaves dependencies should be built before root ones +set -ex + +pushd ./hcl2 +helm dependency build +popd +pushd ./hcl1 +helm dependency build +helm template . +popd +rm hcl*/{charts,Chart.lock} -r diff --git a/runner/fixtures/f4/base/hcl1/Chart.yaml b/runner/fixtures/f4/base/hcl1/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMS9DaGFydC55YW1s --- /dev/null +++ b/runner/fixtures/f4/base/hcl1/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: hcl1 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.0.0" +dependencies: +- name: hcl2 + repository: file://../hcl2 + version: "*" diff --git a/runner/fixtures/f4/base/hcl1/templates/cm.yaml b/runner/fixtures/f4/base/hcl1/templates/cm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMS90ZW1wbGF0ZXMvY20ueWFtbA== --- /dev/null +++ b/runner/fixtures/f4/base/hcl1/templates/cm.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: hcl1 +data: + helm: level 1 diff --git a/runner/fixtures/f4/base/hcl1/values.yaml b/runner/fixtures/f4/base/hcl1/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMS92YWx1ZXMueWFtbA== --- /dev/null +++ b/runner/fixtures/f4/base/hcl1/values.yaml @@ -0,0 +1,1 @@ +nothing: special diff --git a/runner/fixtures/f4/base/hcl2/Chart.yaml b/runner/fixtures/f4/base/hcl2/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMi9DaGFydC55YW1s --- /dev/null +++ b/runner/fixtures/f4/base/hcl2/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: hcl2 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.0.0" +dependencies: +- name: hcl3 + repository: file://../hcl3 + version: "*" diff --git a/runner/fixtures/f4/base/hcl2/templates/cm.yaml b/runner/fixtures/f4/base/hcl2/templates/cm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMi90ZW1wbGF0ZXMvY20ueWFtbA== --- /dev/null +++ b/runner/fixtures/f4/base/hcl2/templates/cm.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: hcl2 +data: + helm: level 2 diff --git a/runner/fixtures/f4/base/hcl2/values.yaml b/runner/fixtures/f4/base/hcl2/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMi92YWx1ZXMueWFtbA== --- /dev/null +++ b/runner/fixtures/f4/base/hcl2/values.yaml @@ -0,0 +1,1 @@ +nothing: special diff --git a/runner/fixtures/f4/base/hcl3/Chart.yaml b/runner/fixtures/f4/base/hcl3/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMy9DaGFydC55YW1s --- /dev/null +++ b/runner/fixtures/f4/base/hcl3/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: hcl3 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/runner/fixtures/f4/base/hcl3/templates/cm.yaml b/runner/fixtures/f4/base/hcl3/templates/cm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMy90ZW1wbGF0ZXMvY20ueWFtbA== --- /dev/null +++ b/runner/fixtures/f4/base/hcl3/templates/cm.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: hcl3 +data: + helm: level 3 diff --git a/runner/fixtures/f4/base/hcl3/values.yaml b/runner/fixtures/f4/base/hcl3/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2ZpeHR1cmVzL2Y0L2Jhc2UvaGNsMy92YWx1ZXMueWFtbA== --- /dev/null +++ b/runner/fixtures/f4/base/hcl3/values.yaml @@ -0,0 +1,1 @@ +nothing: special diff --git a/runner/helm.go b/runner/helm.go new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2hlbG0uZ28= --- /dev/null +++ b/runner/helm.go @@ -0,0 +1,143 @@ +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 +} diff --git a/runner/helm_test.go b/runner/helm_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL2hlbG1fdGVzdC5nbw== --- /dev/null +++ b/runner/helm_test.go @@ -0,0 +1,42 @@ +package runner_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "orus.io/orus-io/beaver/runner" + "orus.io/orus-io/beaver/testutils" +) + +func TestHelmDependencyBuild(t *testing.T) { + fixtures = "fixtures/f4" + tl := testutils.NewTestLogger(t) + + absConfigDir, err := filepath.Abs(fixtures) + require.NoError(t, err) + + tmpDir, err := os.MkdirTemp(os.TempDir(), "beaver-") + require.NoError(t, err) + + c := runner.NewCmdConfig(tl.Logger(), absConfigDir, "base", false, false, "", "") + require.NoError(t, c.Initialize(tmpDir)) + + chartsPaths, err := c.HelmChartsPaths() + require.NoError(t, err) + require.Equal(t, 3, len(chartsPaths)) + assert.True(t, strings.HasSuffix(chartsPaths[0], "hcl3")) + assert.True(t, strings.HasSuffix(chartsPaths[1], "hcl2")) + assert.True(t, strings.HasSuffix(chartsPaths[2], "hcl1")) + + buildDir := filepath.Join(fixtures, "build") + defer func() { + require.NoError(t, runner.CleanDir(buildDir)) + }() + r := runner.NewRunner(c) + require.NoError(t, r.Build(tmpDir)) +} diff --git a/runner/main.go b/runner/main.go index 47c77405da51921b6b3a85820828fb419ab3082e_cnVubmVyL21haW4uZ28=..ac9b15f6fa5bb32edaa8e465007c66674a162de8_cnVubmVyL21haW4uZ28= 100644 --- a/runner/main.go +++ b/runner/main.go @@ -42,6 +42,9 @@ return fmt.Errorf("cannot prepare variables: %w", err) } var outputDir string + if err := r.config.HelmDependencyBuild(); err != nil { + return err + } if r.config.Output == "" { w := bytes.NewBuffer([]byte{}) if err := hydrateString(r.config.Namespace, w, variables); err != nil {