diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index eae3d96e..28e03f7c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,7 +12,7 @@ ### Image Definition Changes -* Added the ability to configure Helm charts under `kubernetes/helmCharts` +* Added the ability to configure Helm charts under `kubernetes/helm` ### Image Configuration Directory Changes diff --git a/pkg/cli/build/build.go b/pkg/cli/build/build.go index 9ee8e628..84e1f615 100644 --- a/pkg/cli/build/build.go +++ b/pkg/cli/build/build.go @@ -74,7 +74,7 @@ func Run(_ *cli.Context) error { appendElementalRPMs(ctx) - appendHelmCharts(ctx) + appendHelm(ctx) if !bootstrapDependencyServices(ctx, args.RootBuildDir) { os.Exit(1) @@ -278,10 +278,11 @@ func appendRPMs(ctx *image.Context, repository image.AddRepo, packages ...string ctx.ImageDefinition.OperatingSystem.Packages.AdditionalRepos = repositories } -func appendHelmCharts(ctx *image.Context) { - componentCharts := combustion.ComponentHelmCharts(ctx) +func appendHelm(ctx *image.Context) { + componentCharts, componentRepos := combustion.ComponentHelmCharts(ctx) - ctx.ImageDefinition.Kubernetes.HelmCharts = append(ctx.ImageDefinition.Kubernetes.HelmCharts, componentCharts...) + ctx.ImageDefinition.Kubernetes.Helm.Charts = append(ctx.ImageDefinition.Kubernetes.Helm.Charts, componentCharts...) + ctx.ImageDefinition.Kubernetes.Helm.Repositories = append(ctx.ImageDefinition.Kubernetes.Helm.Repositories, componentRepos...) } // If the image definition requires it, starts the necessary services, displaying appropriate messages @@ -306,7 +307,7 @@ func bootstrapDependencyServices(ctx *image.Context, rootDir string) bool { } if combustion.IsEmbeddedArtifactRegistryConfigured(ctx) { - ctx.Helm = helm.New(ctx.BuildDir) + ctx.HelmClient = helm.New(ctx.BuildDir) } if ctx.ImageDefinition.Kubernetes.Version != "" { diff --git a/pkg/combustion/helm.go b/pkg/combustion/helm.go index f82ac110..a944140b 100644 --- a/pkg/combustion/helm.go +++ b/pkg/combustion/helm.go @@ -2,22 +2,24 @@ package combustion import "github.com/suse-edge/edge-image-builder/pkg/image" -func ComponentHelmCharts(ctx *image.Context) []image.HelmChart { +func ComponentHelmCharts(ctx *image.Context) ([]image.HelmChart, []image.HelmRepository) { if ctx.ImageDefinition.Kubernetes.Version == "" { - return nil + return nil, nil } const ( - suseEdgeRepository = "https://suse-edge.github.io/charts" - installationNamespace = "kube-system" + suseEdgeRepositoryName = "suse-edge" + suseEdgeRepositoryURL = "https://suse-edge.github.io/charts" + installationNamespace = "kube-system" ) var charts []image.HelmChart + var repos []image.HelmRepository if ctx.ImageDefinition.Kubernetes.Network.APIVIP != "" { metalLBChart := image.HelmChart{ Name: "metallb", - Repo: suseEdgeRepository, + RepositoryName: suseEdgeRepositoryName, TargetNamespace: "metallb-system", CreateNamespace: true, InstallationNamespace: installationNamespace, @@ -26,7 +28,7 @@ func ComponentHelmCharts(ctx *image.Context) []image.HelmChart { endpointCopierOperatorChart := image.HelmChart{ Name: "endpoint-copier-operator", - Repo: suseEdgeRepository, + RepositoryName: suseEdgeRepositoryName, TargetNamespace: "endpoint-copier-operator", CreateNamespace: true, InstallationNamespace: installationNamespace, @@ -34,7 +36,14 @@ func ComponentHelmCharts(ctx *image.Context) []image.HelmChart { } charts = append(charts, metalLBChart, endpointCopierOperatorChart) + + suseEdgeRepo := image.HelmRepository{ + Name: suseEdgeRepositoryName, + URL: suseEdgeRepositoryURL, + } + + repos = append(repos, suseEdgeRepo) } - return charts + return charts, repos } diff --git a/pkg/combustion/registry.go b/pkg/combustion/registry.go index d910e0a2..4a2610c7 100644 --- a/pkg/combustion/registry.go +++ b/pkg/combustion/registry.go @@ -185,7 +185,7 @@ func createRegistryCommand(ctx *image.Context, commandName string, args []string func IsEmbeddedArtifactRegistryConfigured(ctx *image.Context) bool { return len(ctx.ImageDefinition.EmbeddedArtifactRegistry.ContainerImages) != 0 || len(ctx.ImageDefinition.Kubernetes.Manifests.URLs) != 0 || - len(ctx.ImageDefinition.Kubernetes.HelmCharts) != 0 || + len(ctx.ImageDefinition.Kubernetes.Helm.Charts) != 0 || isComponentConfigured(ctx, filepath.Join(K8sDir, k8sManifestsDir)) } @@ -320,7 +320,7 @@ func parseManifests(ctx *image.Context) ([]string, error) { } func parseHelmCharts(ctx *image.Context) ([]*registry.HelmChart, error) { - if len(ctx.ImageDefinition.Kubernetes.HelmCharts) == 0 { + if len(ctx.ImageDefinition.Kubernetes.Helm.Charts) == 0 { return nil, nil } @@ -335,7 +335,7 @@ func parseHelmCharts(ctx *image.Context) ([]*registry.HelmChart, error) { helmValuesDir := filepath.Join(ctx.ImageConfigDir, K8sDir, HelmDir, ValuesDir) - return registry.HelmCharts(ctx.ImageDefinition.Kubernetes.HelmCharts, helmValuesDir, buildDir, ctx.ImageDefinition.Kubernetes.Version, ctx.Helm) + return registry.HelmCharts(&ctx.ImageDefinition.Kubernetes.Helm, helmValuesDir, buildDir, ctx.ImageDefinition.Kubernetes.Version, ctx.HelmClient) } func storeHelmCharts(ctx *image.Context, helmCharts []*registry.HelmChart) error { diff --git a/pkg/combustion/registry_test.go b/pkg/combustion/registry_test.go index 89f9a04b..793e2ca9 100644 --- a/pkg/combustion/registry_test.go +++ b/pkg/combustion/registry_test.go @@ -147,11 +147,13 @@ func TestIsEmbeddedArtifactRegistryConfigured(t *testing.T) { "https://k8s.io/examples/application/nginx-app.yaml", }, }, - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, }, }, }, @@ -194,11 +196,13 @@ func TestIsEmbeddedArtifactRegistryConfigured(t *testing.T) { ctx: &image.Context{ ImageDefinition: &image.Definition{ Kubernetes: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, }, }, }, @@ -312,7 +316,7 @@ func TestStoreHelmCharts(t *testing.T) { helmChart := &image.HelmChart{ Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", + RepositoryName: "apache-repo", TargetNamespace: "web", CreateNamespace: true, InstallationNamespace: "kube-system", diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index a8fa6ba0..5ed1e313 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/suse-edge/edge-image-builder/pkg/fileio" + "github.com/suse-edge/edge-image-builder/pkg/image" "go.uber.org/zap" "gopkg.in/yaml.v3" ) @@ -31,21 +32,17 @@ func New(outputDir string) *Helm { } } -func tempRepo(chart string) string { - return fmt.Sprintf("repo-%s", chart) -} - -func repositoryName(repoURL, chart string) string { +func repositoryName(repoName, repoURL, chart string) string { if strings.HasPrefix(repoURL, "http") { - return fmt.Sprintf("%s/%s", tempRepo(chart), chart) + return fmt.Sprintf("%s/%s", repoName, chart) } return repoURL } -func (h *Helm) AddRepo(chart, repository string) error { - if !strings.HasPrefix(repository, "http") { - zap.S().Infof("Skipping 'helm repo add' for non-http(s) repository: %s", repository) +func (h *Helm) AddRepo(repo *image.HelmRepository) error { + if !strings.HasPrefix(repo.URL, "http") { + zap.S().Infof("Skipping 'helm repo add' for non-http(s) repository: %s", repo.Name) return nil } @@ -61,7 +58,7 @@ func (h *Helm) AddRepo(chart, repository string) error { } }() - cmd := addRepoCommand(chart, repository, file) + cmd := addRepoCommand(repo, file) if _, err = fmt.Fprintf(file, "command: %s\n", cmd); err != nil { return fmt.Errorf("writing command prefix to log file: %w", err) @@ -70,9 +67,9 @@ func (h *Helm) AddRepo(chart, repository string) error { return cmd.Run() } -func addRepoCommand(chart, repository string, output io.Writer) *exec.Cmd { +func addRepoCommand(repo *image.HelmRepository, output io.Writer) *exec.Cmd { var args []string - args = append(args, "repo", "add", tempRepo(chart), repository) + args = append(args, "repo", "add", repo.Name, repo.URL) cmd := exec.Command("helm", args...) cmd.Stdout = output @@ -81,7 +78,7 @@ func addRepoCommand(chart, repository string, output io.Writer) *exec.Cmd { return cmd } -func (h *Helm) Pull(chart, repository, version, destDir string) (string, error) { +func (h *Helm) Pull(chart string, repo *image.HelmRepository, version, destDir string) (string, error) { logFile := filepath.Join(h.outputDir, pullLogFileName) file, err := os.OpenFile(logFile, outputFileFlags, fileio.NonExecutablePerms) @@ -94,7 +91,7 @@ func (h *Helm) Pull(chart, repository, version, destDir string) (string, error) } }() - cmd := pullCommand(chart, repository, version, destDir, file) + cmd := pullCommand(chart, repo, version, destDir, file) if _, err = fmt.Fprintf(file, "command: %s\n", cmd); err != nil { return "", fmt.Errorf("writing command prefix to log file: %w", err) @@ -117,8 +114,8 @@ func (h *Helm) Pull(chart, repository, version, destDir string) (string, error) return chartPath, nil } -func pullCommand(chart, repository, version, destDir string, output io.Writer) *exec.Cmd { - repository = repositoryName(repository, chart) +func pullCommand(chart string, repo *image.HelmRepository, version, destDir string, output io.Writer) *exec.Cmd { + repository := repositoryName(repo.Name, repo.URL, chart) var args []string args = append(args, "pull", repository) diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index e7948604..beb0b7b3 100644 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -6,32 +6,36 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/suse-edge/edge-image-builder/pkg/image" ) func TestHelmRepositoryName(t *testing.T) { tests := []struct { name string + repoName string repoURL string chart string expectedOutput string }{ { name: "OCI", + repoName: "apache-repo", repoURL: "oci://registry-1.docker.io/bitnamicharts/apache", chart: "apache", expectedOutput: "oci://registry-1.docker.io/bitnamicharts/apache", }, { name: "HTTP", + repoName: "suse-edge", repoURL: "https://suse-edge.github.io/charts", chart: "metallb", - expectedOutput: "repo-metallb/metallb", + expectedOutput: "suse-edge/metallb", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - repoName := repositoryName(test.repoURL, test.chart) + repoName := repositoryName(test.repoName, test.repoURL, test.chart) assert.Equal(t, test.expectedOutput, repoName) }) } @@ -39,13 +43,18 @@ func TestHelmRepositoryName(t *testing.T) { func TestAddRepoCommand(t *testing.T) { var buf bytes.Buffer - cmd := addRepoCommand("kubevirt", "https://suse-edge.github.io/charts", &buf) + repo := &image.HelmRepository{ + Name: "suse-edge", + URL: "https://suse-edge.github.io/charts", + } + + cmd := addRepoCommand(repo, &buf) assert.Equal(t, []string{ "helm", "repo", "add", - "repo-kubevirt", + "suse-edge", "https://suse-edge.github.io/charts", }, cmd.Args) @@ -56,15 +65,18 @@ func TestAddRepoCommand(t *testing.T) { func TestPullCommand(t *testing.T) { tests := []struct { name string - repo string + repo *image.HelmRepository chart string version string destDir string expectedArgs []string }{ { - name: "OCI repository", - repo: "oci://registry-1.docker.io/bitnamicharts/apache", + name: "OCI repository", + repo: &image.HelmRepository{ + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, version: "10.5.2", destDir: "charts", expectedArgs: []string{ @@ -78,15 +90,18 @@ func TestPullCommand(t *testing.T) { }, }, { - name: "HTTP repository", - repo: "https://suse-edge.github.io/charts", + name: "HTTP repository", + repo: &image.HelmRepository{ + Name: "suse-edge", + URL: "https://suse-edge.github.io/charts", + }, chart: "kubevirt", version: "0.2.1", destDir: "charts", expectedArgs: []string{ "helm", "pull", - "repo-kubevirt/kubevirt", + "suse-edge/kubevirt", "--version", "0.2.1", "--destination", @@ -95,7 +110,10 @@ func TestPullCommand(t *testing.T) { }, { name: "OCI repository without optional args", - repo: "oci://registry-1.docker.io/bitnamicharts/apache", + repo: &image.HelmRepository{ + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, expectedArgs: []string{ "helm", "pull", @@ -103,13 +121,16 @@ func TestPullCommand(t *testing.T) { }, }, { - name: "HTTP repository without optional args", - repo: "https://suse-edge.github.io/charts", + name: "HTTP repository without optional args", + repo: &image.HelmRepository{ + Name: "suse-edge", + URL: "https://suse-edge.github.io/charts", + }, chart: "kubevirt", expectedArgs: []string{ "helm", "pull", - "repo-kubevirt/kubevirt", + "suse-edge/kubevirt", }, }, } @@ -139,7 +160,7 @@ func TestTemplateCommand(t *testing.T) { }{ { name: "Template with all parameters", - repo: "https://suse-edge.github.io/charts", + repo: "suse-edge/kubevirt", chart: "kubevirt", version: "0.2.1", kubeVersion: "v1.29.0+rke2r1", @@ -149,7 +170,7 @@ func TestTemplateCommand(t *testing.T) { "template", "--skip-crds", "kubevirt", - "https://suse-edge.github.io/charts", + "suse-edge/kubevirt", "--version", "0.2.1", "-f", @@ -160,7 +181,7 @@ func TestTemplateCommand(t *testing.T) { }, { name: "Template without optional parameters", - repo: "https://suse-edge.github.io/charts", + repo: "suse-edge/kubevirt", chart: "kubevirt", kubeVersion: "v1.29.0+rke2r1", expectedArgs: []string{ @@ -168,7 +189,7 @@ func TestTemplateCommand(t *testing.T) { "template", "--skip-crds", "kubevirt", - "https://suse-edge.github.io/charts", + "suse-edge/kubevirt", "--kube-version", "v1.29.0+rke2r1", }, diff --git a/pkg/image/context.go b/pkg/image/context.go index 5dbdfc24..33727d05 100644 --- a/pkg/image/context.go +++ b/pkg/image/context.go @@ -4,9 +4,9 @@ import ( "io" ) -type Helm interface { - AddRepo(chart, repository string) error - Pull(chart, repository, version, destDir string) (string, error) +type HelmClient interface { + AddRepo(repository *HelmRepository) error + Pull(chart string, repository *HelmRepository, version, destDir string) (string, error) Template(chart, repository, version, valuesFilePath, kubeVersion string) ([]map[string]any, error) } @@ -58,5 +58,5 @@ type Context struct { // RPMResolver responsible for resolving rpm/package dependencies RPMResolver rpmResolver RPMRepoCreator rpmRepoCreator - Helm Helm + HelmClient HelmClient } diff --git a/pkg/image/definition.go b/pkg/image/definition.go index 32cb3954..59787151 100644 --- a/pkg/image/definition.go +++ b/pkg/image/definition.go @@ -142,11 +142,11 @@ type ContainerImage struct { } type Kubernetes struct { - Version string `yaml:"version"` - Network Network `yaml:"network"` - Nodes []Node `yaml:"nodes"` - Manifests Manifests `yaml:"manifests"` - HelmCharts []HelmChart `yaml:"helmCharts"` + Version string `yaml:"version"` + Network Network `yaml:"network"` + Nodes []Node `yaml:"nodes"` + Manifests Manifests `yaml:"manifests"` + Helm Helm `yaml:"helm"` } type Network struct { @@ -164,16 +164,26 @@ type Manifests struct { URLs []string `yaml:"urls"` } +type Helm struct { + Charts []HelmChart `yaml:"charts"` + Repositories []HelmRepository `yaml:"repositories"` +} + type HelmChart struct { Name string `yaml:"name"` - Repo string `yaml:"repo"` + RepositoryName string `yaml:"repositoryName"` + Version string `yaml:"version"` TargetNamespace string `yaml:"targetNamespace"` CreateNamespace bool `yaml:"createNamespace"` InstallationNamespace string `yaml:"installationNamespace"` - Version string `yaml:"version"` ValuesFile string `yaml:"valuesFile"` } +type HelmRepository struct { + Name string `yaml:"name"` + URL string `yaml:"url"` +} + func ParseDefinition(data []byte) (*Definition, error) { var definition Definition diff --git a/pkg/image/definition_test.go b/pkg/image/definition_test.go index 3ae10efe..f53a303e 100644 --- a/pkg/image/definition_test.go +++ b/pkg/image/definition_test.go @@ -176,13 +176,20 @@ func TestParse(t *testing.T) { assert.Equal(t, "agent", kubernetes.Nodes[4].Type) assert.Equal(t, false, kubernetes.Nodes[4].Initialiser) assert.Equal(t, "https://k8s.io/examples/application/nginx-app.yaml", kubernetes.Manifests.URLs[0]) - assert.Equal(t, "apache", kubernetes.HelmCharts[0].Name) - assert.Equal(t, "oci://registry-1.docker.io/bitnamicharts/apache", kubernetes.HelmCharts[0].Repo) - assert.Equal(t, "10.7.0", kubernetes.HelmCharts[0].Version) - assert.Equal(t, "web", kubernetes.HelmCharts[0].TargetNamespace) - assert.Equal(t, true, kubernetes.HelmCharts[0].CreateNamespace) - assert.Equal(t, "kube-system", kubernetes.HelmCharts[0].InstallationNamespace) - assert.Equal(t, "apache-values.yaml", kubernetes.HelmCharts[0].ValuesFile) + assert.Equal(t, "apache", kubernetes.Helm.Charts[0].Name) + assert.Equal(t, "bitnami", kubernetes.Helm.Charts[0].RepositoryName) + assert.Equal(t, "10.7.0", kubernetes.Helm.Charts[0].Version) + assert.Equal(t, "web", kubernetes.Helm.Charts[0].TargetNamespace) + assert.Equal(t, true, kubernetes.Helm.Charts[0].CreateNamespace) + assert.Equal(t, "apache-system", kubernetes.Helm.Charts[0].InstallationNamespace) + assert.Equal(t, "apache-values.yaml", kubernetes.Helm.Charts[0].ValuesFile) + assert.Equal(t, "metallb", kubernetes.Helm.Charts[1].Name) + assert.Equal(t, "suse-edge", kubernetes.Helm.Charts[1].RepositoryName) + assert.Equal(t, "0.14.3", kubernetes.Helm.Charts[1].Version) + assert.Equal(t, "suse-edge", kubernetes.Helm.Repositories[0].Name) + assert.Equal(t, "https://suse-edge.github.io/charts", kubernetes.Helm.Repositories[0].URL) + assert.Equal(t, "bitnami", kubernetes.Helm.Repositories[1].Name) + assert.Equal(t, "oci://registry-1.docker.io/bitnamicharts/apache", kubernetes.Helm.Repositories[1].URL) } func TestParseBadConfig_InvalidFormat(t *testing.T) { diff --git a/pkg/image/testdata/full-valid-example.yaml b/pkg/image/testdata/full-valid-example.yaml index 9f06b31a..38afd1fe 100644 --- a/pkg/image/testdata/full-valid-example.yaml +++ b/pkg/image/testdata/full-valid-example.yaml @@ -99,11 +99,20 @@ kubernetes: manifests: urls: - https://k8s.io/examples/application/nginx-app.yaml - helmCharts: - - name: apache - repo: oci://registry-1.docker.io/bitnamicharts/apache - version: 10.7.0 - targetNamespace: web - createNamespace: true - installationNamespace: kube-system - valuesFile: apache-values.yaml + helm: + charts: + - name: apache + repositoryName: bitnami + version: 10.7.0 + targetNamespace: web + createNamespace: true + installationNamespace: apache-system + valuesFile: apache-values.yaml + - name: metallb + repositoryName: suse-edge + version: 0.14.3 + repositories: + - name: suse-edge + url: https://suse-edge.github.io/charts + - name: bitnami + url: oci://registry-1.docker.io/bitnamicharts/apache diff --git a/pkg/image/validation/kubernetes.go b/pkg/image/validation/kubernetes.go index f7b59509..43713dbb 100644 --- a/pkg/image/validation/kubernetes.go +++ b/pkg/image/validation/kubernetes.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/suse-edge/edge-image-builder/pkg/combustion" - "go.uber.org/zap" "github.com/suse-edge/edge-image-builder/pkg/image" @@ -32,7 +31,7 @@ func validateKubernetes(ctx *image.Context) []FailedValidation { failures = append(failures, validateNodes(&def.Kubernetes)...) failures = append(failures, validateManifestURLs(&def.Kubernetes)...) - failures = append(failures, validateHelmCharts(&def.Kubernetes, ctx.ImageConfigDir)...) + failures = append(failures, validateHelm(&def.Kubernetes, ctx.ImageConfigDir)...) return failures } @@ -143,80 +142,137 @@ func validateManifestURLs(k8s *image.Kubernetes) []FailedValidation { return failures } -func validateHelmCharts(k8s *image.Kubernetes, imageConfigDir string) []FailedValidation { +func validateHelm(k8s *image.Kubernetes, imageConfigDir string) []FailedValidation { var failures []FailedValidation - if len(k8s.HelmCharts) == 0 { + if len(k8s.Helm.Charts) == 0 { return failures } - seenHelmCharts := make(map[string]bool) - for _, chart := range k8s.HelmCharts { - if chart.Name == "" { - failures = append(failures, FailedValidation{ - UserMessage: "Helm Chart 'name' field must be defined.", - }) - } + if len(k8s.Helm.Repositories) == 0 { + failures = append(failures, FailedValidation{ + UserMessage: "Helm charts defined with no Helm repositories defined.", + }) - if chart.Repo == "" { - failures = append(failures, FailedValidation{ - UserMessage: "Helm Chart 'repo' field must be defined.", - }) - } else if !strings.HasPrefix(chart.Repo, "http") && !strings.HasPrefix(chart.Repo, "oci://") { - failures = append(failures, FailedValidation{ - UserMessage: "Helm Chart 'repo' field must begin with either 'oci://', 'http://', or 'https://'.", - }) - } + return failures + } - if chart.Version == "" { - failures = append(failures, FailedValidation{ - UserMessage: "Helm Chart 'version' field must be defined.", - }) - } + if failure := validateHelmChartDuplicates(k8s.Helm.Charts); failure != "" { + failures = append(failures, FailedValidation{ + UserMessage: failure, + }) + } - if chart.CreateNamespace && chart.TargetNamespace == "" { - failures = append(failures, FailedValidation{ - UserMessage: "Helm Chart 'createNamespace' field cannot be true without 'targetNamespace' being defined.", - }) - } + seenHelmRepos := make(map[string]bool) + for _, chart := range k8s.Helm.Charts { + c := chart + failures = append(failures, validateChart(&c, imageConfigDir)...) - if failure := validateHelmChartValues(chart.ValuesFile, imageConfigDir); failure != "" { - failures = append(failures, FailedValidation{ - UserMessage: failure, - }) - } + seenHelmRepos[chart.RepositoryName] = true + } - if _, exists := seenHelmCharts[chart.Name]; exists { - msg := fmt.Sprintf("The 'helmCharts' field contains duplicate entries: %s", chart.Name) - failures = append(failures, FailedValidation{ - UserMessage: msg, - }) - } + for _, repo := range k8s.Helm.Repositories { + r := repo + failures = append(failures, validateRepo(&r, seenHelmRepos)...) + } - seenHelmCharts[chart.Name] = true + return failures +} + +func validateChart(chart *image.HelmChart, imageConfigDir string) []FailedValidation { + var failures []FailedValidation + + if chart.Name == "" { + failures = append(failures, FailedValidation{ + UserMessage: "Helm chart 'name' field must be defined.", + }) + } + + if chart.RepositoryName == "" { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Helm chart 'repositoryName' field for %q must be defined.", chart.Name), + }) + } + + if chart.Version == "" { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Helm chart 'version' field for %q field must be defined.", chart.Name), + }) + } + + if chart.CreateNamespace && chart.TargetNamespace == "" { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Helm chart 'createNamespace' field for %q cannot be true without 'targetNamespace' being defined.", chart.Name), + }) + } + + if failure := validateHelmChartValues(chart.Name, chart.ValuesFile, imageConfigDir); failure != "" { + failures = append(failures, FailedValidation{ + UserMessage: failure, + }) + } + + return failures +} + +func validateRepo(repo *image.HelmRepository, seenHelmRepos map[string]bool) []FailedValidation { + var failures []FailedValidation + + if repo.Name == "" { + failures = append(failures, FailedValidation{ + UserMessage: "Helm repository 'name' field must be defined.", + }) + } else if !seenHelmRepos[repo.Name] { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Helm repository 'name' field for %q must match the 'repositoryName' field in at least one defined Helm chart.", repo.Name), + }) + } + + if repo.URL == "" { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Helm repository 'url' field for %q must be defined.", repo.Name), + }) + } else if !strings.HasPrefix(repo.URL, "http") && !strings.HasPrefix(repo.URL, "oci://") { + failures = append(failures, FailedValidation{ + UserMessage: fmt.Sprintf("Helm repository 'url' field for %q must begin with either 'oci://', 'http://', or 'https://'.", repo.Name), + }) } return failures } -func validateHelmChartValues(valuesFile string, imageConfigDir string) string { +func validateHelmChartValues(chartName, valuesFile string, imageConfigDir string) string { if valuesFile == "" { return "" } if filepath.Ext(valuesFile) != ".yaml" && filepath.Ext(valuesFile) != ".yml" { - return "Helm Chart 'valuesFile' field must be the name of a valid yaml file ending in '.yaml' or '.yml'." + return fmt.Sprintf("Helm chart 'valuesFile' field for %q must be the name of a valid yaml file ending in '.yaml' or '.yml'.", chartName) } valuesFilePath := filepath.Join(imageConfigDir, combustion.K8sDir, combustion.HelmDir, combustion.ValuesDir, valuesFile) _, err := os.Stat(valuesFilePath) if err != nil { if errors.Is(err, os.ErrNotExist) { - return fmt.Sprintf("Helm Chart Values File '%s' could not be found at '%s'.", valuesFile, valuesFilePath) + return fmt.Sprintf("Helm chart values file '%s' could not be found at '%s'.", valuesFile, valuesFilePath) } zap.S().Errorf("values file '%s' could not be read: %s", valuesFile, err) - return fmt.Sprintf("Helm Chart Values File '%s' could not be read.", valuesFile) + return fmt.Sprintf("Helm chart values file '%s' could not be read.", valuesFile) + } + + return "" +} + +func validateHelmChartDuplicates(charts []image.HelmChart) string { + seenHelmCharts := make(map[string]bool) + + for _, chart := range charts { + if _, exists := seenHelmCharts[chart.Name]; exists { + return fmt.Sprintf("The 'helmCharts' field contains duplicate entries: %s", chart.Name) + } + + seenHelmCharts[chart.Name] = true } return "" diff --git a/pkg/image/validation/kubernetes_test.go b/pkg/image/validation/kubernetes_test.go index 0a633bd9..b9644324 100644 --- a/pkg/image/validation/kubernetes_test.go +++ b/pkg/image/validation/kubernetes_test.go @@ -36,15 +36,23 @@ func TestValidateKubernetes(t *testing.T) { Type: image.KubernetesNodeTypeAgent, }, }, - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - TargetNamespace: "web", - CreateNamespace: true, - InstallationNamespace: "kube-system", - Version: "10.7.0", - ValuesFile: "apache-values.yaml", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + TargetNamespace: "web", + CreateNamespace: true, + InstallationNamespace: "kube-system", + Version: "10.7.0", + ValuesFile: "apache-values.yaml", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, }, }, @@ -68,18 +76,27 @@ func TestValidateKubernetes(t *testing.T) { "example.com", }, }, - HelmCharts: []image.HelmChart{ - { - Name: "", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "", + RepositoryName: "another-apache-repo", + Version: "10.7.0", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, }, }, ExpectedFailedMessages: []string{ "The 'hostname' field is required for entries in the 'nodes' section.", "Entries in 'urls' must begin with either 'http://' or 'https://'.", - "Helm Chart 'name' field must be defined.", + "Helm chart 'name' field must be defined.", + "Helm repository 'name' field for \"apache-repo\" must match the 'repositoryName' field in at least one defined Helm chart.", }, }, } @@ -114,10 +131,10 @@ func TestIsKubernetesDefined(t *testing.T) { assert.True(t, result) result = isKubernetesDefined(&image.Kubernetes{ - Network: image.Network{}, - Nodes: []image.Node{}, - Manifests: image.Manifests{}, - HelmCharts: []image.HelmChart{}, + Network: image.Network{}, + Nodes: []image.Node{}, + Manifests: image.Manifests{}, + Helm: image.Helm{}, }) assert.False(t, result) } @@ -402,132 +419,277 @@ func TestValidateHelmCharts(t *testing.T) { }{ `valid`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - TargetNamespace: "web", - CreateNamespace: true, - InstallationNamespace: "kube-system", - Version: "10.7.0", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + TargetNamespace: "web", + CreateNamespace: true, + InstallationNamespace: "kube-system", + Version: "10.7.0", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, }, }, }, - `no name`: { + `helm no repos`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, }, }, }, ExpectedFailedMessages: []string{ - "Helm Chart 'name' field must be defined.", + "Helm charts defined with no Helm repositories defined.", }, }, - `duplicate name`: { + `helm chart no name`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + }, + }, + ExpectedFailedMessages: []string{ + "Helm chart 'name' field must be defined.", + }, + }, + `helm chart no repository name`: { + K8s: image.Kubernetes{ + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "kubevirt", + RepositoryName: "suse-edge", + Version: "0.2.2", + }, + { + Name: "metallb", + RepositoryName: "", + Version: "0.14.3", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "suse-edge", + URL: "https://suse-edge.github.io/charts", + }, }, - { - Name: "metallb", - Repo: "https://suse-edge.github.io/charts", - Version: "0.13.10", + }, + }, + ExpectedFailedMessages: []string{ + "Helm chart 'repositoryName' field for \"metallb\" must be defined.", + }, + }, + `helm chart no version`: { + K8s: image.Kubernetes{ + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, - { - Name: "metallb", - Repo: "https://suse-edge.github.io/charts", - Version: "0.13.10", + }, + }, + ExpectedFailedMessages: []string{ + "Helm chart 'version' field for \"apache\" field must be defined.", + }, + }, + `helm chart create namespace no target`: { + K8s: image.Kubernetes{ + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + CreateNamespace: true, + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, + }, + }, + }, + ExpectedFailedMessages: []string{ + "Helm chart 'createNamespace' field for \"apache\" cannot be true without 'targetNamespace' being defined.", + }, + }, + `helm chart duplicate name`: { + K8s: image.Kubernetes{ + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, }, }, ExpectedFailedMessages: []string{ "The 'helmCharts' field contains duplicate entries: apache", - "The 'helmCharts' field contains duplicate entries: metallb", }, }, - `no repo`: { + `helm chart invalid values file`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Version: "10.7.0", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + ValuesFile: "invalid", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, }, }, ExpectedFailedMessages: []string{ - "Helm Chart 'repo' field must be defined.", + "Helm chart 'valuesFile' field for \"apache\" must be the name of a valid yaml file ending in '.yaml' or '.yml'.", }, }, - `no version`: { + `helm chart nonexistent values file`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "https://suse-edge.github.io/charts", - Version: "", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + ValuesFile: "nonexistent.yaml", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, }, }, ExpectedFailedMessages: []string{ - "Helm Chart 'version' field must be defined.", + "Helm chart values file 'nonexistent.yaml' could not be found at 'kubernetes/helm/values/nonexistent.yaml'.", }, }, - `create namespace no target`: { + `helm repository no name`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "https://suse-edge.github.io/charts", - Version: "0.13.10", - CreateNamespace: true, + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "", + URL: "https://suse-edge.github.io/charts", + }, + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, }, }, ExpectedFailedMessages: []string{ - "Helm Chart 'createNamespace' field cannot be true without 'targetNamespace' being defined.", + "Helm repository 'name' field must be defined.", }, }, - `invalid values file`: { + `helm repository no url`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "https://suse-edge.github.io/charts", - Version: "0.13.10", - ValuesFile: "invalid", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "", + }, }, }, }, ExpectedFailedMessages: []string{ - "Helm Chart 'valuesFile' field must be the name of a valid yaml file ending in '.yaml' or '.yml'.", + "Helm repository 'url' field for \"apache-repo\" must be defined.", }, }, - `nonexistent values file`: { + `helm repository invalid url`: { K8s: image.Kubernetes{ - HelmCharts: []image.HelmChart{ - { - Name: "apache", - Repo: "https://suse-edge.github.io/charts", - Version: "0.13.10", - ValuesFile: "nonexistent.yaml", + Helm: image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "invalid.repo.io/bitnami/apache", + }, }, }, }, ExpectedFailedMessages: []string{ - "Helm Chart Values File 'nonexistent.yaml' could not be found at 'kubernetes/helm/values/nonexistent.yaml'.", + "Helm repository 'url' field for \"apache-repo\" must begin with either 'oci://', 'http://', or 'https://'.", }, }, } @@ -535,7 +697,7 @@ func TestValidateHelmCharts(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { k := test.K8s - failures := validateHelmCharts(&k, "") + failures := validateHelm(&k, "") assert.Len(t, failures, len(test.ExpectedFailedMessages)) var foundMessages []string diff --git a/pkg/registry/helm.go b/pkg/registry/helm.go index 9109fbd7..f72493f4 100644 --- a/pkg/registry/helm.go +++ b/pkg/registry/helm.go @@ -14,12 +14,18 @@ type HelmChart struct { ContainerImages []string } -func HelmCharts(helmCharts []image.HelmChart, valuesDir, buildDir, kubeVersion string, helm image.Helm) ([]*HelmChart, error) { +func HelmCharts(helm *image.Helm, valuesDir, buildDir, kubeVersion string, helmClient image.HelmClient) ([]*HelmChart, error) { var charts []*HelmChart + chartRepoMap := mapChartRepos(helm) - for _, helmChart := range helmCharts { + for _, helmChart := range helm.Charts { c := helmChart - chart, err := handleChart(&c, valuesDir, buildDir, kubeVersion, helm) + r, ok := chartRepoMap[c.RepositoryName] + if !ok { + return nil, fmt.Errorf("repository not found for chart %s", c.Name) + } + + chart, err := handleChart(&c, r, valuesDir, buildDir, kubeVersion, helmClient) if err != nil { return nil, fmt.Errorf("handling chart resource: %w", err) } @@ -30,7 +36,7 @@ func HelmCharts(helmCharts []image.HelmChart, valuesDir, buildDir, kubeVersion s return charts, nil } -func handleChart(chart *image.HelmChart, valuesDir, buildDir, kubeVersion string, helm image.Helm) (*HelmChart, error) { +func handleChart(chart *image.HelmChart, repo *image.HelmRepository, valuesDir, buildDir, kubeVersion string, helmClient image.HelmClient) (*HelmChart, error) { var valuesPath string var valuesContent []byte if chart.ValuesFile != "" { @@ -42,12 +48,12 @@ func handleChart(chart *image.HelmChart, valuesDir, buildDir, kubeVersion string } } - chartPath, err := downloadChart(chart, helm, buildDir) + chartPath, err := downloadChart(chart, repo, helmClient, buildDir) if err != nil { return nil, fmt.Errorf("downloading chart: %w", err) } - images, err := getChartContainerImages(chart, helm, chartPath, valuesPath, kubeVersion) + images, err := getChartContainerImages(chart, helmClient, chartPath, valuesPath, kubeVersion) if err != nil { return nil, fmt.Errorf("getting chart container images: %w", err) } @@ -65,12 +71,12 @@ func handleChart(chart *image.HelmChart, valuesDir, buildDir, kubeVersion string return &helmChart, nil } -func downloadChart(chart *image.HelmChart, helm image.Helm, destDir string) (string, error) { - if err := helm.AddRepo(chart.Name, chart.Repo); err != nil { +func downloadChart(chart *image.HelmChart, repo *image.HelmRepository, helmClient image.HelmClient, destDir string) (string, error) { + if err := helmClient.AddRepo(repo); err != nil { return "", fmt.Errorf("adding repo: %w", err) } - chartPath, err := helm.Pull(chart.Name, chart.Repo, chart.Version, destDir) + chartPath, err := helmClient.Pull(chart.Name, repo, chart.Version, destDir) if err != nil { return "", fmt.Errorf("pulling chart: %w", err) } @@ -87,8 +93,8 @@ func getChartContent(chartPath string) (string, error) { return base64.StdEncoding.EncodeToString(data), nil } -func getChartContainerImages(chart *image.HelmChart, helm image.Helm, chartPath, valuesPath, kubeVersion string) ([]string, error) { - chartResources, err := helm.Template(chart.Name, chartPath, chart.Version, valuesPath, kubeVersion) +func getChartContainerImages(chart *image.HelmChart, helmClient image.HelmClient, chartPath, valuesPath, kubeVersion string) ([]string, error) { + chartResources, err := helmClient.Template(chart.Name, chartPath, chart.Version, valuesPath, kubeVersion) if err != nil { return nil, fmt.Errorf("templating chart: %w", err) } @@ -105,3 +111,18 @@ func getChartContainerImages(chart *image.HelmChart, helm image.Helm, chartPath, return images, nil } + +func mapChartRepos(helm *image.Helm) map[string]*image.HelmRepository { + chartRepoMap := make(map[string]*image.HelmRepository) + + for _, chart := range helm.Charts { + for _, repo := range helm.Repositories { + if chart.RepositoryName == repo.Name { + r := repo + chartRepoMap[chart.RepositoryName] = &r + } + } + } + + return chartRepoMap +} diff --git a/pkg/registry/helm_test.go b/pkg/registry/helm_test.go index fb9e4ddd..8b36a637 100644 --- a/pkg/registry/helm_test.go +++ b/pkg/registry/helm_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "testing" "github.com/stretchr/testify/assert" @@ -11,27 +12,27 @@ import ( "github.com/suse-edge/edge-image-builder/pkg/image" ) -type mockHelm struct { - addRepoFunc func(chart, repository string) error - pullFunc func(chart, repository, version, destDir string) (string, error) +type mockHelmClient struct { + addRepoFunc func(repository *image.HelmRepository) error + pullFunc func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) templateFunc func(chart, repository, version, valuesFilePath, kubeVersion string) ([]map[string]any, error) } -func (m mockHelm) AddRepo(chart, repository string) error { +func (m mockHelmClient) AddRepo(repository *image.HelmRepository) error { if m.addRepoFunc != nil { - return m.addRepoFunc(chart, repository) + return m.addRepoFunc(repository) } panic("not implemented") } -func (m mockHelm) Pull(chart, repository, version, destDir string) (string, error) { +func (m mockHelmClient) Pull(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { if m.pullFunc != nil { return m.pullFunc(chart, repository, version, destDir) } panic("not implemented") } -func (m mockHelm) Template(chart, repository, version, valuesFilePath, kubeVersion string) ([]map[string]any, error) { +func (m mockHelmClient) Template(chart, repository, version, valuesFilePath, kubeVersion string) ([]map[string]any, error) { if m.templateFunc != nil { return m.templateFunc(chart, repository, version, valuesFilePath, kubeVersion) } @@ -39,16 +40,24 @@ func (m mockHelm) Template(chart, repository, version, valuesFilePath, kubeVersi } func TestHelmCharts_ValuesFileNotFoundError(t *testing.T) { - helmCharts := []image.HelmChart{ - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", - ValuesFile: "apache-values.yaml", + helm := &image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + ValuesFile: "apache-values.yaml", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, } - charts, err := HelmCharts(helmCharts, "", "", "", nil) + charts, err := HelmCharts(helm, "", "", "", nil) require.Error(t, err) assert.EqualError(t, err, "handling chart resource: reading values content: open apache-values.yaml: no such file or directory") assert.Nil(t, charts) @@ -56,31 +65,39 @@ func TestHelmCharts_ValuesFileNotFoundError(t *testing.T) { func TestHandleChart_MissingValuesDir(t *testing.T) { helmChart := &image.HelmChart{ - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", - ValuesFile: "apache-values.yaml", + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + ValuesFile: "apache-values.yaml", + } + helmRepo := &image.HelmRepository{ + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", } - chart, err := handleChart(helmChart, "oops!", "", "", nil) + chart, err := handleChart(helmChart, helmRepo, "oops!", "", "", nil) assert.EqualError(t, err, "reading values content: open oops!/apache-values.yaml: no such file or directory") assert.Nil(t, chart) } func TestHandleChart_FailedDownload(t *testing.T) { helmChart := &image.HelmChart{ - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + } + helmRepo := &image.HelmRepository{ + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", } - helm := mockHelm{ - addRepoFunc: func(chart, repository string) error { + helmClient := mockHelmClient{ + addRepoFunc: func(repository *image.HelmRepository) error { return fmt.Errorf("failed downloading") }, } - charts, err := handleChart(helmChart, "", "", "", helm) + charts, err := handleChart(helmChart, helmRepo, "", "", "", helmClient) require.Error(t, err) assert.ErrorContains(t, err, "downloading chart: adding repo: failed downloading") assert.Nil(t, charts) @@ -88,16 +105,20 @@ func TestHandleChart_FailedDownload(t *testing.T) { func TestHandleChart_FailedTemplate(t *testing.T) { helmChart := &image.HelmChart{ - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + } + helmRepo := &image.HelmRepository{ + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", } - helm := mockHelm{ - addRepoFunc: func(chart, repository string) error { + helmClient := mockHelmClient{ + addRepoFunc: func(repository *image.HelmRepository) error { return nil }, - pullFunc: func(chart, repository, version, destDir string) (string, error) { + pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { return "", nil }, templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion string) ([]map[string]any, error) { @@ -105,7 +126,7 @@ func TestHandleChart_FailedTemplate(t *testing.T) { }, } - charts, err := handleChart(helmChart, "", "", "", helm) + charts, err := handleChart(helmChart, helmRepo, "", "", "", helmClient) require.Error(t, err) assert.ErrorContains(t, err, "templating chart: failed templating") assert.Nil(t, charts) @@ -113,16 +134,20 @@ func TestHandleChart_FailedTemplate(t *testing.T) { func TestHandleChart_FailedGetChartContent(t *testing.T) { helmChart := &image.HelmChart{ - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + } + helmRepo := &image.HelmRepository{ + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", } - helm := mockHelm{ - addRepoFunc: func(chart, repository string) error { + helmClient := mockHelmClient{ + addRepoFunc: func(repository *image.HelmRepository) error { return nil }, - pullFunc: func(chart, repository, version, destDir string) (string, error) { + pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { return "does-not-exist.tgz", nil }, templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion string) ([]map[string]any, error) { @@ -130,7 +155,7 @@ func TestHandleChart_FailedGetChartContent(t *testing.T) { }, } - charts, err := handleChart(helmChart, "", "", "", helm) + charts, err := handleChart(helmChart, helmRepo, "", "", "", helmClient) require.Error(t, err) assert.ErrorContains(t, err, "getting chart content: reading chart: open does-not-exist.tgz: no such file or directory") assert.Nil(t, charts) @@ -138,14 +163,15 @@ func TestHandleChart_FailedGetChartContent(t *testing.T) { func TestDownloadChart_FailedAddingRepo(t *testing.T) { helmChart := &image.HelmChart{} + helmRepo := &image.HelmRepository{} - helm := mockHelm{ - addRepoFunc: func(chart, repository string) error { + helmClient := mockHelmClient{ + addRepoFunc: func(repository *image.HelmRepository) error { return fmt.Errorf("failed to add repo") }, } - chartPath, err := downloadChart(helmChart, helm, "") + chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") require.Error(t, err) assert.ErrorContains(t, err, "adding repo: failed to add repo") assert.Empty(t, chartPath) @@ -153,17 +179,18 @@ func TestDownloadChart_FailedAddingRepo(t *testing.T) { func TestDownloadChart_FailedPulling(t *testing.T) { helmChart := &image.HelmChart{} + helmRepo := &image.HelmRepository{} - helm := mockHelm{ - addRepoFunc: func(chart, repository string) error { + helmClient := mockHelmClient{ + addRepoFunc: func(repository *image.HelmRepository) error { return nil }, - pullFunc: func(chart, repository, version, destDir string) (string, error) { + pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { return "", fmt.Errorf("failed pulling chart") }, } - chartPath, err := downloadChart(helmChart, helm, "") + chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") require.Error(t, err) assert.ErrorContains(t, err, "pulling chart: failed pulling chart") assert.Empty(t, chartPath) @@ -171,34 +198,46 @@ func TestDownloadChart_FailedPulling(t *testing.T) { func TestDownloadChart(t *testing.T) { helmChart := &image.HelmChart{ - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + } + helmRepo := &image.HelmRepository{ + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", } - helm := mockHelm{ - addRepoFunc: func(chart, repository string) error { + helmClient := mockHelmClient{ + addRepoFunc: func(repository *image.HelmRepository) error { return nil }, - pullFunc: func(chart, repository, version, destDir string) (string, error) { + pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { return "apache-chart.tgz", nil }, } - chartPath, err := downloadChart(helmChart, helm, "") + chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") require.NoError(t, err) assert.Equal(t, "apache-chart.tgz", chartPath) } func TestHelmCharts(t *testing.T) { - helmCharts := []image.HelmChart{ - { - Name: "apache", - Repo: "oci://registry-1.docker.io/bitnamicharts/apache", - Version: "10.7.0", - InstallationNamespace: "apache-system", - CreateNamespace: true, - TargetNamespace: "web", + helm := &image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + InstallationNamespace: "apache-system", + CreateNamespace: true, + TargetNamespace: "web", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, }, } @@ -211,11 +250,11 @@ func TestHelmCharts(t *testing.T) { file := filepath.Join(dir, "apache-chart.tgz") require.NoError(t, os.WriteFile(file, []byte("abc"), 0o600)) - helm := mockHelm{ - addRepoFunc: func(chart, repository string) error { + helmClient := mockHelmClient{ + addRepoFunc: func(repository *image.HelmRepository) error { return nil }, - pullFunc: func(chart, repository, version, destDir string) (string, error) { + pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { return file, nil }, templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion string) ([]map[string]any, error) { @@ -240,7 +279,7 @@ func TestHelmCharts(t *testing.T) { }, } - charts, err := HelmCharts(helmCharts, "", "", "", helm) + charts, err := HelmCharts(helm, "", "", "", helmClient) require.NoError(t, err) assert.ElementsMatch(t, charts[0].ContainerImages, []string{"cronjob-image:0.5.6", "job-image:6.1.0"}) @@ -256,3 +295,43 @@ func TestHelmCharts(t *testing.T) { assert.Equal(t, "web", charts[0].CRD.Spec.TargetNamespace) assert.Equal(t, true, charts[0].CRD.Spec.CreateNamespace) } + +func TestMapChartRepos(t *testing.T) { + helm := &image.Helm{ + Charts: []image.HelmChart{ + { + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + { + Name: "metallb", + RepositoryName: "suse-edge", + Version: "0.14.3", + }, + }, + Repositories: []image.HelmRepository{ + { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, + { + Name: "suse-edge", + URL: "https://suse-edge.github.io/charts", + }, + }, + } + + expectedMap := map[string]*image.HelmRepository{ + "apache-repo": { + Name: "apache-repo", + URL: "oci://registry-1.docker.io/bitnamicharts/apache", + }, + "suse-edge": { + Name: "suse-edge", + URL: "https://suse-edge.github.io/charts", + }, + } + + assert.True(t, reflect.DeepEqual(expectedMap, mapChartRepos(helm))) +}