Repository: suse-edge/edge-image-builder (go)

Issue:
Add option to specify --api-versions on the helm charts
Currently for every chart EIB runs `helm template ...`.
This will not work in cases where we have
```
{{- if not (.Capabilities.APIVersions.Has "...") -}}
{{ end }}
```

and the template step will fail:
```
command: /usr/bin/helm template --skip-crds sriov-network-operator-chart /builds/edge-engineering/stack-validation/eib-temp/_build/build-Jul02_11-34-12/helm/sriov-network-operator-chart/sriov-network-operator-chart-1.2.2.tgz --namespace sriov-system --version 1.2.2 -f /builds/edge-engineering/stack-validation/eib-temp/kubernetes/helm/values/sriov.yaml --kube-version v1.28.9+rke2r1
Error: execution error at (sriov-network-operator/templates/operator.yaml:2:4): rke2-multus is required but not found
```

As during the `helm template` there is no working cluster, Helm allows a workaround of this by specifying the flag:
```
-a, --api-versions strings                       Kubernetes api versions used for Capabilities.APIVersions
```
But this is currently not supported in EIB.

A good approach here will be, to make it possible for every helm chart in the EIB config to specify an optional list of `API Versions` so it can pass the template step successfully.


Pull request description:
Helm API Versions
Closes #481 

Adds ability to specify Helm API Versions.

