diff --git a/pkg/combustion/combustion.go b/pkg/combustion/combustion.go index b66b0c1b..f6f8929f 100644 --- a/pkg/combustion/combustion.go +++ b/pkg/combustion/combustion.go @@ -11,6 +11,7 @@ import ( "github.com/suse-edge/edge-image-builder/pkg/fileio" "github.com/suse-edge/edge-image-builder/pkg/image" "github.com/suse-edge/edge-image-builder/pkg/log" + "github.com/suse-edge/edge-image-builder/pkg/registry" "go.uber.org/zap" ) @@ -47,6 +48,12 @@ type rpmRepoCreator interface { Create(path string) error } +type embeddedRegistry interface { + ManifestsPath() string + ContainerImages() ([]string, error) + HelmCharts() ([]*registry.HelmCRD, error) +} + type Combustion struct { NetworkConfigGenerator networkConfigGenerator NetworkConfiguratorInstaller networkConfiguratorInstaller @@ -54,7 +61,7 @@ type Combustion struct { KubernetesArtefactDownloader kubernetesArtefactDownloader RPMResolver rpmResolver RPMRepoCreator rpmRepoCreator - HelmClient image.HelmClient + Registry embeddedRegistry } // Configure iterates over all separate Combustion components and configures them independently. diff --git a/pkg/combustion/kubernetes.go b/pkg/combustion/kubernetes.go index 3dcc569b..6883e468 100644 --- a/pkg/combustion/kubernetes.go +++ b/pkg/combustion/kubernetes.go @@ -11,7 +11,6 @@ import ( "github.com/suse-edge/edge-image-builder/pkg/image" "github.com/suse-edge/edge-image-builder/pkg/kubernetes" "github.com/suse-edge/edge-image-builder/pkg/log" - "github.com/suse-edge/edge-image-builder/pkg/registry" "github.com/suse-edge/edge-image-builder/pkg/template" "go.uber.org/zap" "gopkg.in/yaml.v3" @@ -20,12 +19,16 @@ import ( const ( k8sComponentName = "kubernetes" - K8sDir = "kubernetes" + k8sDir = "kubernetes" k8sConfigDir = "config" k8sInstallDir = "install" k8sImagesDir = "images" k8sManifestsDir = "manifests" + helmDir = "helm" + helmValuesDir = "values" + helmCertsDir = "certs" + k8sInitServerConfigFile = "init_server.yaml" k8sServerConfigFile = "server.yaml" k8sAgentConfigFile = "agent.yaml" @@ -73,7 +76,7 @@ func (c *Combustion) configureKubernetes(ctx *image.Context) ([]string, error) { zap.S().Warn("Kubernetes cluster of two server nodes has been requested") } - configDir := generateComponentPath(ctx, K8sDir) + configDir := generateComponentPath(ctx, k8sDir) configPath := filepath.Join(configDir, k8sConfigDir) cluster, err := kubernetes.NewCluster(&ctx.ImageDefinition.Kubernetes, configPath) @@ -121,7 +124,7 @@ func (c *Combustion) downloadKubernetesInstallScript(ctx *image.Context, distrib return "", fmt.Errorf("downloading install script: %w", err) } - return prependArtefactPath(filepath.Join(K8sDir, installScript)), nil + return prependArtefactPath(filepath.Join(k8sDir, installScript)), nil } func (c *Combustion) configureK3S(ctx *image.Context, cluster *kubernetes.Cluster) (string, error) { @@ -137,7 +140,7 @@ func (c *Combustion) configureK3S(ctx *image.Context, cluster *kubernetes.Cluste return "", fmt.Errorf("downloading k3s artefacts: %w", err) } - manifestsPath, err := configureManifests(ctx) + manifestsPath, err := c.configureManifests(ctx) if err != nil { return "", fmt.Errorf("configuring kubernetes manifests: %w", err) } @@ -149,8 +152,8 @@ func (c *Combustion) configureK3S(ctx *image.Context, cluster *kubernetes.Cluste "binaryPath": binaryPath, "imagesPath": imagesPath, "manifestsPath": manifestsPath, - "configFilePath": prependArtefactPath(K8sDir), - "registryMirrors": prependArtefactPath(filepath.Join(K8sDir, registryMirrorsFileName)), + "configFilePath": prependArtefactPath(k8sDir), + "registryMirrors": prependArtefactPath(filepath.Join(k8sDir, registryMirrorsFileName)), } singleNode := len(ctx.ImageDefinition.Kubernetes.Nodes) < 2 @@ -179,13 +182,13 @@ func (c *Combustion) configureK3S(ctx *image.Context, cluster *kubernetes.Cluste } func (c *Combustion) downloadK3sArtefacts(ctx *image.Context) (binaryPath, imagesPath string, err error) { - imagesPath = filepath.Join(K8sDir, k8sImagesDir) + imagesPath = filepath.Join(k8sDir, k8sImagesDir) imagesDestination := filepath.Join(ctx.ArtefactsDir, imagesPath) if err = os.MkdirAll(imagesDestination, os.ModePerm); err != nil { return "", "", fmt.Errorf("creating kubernetes images dir: %w", err) } - installPath := filepath.Join(K8sDir, k8sInstallDir) + installPath := filepath.Join(k8sDir, k8sInstallDir) installDestination := filepath.Join(ctx.ArtefactsDir, installPath) if err = os.MkdirAll(installDestination, os.ModePerm); err != nil { return "", "", fmt.Errorf("creating kubernetes install dir: %w", err) @@ -232,7 +235,7 @@ func (c *Combustion) configureRKE2(ctx *image.Context, cluster *kubernetes.Clust return "", fmt.Errorf("downloading RKE2 artefacts: %w", err) } - manifestsPath, err := configureManifests(ctx) + manifestsPath, err := c.configureManifests(ctx) if err != nil { return "", fmt.Errorf("configuring kubernetes manifests: %w", err) } @@ -244,8 +247,8 @@ func (c *Combustion) configureRKE2(ctx *image.Context, cluster *kubernetes.Clust "installPath": installPath, "imagesPath": imagesPath, "manifestsPath": manifestsPath, - "configFilePath": prependArtefactPath(K8sDir), - "registryMirrors": prependArtefactPath(filepath.Join(K8sDir, registryMirrorsFileName)), + "configFilePath": prependArtefactPath(k8sDir), + "registryMirrors": prependArtefactPath(filepath.Join(k8sDir, registryMirrorsFileName)), } singleNode := len(ctx.ImageDefinition.Kubernetes.Nodes) < 2 @@ -286,13 +289,13 @@ func (c *Combustion) downloadRKE2Artefacts(ctx *image.Context, cluster *kubernet return "", "", fmt.Errorf("extracting CNI from cluster config: %w", err) } - imagesPath = filepath.Join(K8sDir, k8sImagesDir) + imagesPath = filepath.Join(k8sDir, k8sImagesDir) imagesDestination := filepath.Join(ctx.ArtefactsDir, imagesPath) if err = os.MkdirAll(imagesDestination, os.ModePerm); err != nil { return "", "", fmt.Errorf("creating kubernetes images dir: %w", err) } - installPath = filepath.Join(K8sDir, k8sInstallDir) + installPath = filepath.Join(k8sDir, k8sInstallDir) installDestination := filepath.Join(ctx.ArtefactsDir, installPath) if err = os.MkdirAll(installDestination, os.ModePerm); err != nil { return "", "", fmt.Errorf("creating kubernetes install dir: %w", err) @@ -358,11 +361,10 @@ func storeKubernetesConfig(config map[string]any, configPath string) error { return os.WriteFile(configPath, data, fileio.NonExecutablePerms) } -func configureManifests(ctx *image.Context) (string, error) { - manifestURLs := ctx.ImageDefinition.Kubernetes.Manifests.URLs - localManifestsConfigured := isComponentConfigured(ctx, filepath.Join(K8sDir, k8sManifestsDir)) +func (c *Combustion) configureManifests(ctx *image.Context) (string, error) { + var manifestsPathPopulated bool - manifestsPath := filepath.Join(K8sDir, k8sManifestsDir) + manifestsPath := localKubernetesManifestsPath() manifestDestDir := filepath.Join(ctx.ArtefactsDir, manifestsPath) if ctx.ImageDefinition.Kubernetes.Network.APIVIP != "" { @@ -379,49 +381,72 @@ func configureManifests(ctx *image.Context) (string, error) { if err = os.WriteFile(manifestPath, []byte(manifest), fileio.NonExecutablePerms); err != nil { return "", fmt.Errorf("storing VIP manifest: %w", err) } - } - - if !localManifestsConfigured && len(manifestURLs) == 0 { - // The registry component would have already created and populated the manifests path if helm resources are configured - // or required. This is a hack until the dependencies between the different combustion components are resolved. - if _, err := os.Stat(manifestDestDir); err == nil { - return prependArtefactPath(manifestsPath), nil - } - return "", nil + manifestsPathPopulated = true } - err := os.MkdirAll(manifestDestDir, os.ModePerm) - if err != nil { - return "", fmt.Errorf("creating manifests destination dir: %w", err) - } + if c.Registry != nil { + if c.Registry.ManifestsPath() != "" { + if err := fileio.CopyFiles(c.Registry.ManifestsPath(), manifestDestDir, "", false); err != nil { + return "", fmt.Errorf("copying manifests to combustion dir: %w", err) + } - if localManifestsConfigured { - localManifestsSrcDir := filepath.Join(ctx.ImageConfigDir, K8sDir, k8sManifestsDir) - err = fileio.CopyFiles(localManifestsSrcDir, manifestDestDir, ".yaml", false) - if err != nil { - return "", fmt.Errorf("copying local manifests to combustion dir: %w", err) + manifestsPathPopulated = true } - err = fileio.CopyFiles(localManifestsSrcDir, manifestDestDir, ".yml", false) + + charts, err := c.Registry.HelmCharts() if err != nil { - return "", fmt.Errorf("copying local manifests to combustion dir: %w", err) + return "", fmt.Errorf("getting helm charts: %w", err) } - } - if len(manifestURLs) != 0 { - _, err = registry.DownloadManifests(manifestURLs, manifestDestDir) - if err != nil { - return "", fmt.Errorf("downloading manifests to combustion dir: %w", err) + if len(charts) != 0 { + if err = os.MkdirAll(manifestDestDir, os.ModePerm); err != nil { + return "", fmt.Errorf("creating manifests destination dir: %w", err) + } + + for _, chart := range charts { + data, err := yaml.Marshal(chart) + if err != nil { + return "", fmt.Errorf("marshaling helm chart: %w", err) + } + + chartFileName := fmt.Sprintf("%s.yaml", chart.Metadata.Name) + if err = os.WriteFile(filepath.Join(manifestDestDir, chartFileName), data, fileio.NonExecutablePerms); err != nil { + return "", fmt.Errorf("storing helm chart: %w", err) + } + } + + manifestsPathPopulated = true } } + if !manifestsPathPopulated { + return "", nil + } + return prependArtefactPath(manifestsPath), nil } func KubernetesConfigPath(ctx *image.Context) string { - return filepath.Join(ctx.ImageConfigDir, K8sDir, k8sConfigDir, k8sServerConfigFile) + return filepath.Join(ctx.ImageConfigDir, k8sDir, k8sConfigDir, k8sServerConfigFile) +} + +func localKubernetesManifestsPath() string { + return filepath.Join(k8sDir, k8sManifestsDir) +} + +func KubernetesManifestsPath(ctx *image.Context) string { + return filepath.Join(ctx.ImageConfigDir, localKubernetesManifestsPath()) +} + +func HelmValuesPath(ctx *image.Context) string { + return filepath.Join(ctx.ImageConfigDir, k8sDir, helmDir, helmValuesDir) +} + +func HelmCertsPath(ctx *image.Context) string { + return filepath.Join(ctx.ImageConfigDir, k8sDir, helmDir, helmCertsDir) } func kubernetesArtefactsPath(ctx *image.Context) string { - return filepath.Join(ctx.ArtefactsDir, K8sDir) + return filepath.Join(ctx.ArtefactsDir, k8sDir) } diff --git a/pkg/combustion/kubernetes_integration_test.go b/pkg/combustion/kubernetes_integration_test.go deleted file mode 100644 index 1db4871d..00000000 --- a/pkg/combustion/kubernetes_integration_test.go +++ /dev/null @@ -1,163 +0,0 @@ -//go:build integration - -package combustion - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/suse-edge/edge-image-builder/pkg/fileio" - "github.com/suse-edge/edge-image-builder/pkg/image" - "gopkg.in/yaml.v3" -) - -func TestConfigureManifestsValidDownload(t *testing.T) { - // Setup - ctx, teardown := setupContext(t) - defer teardown() - - ctx.ImageDefinition.Kubernetes.Manifests.URLs = []string{ - "https://k8s.io/examples/application/nginx-app.yaml", - } - - k8sCombDir := filepath.Join(ctx.ArtefactsDir, K8sDir) - require.NoError(t, os.Mkdir(k8sCombDir, os.ModePerm)) - - downloadedManifestsPath := filepath.Join("$ARTEFACTS_DIR", K8sDir, k8sManifestsDir) - downloadedManifestsDestDir := filepath.Join(k8sCombDir, k8sManifestsDir) - expectedDownloadedFilePath := filepath.Join(downloadedManifestsDestDir, "dl-manifest-1.yaml") - - // Test - manifestsPath, err := configureManifests(ctx) - - // Verify - require.NoError(t, err) - assert.Equal(t, downloadedManifestsPath, manifestsPath) - - assert.FileExists(t, expectedDownloadedFilePath) - b, err := os.ReadFile(expectedDownloadedFilePath) - require.NoError(t, err) - - contents := string(b) - assert.Contains(t, contents, "apiVersion: v1") - assert.Contains(t, contents, "name: my-nginx-svc") - assert.Contains(t, contents, "type: LoadBalancer") - assert.Contains(t, contents, "image: nginx:1.14.2") -} - -func TestConfigureKubernetes_SuccessfulRKE2ServerWithManifests(t *testing.T) { - ctx, teardown := setupContext(t) - defer teardown() - - ctx.ImageDefinition.Kubernetes = image.Kubernetes{ - Version: "v1.29.0+rke2r1", - Network: image.Network{ - APIVIP: "192.168.122.100", - APIHost: "api.cluster01.hosted.on.edge.suse.com", - }, - } - - c := Combustion{ - KubernetesScriptDownloader: mockKubernetesScriptDownloader{ - downloadScript: func(distribution, destPath string) (string, error) { - return "install-k8s.sh", nil - }, - }, - KubernetesArtefactDownloader: mockKubernetesArtefactDownloader{ - downloadRKE2Artefacts: func(arch image.Arch, version, cni string, multusEnabled bool, installPath, imagesPath string) error { - return nil - }, - }, - } - - ctx.ImageDefinition.Kubernetes.Manifests.URLs = []string{ - "https://k8s.io/examples/application/nginx-app.yaml", - } - - artefactsDir := filepath.Join(ctx.ArtefactsDir, K8sDir) - require.NoError(t, os.Mkdir(artefactsDir, os.ModePerm)) - - localManifestsSrcDir := filepath.Join(ctx.ImageConfigDir, K8sDir, k8sManifestsDir) - require.NoError(t, os.MkdirAll(localManifestsSrcDir, 0o755)) - - localSampleManifestPath := filepath.Join("..", "registry", "testdata", "sample-crd.yaml") - err := fileio.CopyFile(localSampleManifestPath, filepath.Join(localManifestsSrcDir, "sample-crd.yaml"), fileio.NonExecutablePerms) - require.NoError(t, err) - - scripts, err := c.configureKubernetes(ctx) - require.NoError(t, err) - require.Len(t, scripts, 1) - - // Script file assertions - scriptPath := filepath.Join(ctx.CombustionDir, scripts[0]) - - info, err := os.Stat(scriptPath) - require.NoError(t, err) - - assert.Equal(t, fileio.ExecutablePerms, info.Mode()) - - b, err := os.ReadFile(scriptPath) - require.NoError(t, err) - - contents := string(b) - assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/images/* /var/lib/rancher/rke2/agent/images/") - assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/server.yaml /etc/rancher/rke2/config.yaml") - assert.Contains(t, contents, "echo \"192.168.122.100 api.cluster01.hosted.on.edge.suse.com\" >> /etc/hosts") - assert.Contains(t, contents, "export INSTALL_RKE2_ARTIFACT_PATH=$ARTEFACTS_DIR/kubernetes/install") - assert.Contains(t, contents, "sh $ARTEFACTS_DIR/kubernetes/install-k8s.sh") - assert.Contains(t, contents, "systemctl enable rke2-server.service") - assert.Contains(t, contents, "mkdir -p /var/lib/rancher/rke2/server/manifests/") - assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/manifests/* /var/lib/rancher/rke2/server/manifests/") - assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/registries.yaml /etc/rancher/rke2/registries.yaml") - - // Config file assertions - configPath := filepath.Join(ctx.ArtefactsDir, "kubernetes/server.yaml") - - info, err = os.Stat(configPath) - require.NoError(t, err) - - assert.Equal(t, fileio.NonExecutablePerms, info.Mode()) - - b, err = os.ReadFile(configPath) - require.NoError(t, err) - - var configContents map[string]any - require.NoError(t, yaml.Unmarshal(b, &configContents)) - - require.Contains(t, configContents, "cni") - assert.Equal(t, "cilium", configContents["cni"], "default CNI is not set") - assert.Equal(t, nil, configContents["server"]) - assert.Equal(t, []any{"192.168.122.100", "api.cluster01.hosted.on.edge.suse.com"}, configContents["tls-san"]) - - // Downloaded manifest assertions - manifestPath := filepath.Join(artefactsDir, k8sManifestsDir, "dl-manifest-1.yaml") - info, err = os.Stat(manifestPath) - require.NoError(t, err) - assert.Equal(t, fileio.NonExecutablePerms, info.Mode()) - - b, err = os.ReadFile(manifestPath) - require.NoError(t, err) - - contents = string(b) - assert.Contains(t, contents, "apiVersion: v1") - assert.Contains(t, contents, "name: my-nginx-svc") - assert.Contains(t, contents, "type: LoadBalancer") - assert.Contains(t, contents, "image: nginx:1.14.2") - - // Local manifest assertions - manifest := filepath.Join(artefactsDir, k8sManifestsDir, "sample-crd.yaml") - info, err = os.Stat(manifest) - require.NoError(t, err) - assert.Equal(t, fileio.NonExecutablePerms, info.Mode()) - - b, err = os.ReadFile(manifest) - require.NoError(t, err) - - contents = string(b) - assert.Contains(t, contents, "apiVersion: \"custom.example.com/v1\"") - assert.Contains(t, contents, "app: complex-application") - assert.Contains(t, contents, "- name: redis-container") -} diff --git a/pkg/combustion/kubernetes_test.go b/pkg/combustion/kubernetes_test.go index 22a4abff..6984cf3b 100644 --- a/pkg/combustion/kubernetes_test.go +++ b/pkg/combustion/kubernetes_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/suse-edge/edge-image-builder/pkg/fileio" "github.com/suse-edge/edge-image-builder/pkg/image" + "github.com/suse-edge/edge-image-builder/pkg/registry" "gopkg.in/yaml.v3" ) @@ -55,6 +56,36 @@ func (m mockKubernetesArtefactDownloader) DownloadK3sArtefacts(arch image.Arch, panic("not implemented") } +type mockEmbeddedRegistry struct { + helmChartsFunc func() ([]*registry.HelmCRD, error) + containerImagesFunc func() ([]string, error) + manifestsPathFunc func() string +} + +func (m mockEmbeddedRegistry) HelmCharts() ([]*registry.HelmCRD, error) { + if m.helmChartsFunc != nil { + return m.helmChartsFunc() + } + + panic("not implemented") +} + +func (m mockEmbeddedRegistry) ContainerImages() ([]string, error) { + if m.containerImagesFunc != nil { + return m.containerImagesFunc() + } + + panic("not implemented") +} + +func (m mockEmbeddedRegistry) ManifestsPath() string { + if m.manifestsPathFunc != nil { + return m.manifestsPathFunc() + } + + panic("not implemented") +} + func TestConfigureKubernetes_Skipped(t *testing.T) { ctx := &image.Context{ ImageDefinition: &image.Definition{}, @@ -300,7 +331,7 @@ func TestConfigureKubernetes_SuccessfulMultiNodeK3sCluster(t *testing.T) { b, err := yaml.Marshal(serverConfig) require.NoError(t, err) - configDir := filepath.Join(ctx.ImageConfigDir, K8sDir, k8sConfigDir) + configDir := filepath.Join(ctx.ImageConfigDir, k8sDir, k8sConfigDir) require.NoError(t, os.MkdirAll(configDir, os.ModePerm)) require.NoError(t, os.WriteFile(filepath.Join(configDir, "server.yaml"), b, os.ModePerm)) @@ -499,7 +530,7 @@ func TestConfigureKubernetes_SuccessfulMultiNodeRKE2Cluster(t *testing.T) { b, err := yaml.Marshal(serverConfig) require.NoError(t, err) - configDir := filepath.Join(ctx.ImageConfigDir, K8sDir, k8sConfigDir) + configDir := filepath.Join(ctx.ImageConfigDir, k8sDir, k8sConfigDir) require.NoError(t, os.MkdirAll(configDir, os.ModePerm)) require.NoError(t, os.WriteFile(filepath.Join(configDir, "server.yaml"), b, os.ModePerm)) @@ -577,47 +608,215 @@ func TestConfigureKubernetes_SuccessfulMultiNodeRKE2Cluster(t *testing.T) { assert.Nil(t, configContents["tls-san"]) } -func TestConfigureKubernetes_InvalidManifestURL(t *testing.T) { +func TestConfigureManifests_NoSetup(t *testing.T) { + ctx, teardown := setupContext(t) + defer teardown() + + var c Combustion + + manifestsPath, err := c.configureManifests(ctx) + require.NoError(t, err) + + assert.Equal(t, "", manifestsPath) +} + +func TestConfigureManifests_InvalidManifestDir(t *testing.T) { + ctx, teardown := setupContext(t) + defer teardown() + + c := Combustion{ + Registry: &mockEmbeddedRegistry{ + manifestsPathFunc: func() string { + return "non-existing" + }, + }, + } + + _, err := c.configureManifests(ctx) + require.Error(t, err) + assert.EqualError(t, err, "copying manifests to combustion dir: reading source dir: open non-existing: no such file or directory") +} + +func TestConfigureManifests_HelmChartsError(t *testing.T) { + ctx, teardown := setupContext(t) + defer teardown() + + c := Combustion{ + Registry: &mockEmbeddedRegistry{ + manifestsPathFunc: func() string { + // Use local test files + return filepath.Join("..", "registry", "testdata") + }, + helmChartsFunc: func() ([]*registry.HelmCRD, error) { + return nil, fmt.Errorf("some error") + }, + }, + } + + _, err := c.configureManifests(ctx) + require.Error(t, err) + assert.EqualError(t, err, "getting helm charts: some error") +} + +func TestConfigureManifests(t *testing.T) { + ctx, teardown := setupContext(t) + defer teardown() + + helmChart := &image.HelmChart{ + Name: "apache", + RepositoryName: "apache-repo", + TargetNamespace: "web", + CreateNamespace: true, + InstallationNamespace: "kube-system", + Version: "10.7.0", + ValuesFile: "", + } + + c := Combustion{ + Registry: &mockEmbeddedRegistry{ + manifestsPathFunc: func() string { + // Use local test files + return filepath.Join("..", "registry", "testdata") + }, + helmChartsFunc: func() ([]*registry.HelmCRD, error) { + return []*registry.HelmCRD{ + registry.NewHelmCRD(helmChart, "some-content", ` +values: content`, "oci://registry-1.docker.io/bitnamicharts"), + }, nil + }, + }, + } + + manifestsPath, err := c.configureManifests(ctx) + require.NoError(t, err) + + assert.Equal(t, "$ARTEFACTS_DIR/kubernetes/manifests", manifestsPath) + + manifestPath := filepath.Join(ctx.ArtefactsDir, k8sDir, k8sManifestsDir, "sample-crd.yaml") + + b, err := os.ReadFile(manifestPath) + require.NoError(t, err) + + contents := string(b) + assert.Contains(t, contents, "apiVersion: apps/v1") + assert.Contains(t, contents, "kind: Deployment") + assert.Contains(t, contents, "name: my-nginx") + assert.Contains(t, contents, "image: nginx:1.14.2") + + chartPath := filepath.Join(ctx.ArtefactsDir, k8sDir, k8sManifestsDir, "apache.yaml") + chartContent := `apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: apache + namespace: kube-system + annotations: + edge.suse.com/repository-url: oci://registry-1.docker.io/bitnamicharts + edge.suse.com/source: edge-image-builder +spec: + version: 10.7.0 + valuesContent: |4- + values: content + chartContent: some-content + targetNamespace: web + createNamespace: true +` + b, err = os.ReadFile(chartPath) + require.NoError(t, err) + + assert.Equal(t, chartContent, string(b)) +} + +func TestConfigureKubernetes_SuccessfulRKE2ServerWithManifests(t *testing.T) { ctx, teardown := setupContext(t) defer teardown() ctx.ImageDefinition.Kubernetes = image.Kubernetes{ Version: "v1.29.0+rke2r1", - } - ctx.ImageDefinition.Kubernetes.Manifests.URLs = []string{ - "k8s.io/examples/application/nginx-app.yaml", + Network: image.Network{ + APIVIP: "192.168.122.100", + APIHost: "api.cluster01.hosted.on.edge.suse.com", + }, } c := Combustion{ KubernetesScriptDownloader: mockKubernetesScriptDownloader{ downloadScript: func(distribution, destPath string) (string, error) { - return kubernetesScriptInstaller, nil + return "install-k8s.sh", nil }, }, KubernetesArtefactDownloader: mockKubernetesArtefactDownloader{ - downloadRKE2Artefacts: func(arch image.Arch, version, cni string, multusEnabled bool, installpath, imagesPath string) error { + downloadRKE2Artefacts: func(arch image.Arch, version, cni string, multusEnabled bool, installPath, imagesPath string) error { return nil }, }, + Registry: mockEmbeddedRegistry{ + manifestsPathFunc: func() string { + // Use local test files + return filepath.Join("..", "registry", "testdata") + }, + helmChartsFunc: func() ([]*registry.HelmCRD, error) { + return nil, nil + }, + }, } - k8sCombDir := filepath.Join(ctx.CombustionDir, K8sDir) - require.NoError(t, os.Mkdir(k8sCombDir, os.ModePerm)) + scripts, err := c.configureKubernetes(ctx) + require.NoError(t, err) + require.Len(t, scripts, 1) - _, err := c.configureKubernetes(ctx) + // Script file assertions + scriptPath := filepath.Join(ctx.CombustionDir, scripts[0]) - require.ErrorContains(t, err, "configuring kubernetes manifests: downloading manifests to combustion dir: downloading manifest 'k8s.io/examples/application/nginx-app.yaml': executing request: Get \"k8s.io/examples/application/nginx-app.yaml\": unsupported protocol scheme \"\"") -} + info, err := os.Stat(scriptPath) + require.NoError(t, err) -func TestConfigureManifestsNoSetup(t *testing.T) { - // Setup - ctx, teardown := setupContext(t) - defer teardown() + assert.Equal(t, fileio.ExecutablePerms, info.Mode()) - // Test - manifestsPath, err := configureManifests(ctx) + b, err := os.ReadFile(scriptPath) + require.NoError(t, err) - // Verify + contents := string(b) + assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/images/* /var/lib/rancher/rke2/agent/images/") + assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/server.yaml /etc/rancher/rke2/config.yaml") + assert.Contains(t, contents, "echo \"192.168.122.100 api.cluster01.hosted.on.edge.suse.com\" >> /etc/hosts") + assert.Contains(t, contents, "export INSTALL_RKE2_ARTIFACT_PATH=$ARTEFACTS_DIR/kubernetes/install") + assert.Contains(t, contents, "sh $ARTEFACTS_DIR/kubernetes/install-k8s.sh") + assert.Contains(t, contents, "systemctl enable rke2-server.service") + assert.Contains(t, contents, "mkdir -p /var/lib/rancher/rke2/server/manifests/") + assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/manifests/* /var/lib/rancher/rke2/server/manifests/") + assert.Contains(t, contents, "cp $ARTEFACTS_DIR/kubernetes/registries.yaml /etc/rancher/rke2/registries.yaml") + + // Config file assertions + configPath := filepath.Join(ctx.ArtefactsDir, "kubernetes", "server.yaml") + + info, err = os.Stat(configPath) require.NoError(t, err) - assert.Equal(t, "", manifestsPath) + + assert.Equal(t, fileio.NonExecutablePerms, info.Mode()) + + b, err = os.ReadFile(configPath) + require.NoError(t, err) + + var configContents map[string]any + require.NoError(t, yaml.Unmarshal(b, &configContents)) + + require.Contains(t, configContents, "cni") + assert.Equal(t, "cilium", configContents["cni"], "default CNI is not set") + assert.Equal(t, nil, configContents["server"]) + assert.Equal(t, []any{"192.168.122.100", "api.cluster01.hosted.on.edge.suse.com"}, configContents["tls-san"]) + + // Manifest assertions + manifest := filepath.Join(ctx.ArtefactsDir, k8sDir, k8sManifestsDir, "sample-crd.yaml") + info, err = os.Stat(manifest) + require.NoError(t, err) + assert.Equal(t, fileio.NonExecutablePerms, info.Mode()) + + b, err = os.ReadFile(manifest) + require.NoError(t, err) + + contents = string(b) + assert.Contains(t, contents, "apiVersion: apps/v1") + assert.Contains(t, contents, "kind: Deployment") + assert.Contains(t, contents, "name: my-nginx") + assert.Contains(t, contents, "image: nginx:1.14.2") } diff --git a/pkg/combustion/registry.go b/pkg/combustion/registry.go index 247accfc..b13168dd 100644 --- a/pkg/combustion/registry.go +++ b/pkg/combustion/registry.go @@ -3,6 +3,7 @@ package combustion import ( _ "embed" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -13,25 +14,18 @@ import ( "github.com/suse-edge/edge-image-builder/pkg/fileio" "github.com/suse-edge/edge-image-builder/pkg/image" "github.com/suse-edge/edge-image-builder/pkg/log" - "github.com/suse-edge/edge-image-builder/pkg/registry" "github.com/suse-edge/edge-image-builder/pkg/template" "go.uber.org/zap" - "gopkg.in/yaml.v3" ) const ( registryScriptName = "26-embedded-registry.sh" registryTarSuffix = "registry.tar.zst" registryComponentName = "embedded artifact registry" - registryLogFileName = "embedded-registry.log" hauler = "hauler" registryDir = "registry" registryPort = "6545" registryMirrorsFileName = "registries.yaml" - - HelmDir = "helm" - ValuesDir = "values" - CertsDir = "certs" ) var ( @@ -48,66 +42,50 @@ func (c *Combustion) configureRegistry(ctx *image.Context) ([]string, error) { return nil, nil } - configured, err := c.configureEmbeddedArtifactRegistry(ctx) + images, err := c.Registry.ContainerImages() if err != nil { log.AuditComponentFailed(registryComponentName) - return nil, fmt.Errorf("configuring embedded artifact registry: %w", err) + return nil, fmt.Errorf("extracting container images: %w", err) } - if !configured { + if len(images) == 0 { log.AuditComponentSkipped(registryComponentName) zap.S().Info("Skipping embedded artifact registry since the provided manifests/helm charts contain no images") return nil, nil } - script, err := writeRegistryScript(ctx) + script, err := c.configureEmbeddedArtifactRegistry(ctx, images) if err != nil { log.AuditComponentFailed(registryComponentName) - return nil, fmt.Errorf("writing registry script: %w", err) + return nil, fmt.Errorf("configuring embedded artifact registry: %w", err) } log.AuditComponentSuccessful(registryComponentName) return []string{script}, nil } -func addImageToHauler(ctx *image.Context, containerImage string) error { - args := []string{"store", "add", "image", containerImage, "-p", fmt.Sprintf("linux/%s", ctx.ImageDefinition.Image.Arch.Short())} +func storeImage(containerImage, arch string, outputWriter io.Writer) error { + args := []string{"store", "add", "image", containerImage, "-p", fmt.Sprintf("linux/%s", arch)} - cmd, registryLog, err := createRegistryCommand(ctx, hauler, args) - if err != nil { - return fmt.Errorf("preparing to add image to hauler store: %w", err) - } - defer func() { - if err = registryLog.Close(); err != nil { - zap.S().Warnf("failed to close registry log file properly: %s", err) - } - }() + cmd := exec.Command(hauler, args...) + cmd.Stdout = outputWriter + cmd.Stderr = outputWriter - if err = cmd.Run(); err != nil { - return fmt.Errorf("running hauler add image command: %w: ", err) - } - - return nil + return cmd.Run() } -func generateRegistryTar(ctx *image.Context, imageTarDest string) error { +func generateRegistryTar(imageTarDest string, outputWriter io.Writer) error { args := []string{"store", "save", "--filename", imageTarDest} - cmd, registryLog, err := createRegistryCommand(ctx, hauler, args) - if err != nil { - return fmt.Errorf("preparing to generate registry tar: %w", err) - } - defer func() { - if err = registryLog.Close(); err != nil { - zap.S().Warnf("failed to close registry log file properly: %s", err) - } - }() + cmd := exec.Command(hauler, args...) + cmd.Stdout = outputWriter + cmd.Stderr = outputWriter - if err = cmd.Run(); err != nil { - return fmt.Errorf("creating registry tar: %w: ", err) + if err := cmd.Run(); err != nil { + return fmt.Errorf("creating registry tarball: %w: ", err) } - if err = os.RemoveAll("store"); err != nil { + if err := os.RemoveAll("store"); err != nil { return fmt.Errorf("removing registry store: %w", err) } @@ -139,25 +117,11 @@ func writeRegistryScript(ctx *image.Context) (string, error) { return registryScriptName, nil } -func createRegistryCommand(ctx *image.Context, commandName string, args []string) (*exec.Cmd, *os.File, error) { - fullLogFilename := filepath.Join(ctx.BuildDir, registryLogFileName) - logFile, err := os.OpenFile(fullLogFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fileio.NonExecutablePerms) - if err != nil { - return nil, nil, fmt.Errorf("error opening registry log file %s: %w", registryLogFileName, err) - } - - cmd := exec.Command(commandName, args...) - cmd.Stdout = logFile - cmd.Stderr = logFile - - return cmd, logFile, nil -} - func IsEmbeddedArtifactRegistryConfigured(ctx *image.Context) bool { return len(ctx.ImageDefinition.EmbeddedArtifactRegistry.ContainerImages) != 0 || len(ctx.ImageDefinition.Kubernetes.Manifests.URLs) != 0 || len(ctx.ImageDefinition.Kubernetes.Helm.Charts) != 0 || - isComponentConfigured(ctx, filepath.Join(K8sDir, k8sManifestsDir)) + isComponentConfigured(ctx, localKubernetesManifestsPath()) } func getImageHostnames(containerImages []string) []string { @@ -202,158 +166,80 @@ func writeRegistryMirrors(ctx *image.Context, hostnames []string) error { return nil } -func (c *Combustion) configureEmbeddedArtifactRegistry(ctx *image.Context) (bool, error) { - helmCharts, err := c.parseHelmCharts(ctx) - if err != nil { - return false, fmt.Errorf("parsing helm charts: %w", err) - } - - if err = storeHelmCharts(ctx, helmCharts); err != nil { - return false, fmt.Errorf("storing helm charts: %w", err) - } - - manifestImages, err := parseManifests(ctx) - if err != nil { - return false, fmt.Errorf("parsing manifests: %w", err) - } - - images := containerImages(ctx.ImageDefinition.EmbeddedArtifactRegistry.ContainerImages, manifestImages, helmCharts) - if len(images) == 0 { - return false, nil +func (c *Combustion) configureEmbeddedArtifactRegistry(ctx *image.Context, containerImages []string) (string, error) { + if len(containerImages) == 0 { + return "", fmt.Errorf("no container images specified") } if ctx.ImageDefinition.Kubernetes.Version != "" { - hostnames := getImageHostnames(images) + hostnames := getImageHostnames(containerImages) - err = writeRegistryMirrors(ctx, hostnames) - if err != nil { - return false, fmt.Errorf("writing registry mirrors: %w", err) + if err := writeRegistryMirrors(ctx, hostnames); err != nil { + return "", fmt.Errorf("writing registry mirrors: %w", err) } } artefactsPath := registryArtefactsPath(ctx) - if err = os.Mkdir(artefactsPath, os.ModePerm); err != nil { - return false, fmt.Errorf("creating registry dir: %w", err) + if err := os.Mkdir(artefactsPath, os.ModePerm); err != nil { + return "", fmt.Errorf("creating registry dir: %w", err) } - if err = populateRegistry(ctx, images); err != nil { - return false, fmt.Errorf("populating registry: %w", err) + if err := populateRegistry(ctx, containerImages); err != nil { + return "", fmt.Errorf("populating registry: %w", err) } sourcePath := "/usr/bin/hauler" destinationPath := filepath.Join(registryArtefactsPath(ctx), "hauler") - if err = fileio.CopyFile(sourcePath, destinationPath, fileio.ExecutablePerms); err != nil { - return false, fmt.Errorf("copying hauler binary: %w", err) - } - - return true, nil -} - -func containerImages(embeddedImages []image.ContainerImage, manifestImages []string, helmCharts []*registry.HelmChart) []string { - imageSet := map[string]bool{} - - for _, img := range embeddedImages { - imageSet[img.Name] = true - } - - for _, img := range manifestImages { - imageSet[img] = true - } - - for _, chart := range helmCharts { - for _, img := range chart.ContainerImages { - imageSet[img] = true - } + if err := fileio.CopyFile(sourcePath, destinationPath, fileio.ExecutablePerms); err != nil { + return "", fmt.Errorf("copying hauler binary: %w", err) } - var images []string - - for img := range imageSet { - images = append(images, img) + script, err := writeRegistryScript(ctx) + if err != nil { + return "", fmt.Errorf("writing registry script: %w", err) } - return images + return script, nil } -func parseManifests(ctx *image.Context) ([]string, error) { - var manifestSrcDir string - if componentDir := filepath.Join(K8sDir, k8sManifestsDir); isComponentConfigured(ctx, componentDir) { - manifestSrcDir = filepath.Join(ctx.ImageConfigDir, componentDir) - } - - if manifestSrcDir != "" && ctx.ImageDefinition.Kubernetes.Version == "" { - return nil, fmt.Errorf("kubernetes manifests are provided but kubernetes version is not configured") - } - - return registry.ManifestImages(ctx.ImageDefinition.Kubernetes.Manifests.URLs, manifestSrcDir) +func registryArtefactsPath(ctx *image.Context) string { + return filepath.Join(ctx.ArtefactsDir, registryDir) } -func (c *Combustion) parseHelmCharts(ctx *image.Context) ([]*registry.HelmChart, error) { - if len(ctx.ImageDefinition.Kubernetes.Helm.Charts) == 0 { - return nil, nil - } - - if ctx.ImageDefinition.Kubernetes.Version == "" { - return nil, fmt.Errorf("helm charts are provided but kubernetes version is not configured") - } - - buildDir := filepath.Join(ctx.BuildDir, HelmDir) - if err := os.MkdirAll(buildDir, os.ModePerm); err != nil { - return nil, fmt.Errorf("creating helm dir: %w", err) - } - - helmValuesDir := filepath.Join(ctx.ImageConfigDir, K8sDir, HelmDir, ValuesDir) - - return registry.HelmCharts(&ctx.ImageDefinition.Kubernetes.Helm, helmValuesDir, buildDir, ctx.ImageDefinition.Kubernetes.Version, c.HelmClient) -} +func populateRegistry(ctx *image.Context, images []string) error { + bar := progressbar.Default(int64(len(images)), "Populating Embedded Artifact Registry...") + zap.S().Infof("Adding the following images to the embedded artifact registry:\n%s", images) -func storeHelmCharts(ctx *image.Context, helmCharts []*registry.HelmChart) error { - if len(helmCharts) == 0 { - return nil - } + const registryLogFileName = "embedded-registry.log" + logFilename := filepath.Join(ctx.BuildDir, registryLogFileName) - manifestsDir := filepath.Join(kubernetesArtefactsPath(ctx), k8sManifestsDir) - if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil { - return fmt.Errorf("creating kubernetes manifests dir: %w", err) + logFile, err := os.OpenFile(logFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fileio.NonExecutablePerms) + if err != nil { + return fmt.Errorf("opening registry log file: %w", err) } - for _, chart := range helmCharts { - data, err := yaml.Marshal(chart.CRD) - if err != nil { - return fmt.Errorf("marshaling resource: %w", err) - } - - chartFileName := fmt.Sprintf("%s.yaml", chart.CRD.Metadata.Name) - if err = os.WriteFile(filepath.Join(manifestsDir, chartFileName), data, fileio.NonExecutablePerms); err != nil { - return fmt.Errorf("storing manifest '%s: %w", chartFileName, err) + defer func() { + if err = logFile.Close(); err != nil { + zap.S().Warnf("Failed to close registry log file properly: %v", err) } - } - - return nil -} - -func registryArtefactsPath(ctx *image.Context) string { - return filepath.Join(ctx.ArtefactsDir, registryDir) -} + }() -func populateRegistry(ctx *image.Context, images []string) error { - bar := progressbar.Default(int64(len(images)), "Populating Embedded Artifact Registry...") - zap.S().Infof("Adding the following images to the embedded artifact registry:\n%s", images) + arch := ctx.ImageDefinition.Image.Arch.Short() - for _, i := range images { - if err := addImageToHauler(ctx, i); err != nil { - return fmt.Errorf("adding image to hauler: %w", err) + for _, img := range images { + if err = storeImage(img, arch, logFile); err != nil { + return fmt.Errorf("adding image to registry store: %w", err) } - convertedImage := strings.ReplaceAll(i, "/", "_") + convertedImage := strings.ReplaceAll(img, "/", "_") convertedImageName := fmt.Sprintf("%s-%s", convertedImage, registryTarSuffix) imageTarDest := filepath.Join(registryArtefactsPath(ctx), convertedImageName) - if err := generateRegistryTar(ctx, imageTarDest); err != nil { - return fmt.Errorf("generating hauler store tar: %w", err) + if err = generateRegistryTar(imageTarDest, logFile); err != nil { + return fmt.Errorf("generating registry store tarball: %w", err) } - if err := bar.Add(1); err != nil { + if err = bar.Add(1); err != nil { zap.S().Debugf("Error incrementing the progress bar: %s", err) } } diff --git a/pkg/combustion/registry_test.go b/pkg/combustion/registry_test.go index 55391a29..b8a27d56 100644 --- a/pkg/combustion/registry_test.go +++ b/pkg/combustion/registry_test.go @@ -8,35 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/suse-edge/edge-image-builder/pkg/image" - "github.com/suse-edge/edge-image-builder/pkg/registry" ) -func TestCreateRegistryCommand(t *testing.T) { - // Setup - ctx, teardown := setupContext(t) - defer teardown() - - // Test - cmd, logFile, err := createRegistryCommand(ctx, "testName", []string{"--flag", "test"}) - - // Verify - require.NoError(t, err) - require.NotNil(t, cmd) - - expectedCommand := "testName" - expectedArgs := []string{"testName", "--flag", "test"} - - assert.Equal(t, expectedCommand, cmd.Path) - assert.Equal(t, expectedArgs, cmd.Args) - - assert.Equal(t, logFile, cmd.Stdout) - assert.Equal(t, logFile, cmd.Stderr) - - foundFile := filepath.Join(ctx.BuildDir, "embedded-registry.log") - _, err = os.ReadFile(foundFile) - require.NoError(t, err) -} - func TestWriteRegistryScript(t *testing.T) { // Setup ctx, teardown := setupContext(t) @@ -180,7 +153,7 @@ func TestWriteRegistryMirrorsValid(t *testing.T) { // Verify require.NoError(t, err) - manifestFileName := filepath.Join(ctx.ArtefactsDir, K8sDir, registryMirrorsFileName) + manifestFileName := filepath.Join(ctx.ArtefactsDir, k8sDir, registryMirrorsFileName) foundBytes, err := os.ReadFile(manifestFileName) require.NoError(t, err) @@ -207,87 +180,3 @@ func TestGetImageHostnames(t *testing.T) { // Verify assert.Equal(t, expectedHostnames, hostnames) } - -func TestContainerImages(t *testing.T) { - embeddedImages := []image.ContainerImage{ - { - Name: "hello-world:latest", - }, - { - Name: "embedded-image:1.0.0", - }, - } - - manifestImages := []string{ - "hello-world:latest", - "manifest-image:1.0.0", - } - - helmCharts := []*registry.HelmChart{ - { - ContainerImages: []string{ - "hello-world:latest", - "helm-image:1.0.0", - }, - }, - { - ContainerImages: []string{ - "helm-image:2.0.0", - }, - }, - } - - assert.ElementsMatch(t, []string{ - "hello-world:latest", - "embedded-image:1.0.0", - "manifest-image:1.0.0", - "helm-image:1.0.0", - "helm-image:2.0.0", - }, containerImages(embeddedImages, manifestImages, helmCharts)) -} - -func TestStoreHelmCharts(t *testing.T) { - ctx, teardown := setupContext(t) - defer teardown() - - helmChart := &image.HelmChart{ - Name: "apache", - RepositoryName: "apache-repo", - TargetNamespace: "web", - CreateNamespace: true, - InstallationNamespace: "kube-system", - Version: "10.7.0", - ValuesFile: "", - } - - charts := []*registry.HelmChart{ - { - CRD: registry.NewHelmCRD(helmChart, "some-content", ` -values: content`, "oci://registry-1.docker.io/bitnamicharts"), - }, - } - - require.NoError(t, storeHelmCharts(ctx, charts)) - - apachePath := filepath.Join(ctx.ArtefactsDir, K8sDir, k8sManifestsDir, "apache.yaml") - apacheContent := `apiVersion: helm.cattle.io/v1 -kind: HelmChart -metadata: - name: apache - namespace: kube-system - annotations: - edge.suse.com/repository-url: oci://registry-1.docker.io/bitnamicharts - edge.suse.com/source: edge-image-builder -spec: - version: 10.7.0 - valuesContent: |4- - values: content - chartContent: some-content - targetNamespace: web - createNamespace: true -` - contents, err := os.ReadFile(apachePath) - require.NoError(t, err) - - assert.Equal(t, apacheContent, string(contents)) -} diff --git a/pkg/eib/eib.go b/pkg/eib/eib.go index 585e83fb..3a5b79e8 100644 --- a/pkg/eib/eib.go +++ b/pkg/eib/eib.go @@ -18,6 +18,7 @@ import ( "github.com/suse-edge/edge-image-builder/pkg/log" "github.com/suse-edge/edge-image-builder/pkg/network" "github.com/suse-edge/edge-image-builder/pkg/podman" + "github.com/suse-edge/edge-image-builder/pkg/registry" "github.com/suse-edge/edge-image-builder/pkg/rpm" "github.com/suse-edge/edge-image-builder/pkg/rpm/resolver" "go.uber.org/zap" @@ -139,8 +140,14 @@ func buildCombustion(ctx *image.Context, rootDir string) (*combustion.Combustion } if combustion.IsEmbeddedArtifactRegistryConfigured(ctx) { - certsDir := filepath.Join(ctx.ImageConfigDir, combustion.K8sDir, combustion.HelmDir, combustion.CertsDir) - combustionHandler.HelmClient = helm.New(ctx.BuildDir, certsDir) + helmClient := helm.New(ctx.BuildDir, combustion.HelmCertsPath(ctx)) + + r, err := registry.New(ctx, combustion.KubernetesManifestsPath(ctx), helmClient, combustion.HelmValuesPath(ctx)) + if err != nil { + return nil, fmt.Errorf("initialising embedded artifact registry: %w", err) + } + + combustionHandler.Registry = r } if ctx.ImageDefinition.Kubernetes.Version != "" { diff --git a/pkg/image/context.go b/pkg/image/context.go index aeea7bb2..6cf9f3fe 100644 --- a/pkg/image/context.go +++ b/pkg/image/context.go @@ -1,12 +1,5 @@ package image -type HelmClient interface { - AddRepo(repository *HelmRepository) error - RegistryLogin(repository *HelmRepository) error - Pull(chart string, repository *HelmRepository, version, destDir string) (string, error) - Template(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) -} - type LocalRPMConfig struct { // RPMPath is the path to the directory holding RPMs that will be side-loaded RPMPath string diff --git a/pkg/image/validation/kubernetes.go b/pkg/image/validation/kubernetes.go index 3f0671c9..233427da 100644 --- a/pkg/image/validation/kubernetes.go +++ b/pkg/image/validation/kubernetes.go @@ -35,7 +35,7 @@ func validateKubernetes(ctx *image.Context) []FailedValidation { failures = append(failures, validateNodes(&def.Kubernetes)...) failures = append(failures, validateManifestURLs(&def.Kubernetes)...) - failures = append(failures, validateHelm(&def.Kubernetes, ctx.ImageConfigDir)...) + failures = append(failures, validateHelm(&def.Kubernetes, combustion.HelmValuesPath(ctx), combustion.HelmCertsPath(ctx))...) return failures } @@ -146,7 +146,7 @@ func validateManifestURLs(k8s *image.Kubernetes) []FailedValidation { return failures } -func validateHelm(k8s *image.Kubernetes, imageConfigDir string) []FailedValidation { +func validateHelm(k8s *image.Kubernetes, valuesDir, certsDir string) []FailedValidation { var failures []FailedValidation if len(k8s.Helm.Charts) == 0 { @@ -175,20 +175,20 @@ func validateHelm(k8s *image.Kubernetes, imageConfigDir string) []FailedValidati seenHelmRepos := make(map[string]bool) for _, chart := range k8s.Helm.Charts { c := chart - failures = append(failures, validateChart(&c, helmRepositoryNames, imageConfigDir)...) + failures = append(failures, validateChart(&c, helmRepositoryNames, valuesDir)...) seenHelmRepos[chart.RepositoryName] = true } for _, repo := range k8s.Helm.Repositories { r := repo - failures = append(failures, validateRepo(&r, seenHelmRepos, imageConfigDir)...) + failures = append(failures, validateRepo(&r, seenHelmRepos, certsDir)...) } return failures } -func validateChart(chart *image.HelmChart, repositoryNames []string, imageConfigDir string) []FailedValidation { +func validateChart(chart *image.HelmChart, repositoryNames []string, valuesDir string) []FailedValidation { var failures []FailedValidation if chart.Name == "" { @@ -219,7 +219,7 @@ func validateChart(chart *image.HelmChart, repositoryNames []string, imageConfig }) } - if failure := validateHelmChartValues(chart.Name, chart.ValuesFile, imageConfigDir); failure != "" { + if failure := validateHelmChartValues(chart.Name, chart.ValuesFile, valuesDir); failure != "" { failures = append(failures, FailedValidation{ UserMessage: failure, }) @@ -228,7 +228,7 @@ func validateChart(chart *image.HelmChart, repositoryNames []string, imageConfig return failures } -func validateRepo(repo *image.HelmRepository, seenHelmRepos map[string]bool, imageConfigDir string) []FailedValidation { +func validateRepo(repo *image.HelmRepository, seenHelmRepos map[string]bool, certsDir string) []FailedValidation { var failures []FailedValidation parsedURL, err := url.Parse(repo.URL) @@ -246,7 +246,7 @@ func validateRepo(repo *image.HelmRepository, seenHelmRepos map[string]bool, ima failures = append(failures, validateHelmRepoAuth(repo)...) failures = append(failures, validateHelmRepoArgs(parsedURL, repo)...) - if failure := validateHelmRepoCert(repo.Name, repo.CAFile, imageConfigDir); failure != "" { + if failure := validateHelmRepoCert(repo.Name, repo.CAFile, certsDir); failure != "" { failures = append(failures, FailedValidation{ UserMessage: failure, }) @@ -353,7 +353,7 @@ func validateHelmRepoArgs(parsedURL *url.URL, repo *image.HelmRepository) []Fail return failures } -func validateHelmRepoCert(repoName, certFile string, imageConfigDir string) string { +func validateHelmRepoCert(repoName, certFile, certsDir string) string { if certFile == "" { return "" } @@ -364,7 +364,7 @@ func validateHelmRepoCert(repoName, certFile string, imageConfigDir string) stri repoName, strings.Join(validExtensions, ", ")) } - certFilePath := filepath.Join(imageConfigDir, combustion.K8sDir, combustion.HelmDir, combustion.CertsDir, certFile) + certFilePath := filepath.Join(certsDir, certFile) _, err := os.Stat(certFilePath) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -378,7 +378,7 @@ func validateHelmRepoCert(repoName, certFile string, imageConfigDir string) stri return "" } -func validateHelmChartValues(chartName, valuesFile string, imageConfigDir string) string { +func validateHelmChartValues(chartName, valuesFile, valuesDir string) string { if valuesFile == "" { return "" } @@ -387,7 +387,7 @@ func validateHelmChartValues(chartName, valuesFile string, imageConfigDir string 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) + valuesFilePath := filepath.Join(valuesDir, valuesFile) _, err := os.Stat(valuesFilePath) if err != nil { if errors.Is(err, os.ErrNotExist) { diff --git a/pkg/image/validation/kubernetes_test.go b/pkg/image/validation/kubernetes_test.go index fb302420..e8be1d9e 100644 --- a/pkg/image/validation/kubernetes_test.go +++ b/pkg/image/validation/kubernetes_test.go @@ -973,7 +973,7 @@ func TestValidateHelmCharts(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { k := test.K8s - failures := validateHelm(&k, "") + failures := validateHelm(&k, "kubernetes/helm/values", "kubernetes/helm/certs") assert.Len(t, failures, len(test.ExpectedFailedMessages)) var foundMessages []string diff --git a/pkg/registry/helm.go b/pkg/registry/helm.go index 29a06a79..ad687e4f 100644 --- a/pkg/registry/helm.go +++ b/pkg/registry/helm.go @@ -5,110 +5,66 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/suse-edge/edge-image-builder/pkg/image" ) -type HelmChart struct { - CRD HelmCRD - ContainerImages []string -} - -func HelmCharts(helm *image.Helm, valuesDir, buildDir, kubeVersion string, helmClient image.HelmClient) ([]*HelmChart, error) { - var charts []*HelmChart - chartRepoMap := mapChartRepos(helm) +func (r *Registry) HelmCharts() ([]*HelmCRD, error) { + var crds []*HelmCRD - for _, helmChart := range helm.Charts { - c := helmChart - 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) + for _, chart := range r.helmCharts { + data, err := os.ReadFile(chart.localPath) if err != nil { - return nil, fmt.Errorf("handling chart resource: %w", err) + return nil, fmt.Errorf("reading chart: %w", err) } - charts = append(charts, chart) - } + chartContent := base64.StdEncoding.EncodeToString(data) - return charts, nil -} - -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 != "" { - var err error - valuesPath = filepath.Join(valuesDir, chart.ValuesFile) - valuesContent, err = os.ReadFile(valuesPath) - if err != nil { - return nil, fmt.Errorf("reading values content: %w", err) + var valuesContent []byte + if chart.ValuesFile != "" { + valuesPath := filepath.Join(r.helmValuesDir, chart.ValuesFile) + valuesContent, err = os.ReadFile(valuesPath) + if err != nil { + return nil, fmt.Errorf("reading values content: %w", err) + } } - } - - chartPath, err := downloadChart(chart, repo, helmClient, buildDir) - if err != nil { - return nil, fmt.Errorf("downloading chart: %w", err) - } - - images, err := getChartContainerImages(chart, helmClient, chartPath, valuesPath, kubeVersion) - if err != nil { - return nil, fmt.Errorf("getting chart container images: %w", err) - } - - chartContent, err := getChartContent(chartPath) - if err != nil { - return nil, fmt.Errorf("getting chart content: %w", err) - } - helmChart := HelmChart{ - CRD: NewHelmCRD(chart, chartContent, string(valuesContent), repo.URL), - ContainerImages: images, + crd := NewHelmCRD(&chart.HelmChart, chartContent, string(valuesContent), chart.repositoryURL) + crds = append(crds, crd) } - return &helmChart, nil + return crds, nil } -func downloadChart(chart *image.HelmChart, repo *image.HelmRepository, helmClient image.HelmClient, destDir string) (string, error) { - if strings.HasPrefix(repo.URL, "http") { - if err := helmClient.AddRepo(repo); err != nil { - return "", fmt.Errorf("adding repo: %w", err) - } - } else if repo.Authentication.Username != "" && repo.Authentication.Password != "" { - if err := helmClient.RegistryLogin(repo); err != nil { - return "", fmt.Errorf("logging into registry: %w", err) - } - } +func (r *Registry) helmChartImages() ([]string, error) { + var containerImages []string - chartPath, err := helmClient.Pull(chart.Name, repo, chart.Version, destDir) - if err != nil { - return "", fmt.Errorf("pulling chart: %w", err) - } + for _, chart := range r.helmCharts { + var valuesPath string + if chart.ValuesFile != "" { + valuesPath = filepath.Join(r.helmValuesDir, chart.ValuesFile) + } - return chartPath, nil -} + images, err := r.getChartContainerImages(&chart.HelmChart, chart.localPath, valuesPath, r.kubeVersion) + if err != nil { + return nil, err + } -func getChartContent(chartPath string) (string, error) { - data, err := os.ReadFile(chartPath) - if err != nil { - return "", fmt.Errorf("reading chart: %w", err) + containerImages = append(containerImages, images...) } - return base64.StdEncoding.EncodeToString(data), nil + return containerImages, nil } -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, chart.TargetNamespace) +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) if err != nil { return nil, fmt.Errorf("templating chart: %w", err) } containerImages := map[string]bool{} for _, resource := range chartResources { - storeManifestImages(resource, containerImages) + extractManifestImages(resource, containerImages) } var images []string @@ -118,18 +74,3 @@ func getChartContainerImages(chart *image.HelmChart, helmClient image.HelmClient 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_crd.go b/pkg/registry/helm_crd.go index 1cd57eae..a771ee2f 100644 --- a/pkg/registry/helm_crd.go +++ b/pkg/registry/helm_crd.go @@ -27,8 +27,8 @@ type HelmCRD struct { } `yaml:"spec"` } -func NewHelmCRD(chart *image.HelmChart, chartContent, valuesContent, repositoryURL string) HelmCRD { - return HelmCRD{ +func NewHelmCRD(chart *image.HelmChart, chartContent, valuesContent, repositoryURL string) *HelmCRD { + return &HelmCRD{ APIVersion: helmChartAPIVersion, Kind: helmChartKind, Metadata: struct { diff --git a/pkg/registry/helm_test.go b/pkg/registry/helm_test.go index ee6c0a23..902c8b08 100644 --- a/pkg/registry/helm_test.go +++ b/pkg/registry/helm_test.go @@ -47,132 +47,76 @@ func (m mockHelmClient) Template(chart, repository, version, valuesFilePath, kub panic("not implemented") } -func TestHelmCharts_ValuesFileNotFoundError(t *testing.T) { - 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", - }, - }, - } - - 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) -} - -func TestHandleChart_MissingValuesDir(t *testing.T) { - helmChart := &image.HelmChart{ - 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", - } - - 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", - RepositoryName: "apache-repo", - Version: "10.7.0", - } - helmRepo := &image.HelmRepository{ - Name: "suse-edge", - URL: "https://suse-edge.github.io/charts", - } +func TestRegistry_HelmChartImages_Empty(t *testing.T) { + var registry Registry - helmClient := mockHelmClient{ - addRepoFunc: func(repository *image.HelmRepository) error { - return fmt.Errorf("failed downloading") - }, - } - - charts, err := handleChart(helmChart, helmRepo, "", "", "", helmClient) - require.Error(t, err) - assert.ErrorContains(t, err, "downloading chart: adding repo: failed downloading") - assert.Nil(t, charts) + images, err := registry.helmChartImages() + require.NoError(t, err) + assert.Empty(t, images) } -func TestHandleChart_FailedTemplate(t *testing.T) { - helmChart := &image.HelmChart{ - Name: "apache", - RepositoryName: "apache-repo", - Version: "10.7.0", - } - helmRepo := &image.HelmRepository{ - Name: "apache-repo", - URL: "oci://registry-1.docker.io/bitnamicharts", - } - - helmClient := mockHelmClient{ - addRepoFunc: func(repository *image.HelmRepository) error { - return nil - }, - registryLoginFunc: func(repository *image.HelmRepository) error { - return nil - }, - pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { - return "", nil +func TestRegistry_HelmChartImages_TemplateError(t *testing.T) { + registry := Registry{ + helmCharts: []*helmChart{ + { + HelmChart: image.HelmChart{ + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + repositoryURL: "oci://registry-1.docker.io/bitnamicharts", + }, }, - templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) { - return nil, fmt.Errorf("failed templating") + helmClient: mockHelmClient{ + templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) { + return nil, fmt.Errorf("failed templating") + }, }, } - charts, err := handleChart(helmChart, helmRepo, "", "", "", helmClient) + images, err := registry.helmChartImages() require.Error(t, err) assert.ErrorContains(t, err, "templating chart: failed templating") - assert.Nil(t, charts) + assert.Nil(t, images) } -func TestHandleChart_FailedGetChartContent(t *testing.T) { - helmChart := &image.HelmChart{ - Name: "apache", - RepositoryName: "apache-repo", - Version: "10.7.0", - } - helmRepo := &image.HelmRepository{ - Name: "apache-repo", - URL: "oci://registry-1.docker.io/bitnamicharts", - } - - helmClient := mockHelmClient{ - addRepoFunc: func(repository *image.HelmRepository) error { - return nil - }, - registryLoginFunc: func(repository *image.HelmRepository) error { - return nil - }, - pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { - return "does-not-exist.tgz", nil +func TestRegistry_HelmChartImages(t *testing.T) { + registry := Registry{ + helmCharts: []*helmChart{ + { + HelmChart: image.HelmChart{ + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + }, }, - templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) { - return nil, nil + helmClient: mockHelmClient{ + templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) { + return []map[string]any{ + { + "kind": "Deployment", + "image": "apache-image:1.1.1", + }, + { + "kind": "Service", + "image": "apache", // not included due to incompatible kind + }, + { + "kind": "Pod", + "image": "apache-image:1.2.3", + }, + { + "kind": "PersistentVolume", // missing image field + }, + }, nil + }, }, } - 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) + images, err := registry.helmChartImages() + require.NoError(t, err) + assert.ElementsMatch(t, images, []string{"apache-image:1.1.1", "apache-image:1.2.3"}) } func TestDownloadChart_FailedAddingRepo(t *testing.T) { @@ -187,7 +131,7 @@ func TestDownloadChart_FailedAddingRepo(t *testing.T) { }, } - chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") + chartPath, err := downloadChart(helmClient, helmChart, helmRepo, "") require.Error(t, err) assert.ErrorContains(t, err, "adding repo: failed to add repo") assert.Empty(t, chartPath) @@ -204,9 +148,6 @@ func TestDownloadChart_ValidRegistryLogin(t *testing.T) { } helmClient := mockHelmClient{ - addRepoFunc: func(repository *image.HelmRepository) error { - return nil - }, registryLoginFunc: func(repository *image.HelmRepository) error { return nil }, @@ -215,7 +156,7 @@ func TestDownloadChart_ValidRegistryLogin(t *testing.T) { }, } - chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") + chartPath, err := downloadChart(helmClient, helmChart, helmRepo, "") require.NoError(t, err) assert.Equal(t, "apache-chart.tgz", chartPath) } @@ -231,15 +172,12 @@ func TestDownloadChart_FailedRegistryLogin(t *testing.T) { } helmClient := mockHelmClient{ - addRepoFunc: func(repository *image.HelmRepository) error { - return nil - }, registryLoginFunc: func(repository *image.HelmRepository) error { return fmt.Errorf("wrong credentials") }, } - chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") + chartPath, err := downloadChart(helmClient, helmChart, helmRepo, "") require.Error(t, err) assert.ErrorContains(t, err, "logging into registry: wrong credentials") assert.Empty(t, chartPath) @@ -260,7 +198,7 @@ func TestDownloadChart_FailedPulling(t *testing.T) { }, } - chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") + chartPath, err := downloadChart(helmClient, helmChart, helmRepo, "") require.Error(t, err) assert.ErrorContains(t, err, "pulling chart: failed pulling chart") assert.Empty(t, chartPath) @@ -286,87 +224,112 @@ func TestDownloadChart(t *testing.T) { }, } - chartPath, err := downloadChart(helmChart, helmRepo, helmClient, "") + chartPath, err := downloadChart(helmClient, helmChart, helmRepo, "") require.NoError(t, err) assert.Equal(t, "apache-chart.tgz", chartPath) } -func TestHelmCharts(t *testing.T) { - helm := &image.Helm{ - Charts: []image.HelmChart{ +func TestRegistry_HelmCharts(t *testing.T) { + helmDir, err := os.MkdirTemp("", "helm-charts-") + require.NoError(t, err) + defer func() { + assert.NoError(t, os.RemoveAll(helmDir)) + }() + + chartFile := filepath.Join(helmDir, "apache-chart.tgz") + require.NoError(t, os.WriteFile(chartFile, []byte("abc"), 0o600)) + + valuesFile := filepath.Join(helmDir, "apache-values.yaml") + require.NoError(t, os.WriteFile(valuesFile, []byte("abcd"), 0o600)) + + registry := Registry{ + helmCharts: []*helmChart{ { - Name: "apache", - RepositoryName: "apache-repo", - Version: "10.7.0", - InstallationNamespace: "apache-system", - CreateNamespace: true, - TargetNamespace: "web", + HelmChart: image.HelmChart{ + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + InstallationNamespace: "apache-system", + CreateNamespace: true, + TargetNamespace: "web", + ValuesFile: "apache-values.yaml", + }, + localPath: chartFile, + repositoryURL: "oci://registry-1.docker.io/bitnamicharts", }, }, - Repositories: []image.HelmRepository{ + helmValuesDir: helmDir, + } + + charts, err := registry.HelmCharts() + require.NoError(t, err) + + assert.Equal(t, helmChartAPIVersion, charts[0].APIVersion) + assert.Equal(t, helmChartKind, charts[0].Kind) + + assert.Equal(t, "apache", charts[0].Metadata.Name) + assert.Equal(t, "apache-system", charts[0].Metadata.Namespace) + + assert.Equal(t, "oci://registry-1.docker.io/bitnamicharts", charts[0].Metadata.Annotations["edge.suse.com/repository-url"]) + assert.Equal(t, "edge-image-builder", charts[0].Metadata.Annotations["edge.suse.com/source"]) + + assert.Equal(t, "10.7.0", charts[0].Spec.Version) + assert.Equal(t, "YWJj", charts[0].Spec.ChartContent) + assert.Equal(t, "web", charts[0].Spec.TargetNamespace) + assert.Equal(t, true, charts[0].Spec.CreateNamespace) + assert.Equal(t, "abcd", charts[0].Spec.ValuesContent) +} + +func TestRegistry_HelmCharts_NonExistingChart(t *testing.T) { + registry := Registry{ + helmCharts: []*helmChart{ { - Name: "apache-repo", - URL: "oci://registry-1.docker.io/bitnamicharts", + HelmChart: image.HelmChart{ + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + }, + localPath: "does-not-exist.tgz", + repositoryURL: "oci://registry-1.docker.io/bitnamicharts", }, }, } - dir, err := os.MkdirTemp("", "helm-chart-charts-") + charts, err := registry.HelmCharts() + require.Error(t, err) + assert.EqualError(t, err, "reading chart: open does-not-exist.tgz: no such file or directory") + assert.Nil(t, charts) +} + +func TestRegistry_HelmCharts_NonExistingValues(t *testing.T) { + helmDir, err := os.MkdirTemp("", "helm-charts-") require.NoError(t, err) defer func() { - assert.NoError(t, os.RemoveAll(dir)) + assert.NoError(t, os.RemoveAll(helmDir)) }() - file := filepath.Join(dir, "apache-chart.tgz") - require.NoError(t, os.WriteFile(file, []byte("abc"), 0o600)) + chartFile := filepath.Join(helmDir, "apache-chart.tgz") + require.NoError(t, os.WriteFile(chartFile, []byte("abc"), 0o600)) - helmClient := mockHelmClient{ - addRepoFunc: func(repository *image.HelmRepository) error { - return nil - }, - registryLoginFunc: func(repository *image.HelmRepository) error { - return nil - }, - pullFunc: func(chart string, repository *image.HelmRepository, version, destDir string) (string, error) { - return file, nil - }, - templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) { - chartResource := []map[string]any{ - { - "apiVersion": "v1", - "kind": "CronJob", - "spec": map[string]any{ - "image": "cronjob-image:0.5.6", - }, - }, - { - "apiVersion": "v1", - "kind": "Job", - "spec": map[string]any{ - "image": "job-image:6.1.0", - }, + registry := Registry{ + helmCharts: []*helmChart{ + { + HelmChart: image.HelmChart{ + Name: "apache", + RepositoryName: "apache-repo", + Version: "10.7.0", + ValuesFile: "does-not-exist.yaml", }, - } - - return chartResource, nil + localPath: chartFile, + }, }, + helmValuesDir: "values", } - 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"}) - - assert.Equal(t, helmChartAPIVersion, charts[0].CRD.APIVersion) - assert.Equal(t, helmChartKind, charts[0].CRD.Kind) - - assert.Equal(t, "apache", charts[0].CRD.Metadata.Name) - assert.Equal(t, "apache-system", charts[0].CRD.Metadata.Namespace) - - assert.Equal(t, "10.7.0", charts[0].CRD.Spec.Version) - assert.Equal(t, "YWJj", charts[0].CRD.Spec.ChartContent) - assert.Equal(t, "web", charts[0].CRD.Spec.TargetNamespace) - assert.Equal(t, true, charts[0].CRD.Spec.CreateNamespace) + charts, err := registry.HelmCharts() + require.Error(t, err) + assert.EqualError(t, err, "reading values content: open values/does-not-exist.yaml: no such file or directory") + assert.Nil(t, charts) } func TestMapChartRepos(t *testing.T) { @@ -406,5 +369,5 @@ func TestMapChartRepos(t *testing.T) { }, } - assert.True(t, reflect.DeepEqual(expectedMap, mapChartRepos(helm))) + assert.True(t, reflect.DeepEqual(expectedMap, mapChartsToRepos(helm))) } diff --git a/pkg/registry/manifests.go b/pkg/registry/manifests.go index 63826a8b..18ea8b0f 100644 --- a/pkg/registry/manifests.go +++ b/pkg/registry/manifests.go @@ -1,57 +1,44 @@ package registry import ( - "context" "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "slices" - "strings" - "github.com/suse-edge/edge-image-builder/pkg/http" - "go.uber.org/zap" "gopkg.in/yaml.v3" ) -func ManifestImages(manifestURLs []string, manifestsDir string) ([]string, error) { - var manifestPaths []string +func (r *Registry) manifestImages() ([]string, error) { + containerImages := make(map[string]bool) - if len(manifestURLs) != 0 { - paths, err := DownloadManifests(manifestURLs, os.TempDir()) - if err != nil { - return nil, fmt.Errorf("downloading manifests: %w", err) - } - - manifestPaths = append(manifestPaths, paths...) - } - - if manifestsDir != "" { - paths, err := getManifestPaths(manifestsDir) - if err != nil { - return nil, fmt.Errorf("getting local manifest paths: %w", err) + entries, err := os.ReadDir(r.manifestsDir) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, nil } - - manifestPaths = append(manifestPaths, paths...) + return nil, fmt.Errorf("reading manifest dir: %w", err) } - var imageSet = make(map[string]bool) + for _, entry := range entries { + path := filepath.Join(r.manifestsDir, entry.Name()) - for _, path := range manifestPaths { - manifests, err := readManifest(path) + resources, err := readManifest(path) if err != nil { - return nil, fmt.Errorf("reading manifest: %w", err) + return nil, fmt.Errorf("reading manifest '%s': %w", path, err) } - for _, manifestData := range manifests { - storeManifestImages(manifestData, imageSet) + for _, resource := range resources { + extractManifestImages(resource, containerImages) } } var images []string - for imageName := range imageSet { + for imageName := range containerImages { images = append(images, imageName) } @@ -61,31 +48,34 @@ func ManifestImages(manifestURLs []string, manifestsDir string) ([]string, error func readManifest(manifestPath string) ([]map[string]any, error) { manifestFile, err := os.Open(manifestPath) if err != nil { - return nil, fmt.Errorf("error opening manifest: %w", err) + return nil, fmt.Errorf("opening manifest: %w", err) } + defer manifestFile.Close() + + var resources []map[string]any - var manifests []map[string]any decoder := yaml.NewDecoder(manifestFile) for { - var manifest map[string]any - err = decoder.Decode(&manifest) - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, fmt.Errorf("error unmarshalling manifest yaml '%s': %w", manifestPath, err) + var r map[string]any + + if err = decoder.Decode(&r); err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("unmarshalling manifest: %w", err) } - manifests = append(manifests, manifest) + + resources = append(resources, r) } - if len(manifests) == 0 { + if len(resources) == 0 { return nil, fmt.Errorf("invalid manifest") } - return manifests, nil + return resources, nil } -func storeManifestImages(resource map[string]any, images map[string]bool) { +func extractManifestImages(resource map[string]any, images map[string]bool) { var k8sKinds = []string{ "Pod", "Deployment", @@ -123,44 +113,3 @@ func storeManifestImages(resource map[string]any, images map[string]bool) { findImages(resource) } - -func getManifestPaths(src string) ([]string, error) { - if src == "" { - return nil, fmt.Errorf("manifest source directory not defined") - } - - var manifestPaths []string - - manifests, err := os.ReadDir(src) - if err != nil { - return nil, fmt.Errorf("reading manifest source dir '%s': %w", src, err) - } - - for _, manifest := range manifests { - manifestName := strings.ToLower(manifest.Name()) - if filepath.Ext(manifestName) != ".yaml" && filepath.Ext(manifestName) != ".yml" { - zap.S().Warnf("Skipping %s as it is not a yaml file", manifest.Name()) - continue - } - - sourcePath := filepath.Join(src, manifest.Name()) - manifestPaths = append(manifestPaths, sourcePath) - } - - return manifestPaths, nil -} - -func DownloadManifests(manifestURLs []string, destPath string) ([]string, error) { - var manifestPaths []string - - for index, manifestURL := range manifestURLs { - filePath := filepath.Join(destPath, fmt.Sprintf("dl-manifest-%d.yaml", index+1)) - manifestPaths = append(manifestPaths, filePath) - - if err := http.DownloadFile(context.Background(), manifestURL, filePath, nil); err != nil { - return nil, fmt.Errorf("downloading manifest '%s': %w", manifestURL, err) - } - } - - return manifestPaths, nil -} diff --git a/pkg/registry/manifests_integration_test.go b/pkg/registry/manifests_integration_test.go index fa69b01c..3904323e 100644 --- a/pkg/registry/manifests_integration_test.go +++ b/pkg/registry/manifests_integration_test.go @@ -10,65 +10,54 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/suse-edge/edge-image-builder/pkg/fileio" + "github.com/suse-edge/edge-image-builder/pkg/image" ) -func TestDownloadManifests(t *testing.T) { +func TestManifestImages(t *testing.T) { // Setup - manifestDownloadDest := "downloaded-manifests" - require.NoError(t, os.Mkdir(manifestDownloadDest, 0o755)) + localManifestsDir := "local-manifests" + + require.NoError(t, os.Mkdir(localManifestsDir, 0o755)) defer func() { - require.NoError(t, os.RemoveAll(manifestDownloadDest)) + assert.NoError(t, os.RemoveAll(localManifestsDir)) }() - expectedFilePath := filepath.Join(manifestDownloadDest, "dl-manifest-1.yaml") + sourceManifest := filepath.Join("testdata", "sample-crd.yaml") + destinationManifest := filepath.Join(localManifestsDir, "sample-crd.yaml") - manifestURLs := []string{ - "https://k8s.io/examples/application/nginx-app.yaml", - } + require.NoError(t, fileio.CopyFile(sourceManifest, destinationManifest, fileio.NonExecutablePerms)) - // Test - manifestPaths, err := DownloadManifests(manifestURLs, manifestDownloadDest) + buildDir := filepath.Join(os.TempDir(), "_manifests-integration") + require.NoError(t, os.MkdirAll(buildDir, os.ModePerm)) + defer func() { + assert.NoError(t, os.RemoveAll(buildDir)) + }() - // Verify - require.NoError(t, err) - assert.FileExists(t, expectedFilePath) - assert.Contains(t, manifestPaths, expectedFilePath) + ctx := &image.Context{ + BuildDir: buildDir, + ImageDefinition: &image.Definition{ + Kubernetes: image.Kubernetes{ + Manifests: image.Manifests{ + URLs: []string{"https://k8s.io/examples/application/nginx-app.yaml"}, + }, + }, + }, + } - foundBytes, err := os.ReadFile(expectedFilePath) + registry, err := New(ctx, localManifestsDir, nil, "") require.NoError(t, err) - found := string(foundBytes) - assert.Contains(t, found, "apiVersion: v1") - assert.Contains(t, found, "image: nginx:1.14.2") -} + // Test + containerImages, err := registry.manifestImages() -func TestManifestImages(t *testing.T) { - // Setup - expectedContainerImages := []string{ + // Verify + require.NoError(t, err) + assert.ElementsMatch(t, []string{ "custom-api:1.2.3", "mysql:5.7", "redis:6.0", "nginx:latest", "node:14", "nginx:1.14.2", - } - - manifestSrcDir := "local-manifests" - require.NoError(t, os.Mkdir(manifestSrcDir, 0o755)) - defer func() { - assert.NoError(t, os.RemoveAll(manifestSrcDir)) - }() - - localSampleManifestPath := filepath.Join("testdata", "sample-crd.yaml") - err := fileio.CopyFile(localSampleManifestPath, filepath.Join(manifestSrcDir, "sample-crd.yaml"), fileio.NonExecutablePerms) - require.NoError(t, err) - - manifestURLs := []string{"https://k8s.io/examples/application/nginx-app.yaml"} - - // Test - containerImages, err := ManifestImages(manifestURLs, manifestSrcDir) - - // Verify - require.NoError(t, err) - assert.ElementsMatch(t, expectedContainerImages, containerImages) + }, containerImages) } diff --git a/pkg/registry/manifests_test.go b/pkg/registry/manifests_test.go index daefc062..4fa4e364 100644 --- a/pkg/registry/manifests_test.go +++ b/pkg/registry/manifests_test.go @@ -10,41 +10,32 @@ import ( "github.com/suse-edge/edge-image-builder/pkg/fileio" ) -const ( - localManifestsSrcDir = "local-manifests" -) - func TestReadManifest(t *testing.T) { // Setup manifestPath := filepath.Join("testdata", "sample-crd.yaml") // Test - manifests, err := readManifest(manifestPath) + resources, err := readManifest(manifestPath) // Verify require.NoError(t, err) + require.Len(t, resources, 2) - // First manifest in sample-crd.yaml - data := manifests[0] - apiVersion, ok := data["apiVersion"].(string) + // First resource in sample-crd.yaml + r := resources[0] + apiVersion, ok := r["apiVersion"].(string) require.True(t, ok) assert.Equal(t, "custom.example.com/v1", apiVersion) - // Second manifest in sample-crd.yaml - data = manifests[1] - apiVersion, ok = data["apiVersion"].(string) + // Second resource in sample-crd.yaml + r = resources[1] + apiVersion, ok = r["apiVersion"].(string) require.True(t, ok) assert.Equal(t, "apps/v1", apiVersion) } func TestReadManifest_NoManifest(t *testing.T) { - // Setup - manifestPath := filepath.Join() - - // Test - _, err := readManifest(manifestPath) - - // Verify + _, err := readManifest("") require.ErrorContains(t, err, "no such file or directory") } @@ -56,7 +47,7 @@ func TestReadManifest_InvalidManifest(t *testing.T) { _, err := readManifest(manifestPath) // Verify - require.ErrorContains(t, err, "error unmarshalling manifest yaml") + require.ErrorContains(t, err, "unmarshalling manifest") } func TestReadManifest_EmptyManifest(t *testing.T) { @@ -70,18 +61,16 @@ func TestReadManifest_EmptyManifest(t *testing.T) { assert.Error(t, err, "invalid manifest") } -func TestStoreManifestImages(t *testing.T) { +func TestExtractManifestImages(t *testing.T) { // Setup var extractedImagesSet = make(map[string]bool) manifestPath := filepath.Join("testdata", "sample-crd.yaml") manifestData, err := readManifest(manifestPath) require.NoError(t, err) - expectedImages := []string{"nginx:latest", "node:14", "custom-api:1.2.3", "mysql:5.7", "redis:6.0", "nginx:1.14.2"} - // Test for _, manifest := range manifestData { - storeManifestImages(manifest, extractedImagesSet) + extractManifestImages(manifest, extractedImagesSet) } allImages := make([]string, 0, len(extractedImagesSet)) for uniqueImage := range extractedImagesSet { @@ -89,10 +78,11 @@ func TestStoreManifestImages(t *testing.T) { } // Verify + expectedImages := []string{"nginx:latest", "node:14", "custom-api:1.2.3", "mysql:5.7", "redis:6.0", "nginx:1.14.2"} assert.ElementsMatch(t, expectedImages, allImages) } -func TestStoreManifestImages_InvalidKinds(t *testing.T) { +func TestExtractManifestImages_InvalidKinds(t *testing.T) { // Setup var extractedImagesSet = make(map[string]bool) manifestData := map[string]any{ @@ -109,151 +99,44 @@ func TestStoreManifestImages_InvalidKinds(t *testing.T) { } // Test - storeManifestImages(manifestData, extractedImagesSet) + extractManifestImages(manifestData, extractedImagesSet) // Verify assert.Equal(t, map[string]bool{}, extractedImagesSet) } -func TestStoreManifestImages_EmptyManifest(t *testing.T) { +func TestExtractManifestImages_EmptyManifest(t *testing.T) { // Setup var extractedImagesSet = make(map[string]bool) var manifestData map[string]any // Test - storeManifestImages(manifestData, extractedImagesSet) + extractManifestImages(manifestData, extractedImagesSet) // Verify assert.Equal(t, map[string]bool{}, extractedImagesSet) } -func TestGetManifestPaths(t *testing.T) { - // Setup - manifestSrcDir := "testdata" - expectedPaths := []string{"testdata/empty-crd.yaml", "testdata/invalid-crd.yml", "testdata/sample-crd.yaml"} - - // Test - manifestPaths, err := getManifestPaths(manifestSrcDir) - - // Verify - require.NoError(t, err) - assert.Equal(t, expectedPaths, manifestPaths) -} - -func TestGetManifestPaths_EmptySrc(t *testing.T) { - // Setup - manifestSrcDir := "" - - // Test - _, err := getManifestPaths(manifestSrcDir) - - // Verify - require.ErrorContains(t, err, "manifest source directory not defined") -} - -func TestGetManifestPaths_InvalidSrc(t *testing.T) { +func TestManifestImages_InvalidLocalManifest(t *testing.T) { // Setup - manifestSrcDir := "not-real" - - // Test - _, err := getManifestPaths(manifestSrcDir) - - // Verify - require.ErrorContains(t, err, "reading manifest source dir 'not-real': open not-real: no such file or directory") -} + localManifestsSrcDir := "local-manifests" -func TestGetManifestPaths_NoManifests(t *testing.T) { - // Setup - require.NoError(t, os.Mkdir("downloaded-manifests", 0o755)) + require.NoError(t, os.Mkdir(localManifestsSrcDir, 0o755)) defer func() { - require.NoError(t, os.RemoveAll("downloaded-manifests")) + assert.NoError(t, os.RemoveAll(localManifestsSrcDir)) }() - // Test - manifestPaths, err := getManifestPaths("downloaded-manifests") - - // Verify - require.NoError(t, err) - assert.Nil(t, manifestPaths) -} - -func TestManifestImages_InvalidURL(t *testing.T) { - // Setup - require.NoError(t, os.Mkdir("downloaded-manifests", 0o755)) - defer func() { - require.NoError(t, os.RemoveAll("downloaded-manifests")) - }() + sourceManifest := filepath.Join("testdata", "invalid-crd.yml") + destinationManifest := filepath.Join(localManifestsSrcDir, "invalid-crd.yml") + require.NoError(t, fileio.CopyFile(sourceManifest, destinationManifest, fileio.NonExecutablePerms)) - manifestURLs := []string{ - "k8s.io/examples/application/nginx-app.yaml", + registry := Registry{ + manifestsDir: localManifestsSrcDir, } // Test - _, err := ManifestImages(manifestURLs, "") - - // Verify - require.ErrorContains(t, err, "downloading manifests: downloading manifest 'k8s.io/examples/application/nginx-app.yaml': executing request: Get \"k8s.io/examples/application/nginx-app.yaml\": unsupported protocol scheme \"\"") -} - -func TestManifestImages_LocalManifestDirNotDefined(t *testing.T) { - // Test - containerImages, err := ManifestImages(nil, "") - - // Verify - require.NoError(t, err) - assert.Empty(t, containerImages) -} - -func TestManifestImages_InvalidLocalManifestsDir(t *testing.T) { - // Setup - localManifestsDir := "does-not-exist" - - // Test - _, err := ManifestImages(nil, localManifestsDir) - - // Verify - require.ErrorContains(t, err, "getting local manifest paths: reading manifest source dir 'does-not-exist': open does-not-exist: no such file or directory") -} - -func TestDownloadManifests_NoManifest(t *testing.T) { - // Setup - manifestDownloadDest := "" - - // Test - manifestPaths, err := DownloadManifests(nil, manifestDownloadDest) - - // Verify - require.NoError(t, err) - assert.Equal(t, 0, len(manifestPaths)) -} - -func TestDownloadManifests_InvalidURL(t *testing.T) { - // Setup - manifestURLs := []string{"k8s.io/examples/application/nginx-app.yaml"} - manifestDownloadDest := "" - - // Test - manifestPaths, err := DownloadManifests(manifestURLs, manifestDownloadDest) - - // Verify - require.ErrorContains(t, err, "downloading manifest 'k8s.io/examples/application/nginx-app.yaml': executing request: Get \"k8s.io/examples/application/nginx-app.yaml\": unsupported protocol scheme \"") - assert.Equal(t, 0, len(manifestPaths)) -} - -func TestManifestImages_InvalidLocalManifest(t *testing.T) { - // Setup - require.NoError(t, os.Mkdir(localManifestsSrcDir, 0o755)) - defer func() { - require.NoError(t, os.RemoveAll(localManifestsSrcDir)) - }() - - localSampleManifestPath := filepath.Join("testdata", "invalid-crd.yml") - err := fileio.CopyFile(localSampleManifestPath, filepath.Join(localManifestsSrcDir, "invalid-crd.yml"), fileio.NonExecutablePerms) - require.NoError(t, err) - - // Test - _, err = ManifestImages(nil, localManifestsSrcDir) + _, err := registry.manifestImages() // Verify - require.ErrorContains(t, err, "reading manifest: error unmarshalling manifest yaml") + require.ErrorContains(t, err, "reading manifest 'local-manifests/invalid-crd.yml': unmarshalling manifest") } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go new file mode 100644 index 00000000..9753f439 --- /dev/null +++ b/pkg/registry/registry.go @@ -0,0 +1,219 @@ +package registry + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/schollz/progressbar/v3" + "github.com/suse-edge/edge-image-builder/pkg/fileio" + "github.com/suse-edge/edge-image-builder/pkg/http" + "github.com/suse-edge/edge-image-builder/pkg/image" + "go.uber.org/zap" +) + +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) +} + +type helmChart struct { + image.HelmChart + localPath string + repositoryURL string +} + +type Registry struct { + embeddedImages []image.ContainerImage + manifestsDir string + helmClient helmClient + helmCharts []*helmChart + helmValuesDir string + kubeVersion string +} + +func New(ctx *image.Context, localManifestsDir string, helmClient helmClient, helmValuesDir string) (*Registry, error) { + manifestsDir, err := storeManifests(ctx, localManifestsDir) + if err != nil { + return nil, fmt.Errorf("storing manifests: %w", err) + } + + charts, err := storeHelmCharts(ctx, helmClient) + if err != nil { + return nil, fmt.Errorf("storing helm charts: %w", err) + } + + return &Registry{ + embeddedImages: ctx.ImageDefinition.EmbeddedArtifactRegistry.ContainerImages, + manifestsDir: manifestsDir, + helmClient: helmClient, + helmCharts: charts, + helmValuesDir: helmValuesDir, + kubeVersion: ctx.ImageDefinition.Kubernetes.Version, + }, nil +} + +func (r *Registry) ManifestsPath() string { + return r.manifestsDir +} + +func storeManifests(ctx *image.Context, localManifestsDir string) (string, error) { + const manifestsDir = "manifests" + + var manifestsPathPopulated bool + + manifestsDestDir := filepath.Join(ctx.BuildDir, manifestsDir) + + manifestURLs := ctx.ImageDefinition.Kubernetes.Manifests.URLs + if len(manifestURLs) != 0 { + if err := os.MkdirAll(manifestsDestDir, os.ModePerm); err != nil { + return "", fmt.Errorf("creating manifests dir: %w", err) + } + + for index, manifestURL := range manifestURLs { + filePath := filepath.Join(manifestsDestDir, fmt.Sprintf("dl-manifest-%d.yaml", index+1)) + + if err := http.DownloadFile(context.Background(), manifestURL, filePath, nil); err != nil { + return "", fmt.Errorf("downloading manifest '%s': %w", manifestURL, err) + } + } + + manifestsPathPopulated = true + } + + if _, err := os.Stat(localManifestsDir); err == nil { + if err = fileio.CopyFiles(localManifestsDir, manifestsDestDir, "", false); err != nil { + return "", fmt.Errorf("copying manifests: %w", err) + } + + manifestsPathPopulated = true + } else if !errors.Is(err, fs.ErrNotExist) { + zap.S().Warnf("Searching for local manifests failed: %v", err) + } + + if !manifestsPathPopulated { + return "", nil + } + + return manifestsDestDir, nil +} + +func storeHelmCharts(ctx *image.Context, helmClient helmClient) ([]*helmChart, error) { + helm := &ctx.ImageDefinition.Kubernetes.Helm + + if len(helm.Charts) == 0 { + return nil, nil + } + + bar := progressbar.Default(int64(len(helm.Charts)), "Pulling selected Helm charts...") + + helmDir := filepath.Join(ctx.BuildDir, "helm") + if err := os.MkdirAll(helmDir, os.ModePerm); err != nil { + return nil, fmt.Errorf("creating helm directory: %w", err) + } + + chartRepositories := mapChartsToRepos(helm) + + var charts []*helmChart + + for _, chart := range helm.Charts { + c := chart + repository, ok := chartRepositories[c.RepositoryName] + if !ok { + return nil, fmt.Errorf("repository not found for chart %s", c.Name) + } + + localPath, err := downloadChart(helmClient, &c, repository, helmDir) + if err != nil { + return nil, fmt.Errorf("downloading chart: %w", err) + } + + charts = append(charts, &helmChart{ + HelmChart: c, + localPath: localPath, + repositoryURL: repository.URL, + }) + + _ = bar.Add(1) + } + + return charts, nil +} + +func mapChartsToRepos(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 +} + +func downloadChart(helmClient helmClient, chart *image.HelmChart, repo *image.HelmRepository, destDir string) (string, error) { + if strings.HasPrefix(repo.URL, "http") { + if err := helmClient.AddRepo(repo); err != nil { + return "", fmt.Errorf("adding repo: %w", err) + } + } else if repo.Authentication.Username != "" && repo.Authentication.Password != "" { + if err := helmClient.RegistryLogin(repo); err != nil { + return "", fmt.Errorf("logging into registry: %w", err) + } + } + + chartPath, err := helmClient.Pull(chart.Name, repo, chart.Version, destDir) + if err != nil { + return "", fmt.Errorf("pulling chart: %w", err) + } + + return chartPath, nil +} + +func (r *Registry) ContainerImages() ([]string, error) { + manifestImages, err := r.manifestImages() + if err != nil { + return nil, fmt.Errorf("getting container images from manifests: %w", err) + } + + chartImages, err := r.helmChartImages() + if err != nil { + return nil, fmt.Errorf("getting container images from helm charts: %w", err) + } + + return deduplicateContainerImages(r.embeddedImages, manifestImages, chartImages), nil +} + +func deduplicateContainerImages(embeddedImages []image.ContainerImage, manifestImages, chartImages []string) []string { + imageSet := map[string]bool{} + + for _, img := range embeddedImages { + imageSet[img.Name] = true + } + + for _, img := range manifestImages { + imageSet[img] = true + } + + for _, img := range chartImages { + imageSet[img] = true + } + + var images []string + + for img := range imageSet { + images = append(images, img) + } + + return images +} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go new file mode 100644 index 00000000..ab1beb0e --- /dev/null +++ b/pkg/registry/registry_test.go @@ -0,0 +1,128 @@ +package registry + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/suse-edge/edge-image-builder/pkg/fileio" + "github.com/suse-edge/edge-image-builder/pkg/image" +) + +func TestRegistry_New_InvalidManifestURL(t *testing.T) { + buildDir := filepath.Join(os.TempDir(), "_build-registry-error") + require.NoError(t, os.MkdirAll(buildDir, os.ModePerm)) + defer func() { + assert.NoError(t, os.RemoveAll(buildDir)) + }() + + ctx := &image.Context{ + BuildDir: buildDir, + ImageDefinition: &image.Definition{ + Kubernetes: image.Kubernetes{ + Manifests: image.Manifests{ + URLs: []string{"k8s.io/examples/application/nginx-app.yaml"}}, + }, + }, + } + + _, err := New(ctx, "", nil, "") + require.Error(t, err) + + assert.ErrorContains(t, err, "downloading manifest 'k8s.io/examples/application/nginx-app.yaml'") + assert.ErrorContains(t, err, "unsupported protocol scheme") +} + +func TestRegistry_ContainerImages(t *testing.T) { + manifestsDir := filepath.Join(os.TempDir(), "_manifests") + require.NoError(t, os.MkdirAll(manifestsDir, os.ModePerm)) + defer func() { + assert.NoError(t, os.RemoveAll(manifestsDir)) + }() + + require.NoError(t, fileio.CopyFile("testdata/sample-crd.yaml", filepath.Join(manifestsDir, "sample-crd.yaml"), fileio.NonExecutablePerms)) + + registry := Registry{ + embeddedImages: []image.ContainerImage{ + { + Name: "hello-world", + }, + { + Name: "nginx:latest", + }, + }, + manifestsDir: manifestsDir, + helmCharts: []*helmChart{ + { + HelmChart: image.HelmChart{ + Name: "apache", + }, + }, + }, + helmClient: mockHelmClient{ + templateFunc: func(chart, repository, version, valuesFilePath, kubeVersion, targetNamespace string) ([]map[string]any, error) { + return []map[string]any{ + { + "kind": "Deployment", + "image": "httpd", + }, + { + "kind": "Service", + }, + }, nil + }, + }, + } + + images, err := registry.ContainerImages() + require.NoError(t, err) + + assert.ElementsMatch(t, images, []string{ + // embedded images + "hello-world", + "nginx:latest", + // manifest images + "node:14", + "custom-api:1.2.3", + "mysql:5.7", + "redis:6.0", + "nginx:1.14.2", + // chart images + "httpd", + }) +} + +func TestDeduplicateContainerImages(t *testing.T) { + embeddedImages := []image.ContainerImage{ + { + Name: "hello-world:latest", + }, + { + Name: "embedded-image:1.0.0", + }, + } + + manifestImages := []string{ + "hello-world:latest", + "manifest-image:1.0.0", + } + + chartImages := []string{ + "hello-world:latest", + "chart-image:1.0.0", + "chart-image:1.0.0", + "chart-image:1.0.1", + "chart-image:2.0.0", + } + + assert.ElementsMatch(t, []string{ + "hello-world:latest", + "embedded-image:1.0.0", + "manifest-image:1.0.0", + "chart-image:1.0.0", + "chart-image:1.0.1", + "chart-image:2.0.0", + }, deduplicateContainerImages(embeddedImages, manifestImages, chartImages)) +}