Patch:
diff --git a/Dockerfile b/Dockerfile
index f5372e7..421fb5d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # ----- EIB Builder Image -----
-FROM registry.suse.com/bci/golang:1.22-1.34.7
+FROM registry.suse.com/bci/golang:1.22-1.35.4
 
 # Dependency uses by line
 # 1. Podman Go library
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 6c7eeed..9290e77 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -22,6 +22,7 @@
 ## Bug Fixes
 
 * [#498](https://github.com/suse-edge/edge-image-builder/issues/498) - Fix kernelArgs issue with Leap Micro 6.0
+* [#481](https://github.com/suse-edge/edge-image-builder/issues/481) - Certain Helm charts fail when templated without specified API Versions
 
 ---
 
diff --git a/docs/building-images.md b/docs/building-images.md
index cc0ce55..85257db 100644
--- a/docs/building-images.md
+++ b/docs/building-images.md
@@ -1,10 +1,20 @@
 # Building Images
 
-Two things are necessary to build an image using EIB:
+Three things are necessary to build an image using EIB:
+1. A SLE Micro image to use as the base of the created image
 1. A definition file that describes the image to build
 1. A directory that contains the base SLE Micro image to modify, along with any other custom files that
    will be included in the built image
 
+# SUSE Linux Enterprise Micro Images
+
+SLE Micro images can be found on [suse.com](http://www.suse.com). There are multiple formats available, however
+only the following image types are compatible with EIB:
+* `.raw` - If the compressed version is downloaded (i.e. ending with `.raw.xz`), it must be manually
+  decompressed before using the image with EIB.
+* `.iso` - Only the self-install ISOs (i.e. ending in `-SelfInstall-GM.install.iso`) may be modified
+  by EIB.
+
 # Image Definition File
 
 The Image Definition File is a YAML document describing a single image to build. The file is specified using
diff --git a/go.mod b/go.mod
index 8446305..67f2727 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/schollz/progressbar/v3 v3.14.4
 	github.com/stretchr/testify v1.9.0
-	github.com/urfave/cli/v2 v2.27.2
+	github.com/urfave/cli/v2 v2.27.3
 	go.uber.org/zap v1.27.0
 	golang.org/x/sync v0.7.0
 	golang.org/x/text v0.16.0
@@ -123,7 +123,7 @@ require (
 	github.com/ulikunitz/xz v0.5.11 // indirect
 	github.com/vbatts/tar-split v0.11.5 // indirect
 	github.com/vbauerster/mpb/v8 v8.6.2 // indirect
-	github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
+	github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
 	go.mongodb.org/mongo-driver v1.11.3 // indirect
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
 	go.opencensus.io v0.24.0 // indirect
diff --git a/go.sum b/go.sum
index 2ad18f4..16ab993 100644
--- a/go.sum
+++ b/go.sum
@@ -952,8 +952,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
-github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
+github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M=
+github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
 github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
 github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
 github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
@@ -986,8 +986,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
-github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go
index 202c074..1d7ac28 100644
--- a/pkg/helm/helm.go
+++ b/pkg/helm/helm.go
@@ -210,7 +210,7 @@ func pullCommand(chart string, repo *image.HelmRepository, version, destDir, cer
 	return cmd
 }
 
-func (h *Helm) Template(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) {
+func (h *Helm) Template(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string) ([]map[string]any, error) {
 	logFile := filepath.Join(h.outputDir, templateLogFileName)
 
 	file, err := os.OpenFile(logFile, outputFileFlags, fileio.NonExecutablePerms)
@@ -224,7 +224,7 @@ func (h *Helm) Template(chart, repository, version, valuesFilePath, kubeVersion,
 	}()
 
 	chartContentsBuffer := new(strings.Builder)
-	cmd := templateCommand(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace, io.MultiWriter(file, chartContentsBuffer), file)
+	cmd := templateCommand(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace, apiVersions, io.MultiWriter(file, chartContentsBuffer), file)
 
 	if _, err = fmt.Fprintf(file, "command: %s\n", cmd); err != nil {
 		return nil, fmt.Errorf("writing command prefix to log file: %w", err)
@@ -243,7 +243,7 @@ func (h *Helm) Template(chart, repository, version, valuesFilePath, kubeVersion,
 	return resources, nil
 }
 
-func templateCommand(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, stdout, stderr io.Writer) *exec.Cmd {
+func templateCommand(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string, stdout, stderr io.Writer) *exec.Cmd {
 	var args []string
 	args = append(args, "template", "--skip-crds", chart, repository)
 
@@ -259,6 +259,10 @@ func templateCommand(chart, repository, version, valuesFilePath, kubeVersion, ta
 		args = append(args, "-f", valuesFilePath)
 	}
 
+	if len(apiVersions) != 0 {
+		args = append(args, "--api-versions", strings.Join(apiVersions, ","))
+	}
+
 	args = append(args, "--kube-version", kubeVersion)
 
 	cmd := exec.Command("helm", args...)
diff --git a/pkg/image/definition.go b/pkg/image/definition.go
index 541ea11..85aedde 100644
--- a/pkg/image/definition.go
+++ b/pkg/image/definition.go
@@ -210,13 +210,14 @@ type Helm struct {
 }
 
 type HelmChart struct {
-	Name                  string `yaml:"name"`
-	RepositoryName        string `yaml:"repositoryName"`
-	Version               string `yaml:"version"`
-	TargetNamespace       string `yaml:"targetNamespace"`
-	CreateNamespace       bool   `yaml:"createNamespace"`
-	InstallationNamespace string `yaml:"installationNamespace"`
-	ValuesFile            string `yaml:"valuesFile"`
+	Name                  string   `yaml:"name"`
+	RepositoryName        string   `yaml:"repositoryName"`
+	Version               string   `yaml:"version"`
+	TargetNamespace       string   `yaml:"targetNamespace"`
+	CreateNamespace       bool     `yaml:"createNamespace"`
+	InstallationNamespace string   `yaml:"installationNamespace"`
+	ValuesFile            string   `yaml:"valuesFile"`
+	APIVersions           []string `yaml:"apiVersions"`
 }
 
 type HelmRepository struct {
diff --git a/pkg/registry/helm.go b/pkg/registry/helm.go
index ad687e4..0e8a02f 100644
--- a/pkg/registry/helm.go
+++ b/pkg/registry/helm.go
@@ -57,7 +57,7 @@ func (r *Registry) helmChartImages() ([]string, error) {
 }
 
 func (r *Registry) getChartContainerImages(chart *image.HelmChart, chartPath, valuesPath, kubeVersion string) ([]string, error) {
-	chartResources, err := r.helmClient.Template(chart.Name, chartPath, chart.Version, valuesPath, kubeVersion, chart.TargetNamespace)
+	chartResources, err := r.helmClient.Template(chart.Name, chartPath, chart.Version, valuesPath, kubeVersion, chart.TargetNamespace, chart.APIVersions)
 	if err != nil {
 		return nil, fmt.Errorf("templating chart: %w", err)
 	}
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 9753f43..0f377a2 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -20,7 +20,7 @@ type helmClient interface {
 	AddRepo(repository *image.HelmRepository) error
 	RegistryLogin(repository *image.HelmRepository) error
 	Pull(chart string, repository *image.HelmRepository, version, destDir string) (string, error)
-	Template(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error)
+	Template(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string) ([]map[string]any, error)
 }
 
 type helmChart struct {


Test patch:
diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go
index 6f4f710..d35567b 100644
--- a/pkg/helm/helm_test.go
+++ b/pkg/helm/helm_test.go
@@ -502,6 +502,7 @@ func TestTemplateCommand(t *testing.T) {
 		repo            string
 		chart           string
 		version         string
+		apiVersions     []string
 		kubeVersion     string
 		targetNamespace string
 		valuesPath      string
@@ -512,6 +513,7 @@ func TestTemplateCommand(t *testing.T) {
 			repo:            "suse-edge/kubevirt",
 			chart:           "kubevirt",
 			version:         "0.2.1",
+			apiVersions:     []string{"batch/v1", "apps/v1/Deployment"},
 			kubeVersion:     "v1.29.0+rke2r1",
 			targetNamespace: "kubevirt-ns",
 			valuesPath:      "/kubevirt/values.yaml",
@@ -527,6 +529,8 @@ func TestTemplateCommand(t *testing.T) {
 				"0.2.1",
 				"-f",
 				"/kubevirt/values.yaml",
+				"--api-versions",
+				"batch/v1,apps/v1/Deployment",
 				"--kube-version",
 				"v1.29.0+rke2r1",
 			},
@@ -553,7 +557,7 @@ func TestTemplateCommand(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			cmd := templateCommand(test.chart, test.repo, test.version, test.valuesPath, test.kubeVersion, test.targetNamespace, &stdout, &stderr)
+			cmd := templateCommand(test.chart, test.repo, test.version, test.valuesPath, test.kubeVersion, test.targetNamespace, test.apiVersions, &stdout, &stderr)
 
 			assert.Equal(t, test.expectedArgs, cmd.Args)
 			assert.Equal(t, &stdout, cmd.Stdout)
diff --git a/pkg/image/definition_test.go b/pkg/image/definition_test.go
index e032d51..0fa353d 100644
--- a/pkg/image/definition_test.go
+++ b/pkg/image/definition_test.go
@@ -190,6 +190,8 @@ func TestParse(t *testing.T) {
 	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, "batch/v1", kubernetes.Helm.Charts[0].APIVersions[0])
+	assert.Equal(t, "apps/v1/Deployment", kubernetes.Helm.Charts[0].APIVersions[1])
 
 	assert.Equal(t, "metallb", kubernetes.Helm.Charts[1].Name)
 	assert.Equal(t, "suse-edge", kubernetes.Helm.Charts[1].RepositoryName)
diff --git a/pkg/image/testdata/full-valid-example.yaml b/pkg/image/testdata/full-valid-example.yaml
index 9723e11..1c3cbe5 100644
--- a/pkg/image/testdata/full-valid-example.yaml
+++ b/pkg/image/testdata/full-valid-example.yaml
@@ -107,6 +107,9 @@ kubernetes:
         createNamespace: true
         installationNamespace: apache-system
         valuesFile: apache-values.yaml
+        apiVersions:
+          - batch/v1
+          - apps/v1/Deployment
       - name: metallb
         repositoryName: suse-edge
         version: 0.14.3
diff --git a/pkg/registry/helm_test.go b/pkg/registry/helm_test.go
index 902c8b0..ab7a833 100644
--- a/pkg/registry/helm_test.go
+++ b/pkg/registry/helm_test.go
@@ -16,7 +16,7 @@ type mockHelmClient struct {
 	addRepoFunc       func(repository *image.HelmRepository) error
 	registryLoginFunc func(repository *image.HelmRepository) error
 	pullFunc          func(chart string, repository *image.HelmRepository, version, destDir string) (string, error)
-	templateFunc      func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error)
+	templateFunc      func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string) ([]map[string]any, error)
 }
 
 func (m mockHelmClient) AddRepo(repository *image.HelmRepository) error {
@@ -40,9 +40,9 @@ func (m mockHelmClient) Pull(chart string, repository *image.HelmRepository, ver
 	panic("not implemented")
 }
 
-func (m mockHelmClient) Template(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) {
+func (m mockHelmClient) Template(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string) ([]map[string]any, error) {
 	if m.templateFunc != nil {
-		return m.templateFunc(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace)
+		return m.templateFunc(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace, apiVersions)
 	}
 	panic("not implemented")
 }
@@ -68,7 +68,7 @@ func TestRegistry_HelmChartImages_TemplateError(t *testing.T) {
 			},
 		},
 		helmClient: mockHelmClient{
-			templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) {
+			templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string) ([]map[string]any, error) {
 				return nil, fmt.Errorf("failed templating")
 			},
 		},
@@ -92,7 +92,7 @@ func TestRegistry_HelmChartImages(t *testing.T) {
 			},
 		},
 		helmClient: mockHelmClient{
-			templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) {
+			templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string) ([]map[string]any, error) {
 				return []map[string]any{
 					{
 						"kind":  "Deployment",
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
index ab1beb0..10878a3 100644
--- a/pkg/registry/registry_test.go
+++ b/pkg/registry/registry_test.go
@@ -62,7 +62,7 @@ func TestRegistry_ContainerImages(t *testing.T) {
 			},
 		},
 		helmClient: mockHelmClient{
-			templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) {
+			templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string, apiVersions []string) ([]map[string]any, error) {
 				return []map[string]any{
 					{
 						"kind":  "Deployment",