Skip to content

Commit

Permalink
337 image annotation discovery helm charts in find-images (#1708)
Browse files Browse the repository at this point in the history
## Description

Adds image annotation discovery in find-images

## Related Issue

Fixes #337 

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [ ] Test, docs, adr added or updated as needed
- [X] [Contributor Guide
Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow)
followed
  • Loading branch information
Racer159 authored May 17, 2023
1 parent 7d496fd commit 288debf
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 80 deletions.
52 changes: 50 additions & 2 deletions src/extensions/bigbang/bigbang.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ package bigbang

import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/Masterminds/semver/v3"
"github.com/defenseunicorns/zarf/src/internal/packager/helm"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/types"
"github.com/defenseunicorns/zarf/src/types/extensions"
fluxHelmCtrl "github.com/fluxcd/helm-controller/api/v2beta1"
fluxSrcCtrl "github.com/fluxcd/source-controller/api/v1beta2"
"helm.sh/helm/v3/pkg/chartutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
Expand Down Expand Up @@ -103,7 +108,7 @@ func Run(YOLO bool, tmpPaths types.ComponentPaths, c types.ZarfComponent) (types

// Template the chart so we can see what GitRepositories are being referenced in the
// manifests created with the provided Helm.
template, err := helmCfg.TemplateChart()
template, _, err := helmCfg.TemplateChart()
if err != nil {
return c, fmt.Errorf("unable to template Big Bang Chart: %w", err)
}
Expand Down Expand Up @@ -211,7 +216,7 @@ func Run(YOLO bool, tmpPaths types.ComponentPaths, c types.ZarfComponent) (types
gitRepo := gitRepos[hr.NamespacedSource]
values := hrValues[namespacedName]

images, err := helm.FindImagesForChartRepo(gitRepo, "chart", values)
images, err := findImagesforBBChartRepo(gitRepo, values)
if err != nil {
return c, fmt.Errorf("unable to find images for chart repo: %w", err)
}
Expand Down Expand Up @@ -434,3 +439,46 @@ func addBigBangManifests(YOLO bool, manifestDir string, cfg *extensions.BigBang)

return manifest, nil
}

// findImagesforBBChartRepo finds and returns the images for the Big Bang chart repo
func findImagesforBBChartRepo(repo string, values chartutil.Values) (images []string, err error) {
matches := strings.Split(repo, "@")
if len(matches) < 2 {
return images, fmt.Errorf("cannot convert git repo %s to helm chart without a version tag", repo)
}

spinner := message.NewProgressSpinner("Discovering images in %s", repo)
defer spinner.Stop()

chart := types.ZarfChart{
Name: repo,
URL: matches[0],
Version: matches[1],
GitPath: "chart",
}

helmCfg := helm.Helm{
Chart: chart,
Cfg: &types.PackagerConfig{
State: types.ZarfState{},
},
}

gitPath, err := helmCfg.DownloadChartFromGitToTemp(spinner)
if err != nil {
return images, err
}
defer os.RemoveAll(gitPath)

// Set the directory for the chart
chartPath := filepath.Join(gitPath, helmCfg.Chart.GitPath)

images, err = helm.FindAnnotatedImagesForChart(chartPath, values)
if err != nil {
return images, err
}

spinner.Success()

return images, err
}
16 changes: 8 additions & 8 deletions src/internal/packager/helm/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (h *Helm) InstallOrUpgradeChart() (types.ConnectStrings, string, error) {
}

// TemplateChart generates a helm template from a given chart.
func (h *Helm) TemplateChart() (string, error) {
func (h *Helm) TemplateChart() (string, chartutil.Values, error) {
message.Debugf("helm.TemplateChart()")
spinner := message.NewProgressSpinner("Templating helm chart %s", h.Chart.Name)
defer spinner.Stop()
Expand All @@ -148,7 +148,7 @@ func (h *Helm) TemplateChart() (string, error) {

// Setup K8s connection.
if err != nil {
return "", fmt.Errorf("unable to initialize the K8s client: %w", err)
return "", nil, fmt.Errorf("unable to initialize the K8s client: %w", err)
}

// Bind the helm action.
Expand All @@ -164,7 +164,7 @@ func (h *Helm) TemplateChart() (string, error) {
if h.KubeVersion != "" {
parsedKubeVersion, err := chartutil.ParseKubeVersion(h.KubeVersion)
if err != nil {
return "", fmt.Errorf("invalid kube version '%s': %s", h.KubeVersion, err)
return "", nil, fmt.Errorf("invalid kube version '%s': %s", h.KubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}
Expand All @@ -180,13 +180,13 @@ func (h *Helm) TemplateChart() (string, error) {

loadedChart, chartValues, err := h.loadChartData()
if err != nil {
return "", fmt.Errorf("unable to load chart data: %w", err)
return "", nil, fmt.Errorf("unable to load chart data: %w", err)
}

// Perform the loadedChart installation.
templatedChart, err := client.Run(loadedChart, chartValues)
if err != nil {
return "", fmt.Errorf("error generating helm chart template: %w", err)
return "", nil, fmt.Errorf("error generating helm chart template: %w", err)
}

manifest := templatedChart.Manifest
Expand All @@ -197,7 +197,7 @@ func (h *Helm) TemplateChart() (string, error) {

spinner.Success()

return manifest, nil
return manifest, chartValues, nil
}

// GenerateChart generates a helm chart for a given Zarf manifest.
Expand Down Expand Up @@ -347,11 +347,11 @@ func (h *Helm) uninstallChart(name string) (*release.UninstallReleaseResponse, e
return client.Run(name)
}

func (h *Helm) loadChartData() (*chart.Chart, map[string]any, error) {
func (h *Helm) loadChartData() (*chart.Chart, chartutil.Values, error) {
message.Debugf("helm.loadChartData()")
var (
loadedChart *chart.Chart
chartValues map[string]any
chartValues chartutil.Values
err error
)

Expand Down
54 changes: 3 additions & 51 deletions src/internal/packager/helm/images.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
package helm

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/types"
"github.com/goccy/go-yaml"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
Expand All @@ -26,50 +20,10 @@ type ChartImages []struct {
Dependency string `yaml:"dependency"`
}

// FindImagesForChartRepo iterates over a Zarf.yaml and attempts to parse any images.
func FindImagesForChartRepo(repo, path string, values chartutil.Values) (images []string, err error) {
matches := strings.Split(repo, "@")
if len(matches) < 2 {
return images, fmt.Errorf("cannot convert git repo %s to helm chart without a version tag", repo)
}

spinner := message.NewProgressSpinner("Discovering images in %s", repo)
defer spinner.Stop()

// Trim the first char to match how the packager expects it, this is messy,need to clean up better
repoHelmChartPath := strings.TrimPrefix(path, "/")

// If a repo helm chart path is specified.
component := types.ZarfComponent{
Charts: []types.ZarfChart{{
Name: repo,
URL: matches[0],
Version: matches[1],
GitPath: repoHelmChartPath,
}},
}

helmCfg := Helm{
Chart: component.Charts[0],
BasePath: path,
Cfg: &types.PackagerConfig{
State: types.ZarfState{},
},
}

// TODO (@runyontr) expand this to work for regular charts for more generic
// capability and pull it out from just being used by Big Bang.
gitPath, err := helmCfg.downloadChartFromGitToTemp(spinner)
if err != nil {
return images, err
}
defer os.RemoveAll(gitPath)

// Set the directory for the chart
chartPath := filepath.Join(gitPath, helmCfg.Chart.GitPath)

// FindAnnotatedImagesForChart attempts to parse any image annotations found in a chart archive or directory.
func FindAnnotatedImagesForChart(chartPath string, values chartutil.Values) (images []string, err error) {
// Load a new chart.
chart, err := loader.LoadDir(chartPath)
chart, err := loader.Load(chartPath)
if err != nil {
return images, err
}
Expand Down Expand Up @@ -98,7 +52,5 @@ func FindImagesForChartRepo(repo, path string, values chartutil.Values) (images
}
}

spinner.Success()

return images, nil
}
5 changes: 3 additions & 2 deletions src/internal/packager/helm/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (h *Helm) PackageChartFromGit(destination string) (string, error) {
client := action.NewPackage()

// Retrieve the repo containing the chart
gitPath, err := h.downloadChartFromGitToTemp(spinner)
gitPath, err := h.DownloadChartFromGitToTemp(spinner)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -148,7 +148,8 @@ func (h *Helm) DownloadPublishedChart(destination string) {
spinner.Success()
}

func (h *Helm) downloadChartFromGitToTemp(spinner *message.Spinner) (string, error) {
// DownloadChartFromGitToTemp downloads a chart from git into a temp directory
func (h *Helm) DownloadChartFromGitToTemp(spinner *message.Spinner) (string, error) {
// Create the Git configuration and download the repo
gitCfg := git.NewWithSpinner(h.Cfg.State.GitServer, spinner)

Expand Down
43 changes: 26 additions & 17 deletions src/pkg/packager/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ func (p *Packager) FindImages(baseDir, repoHelmChartPath string, kubeVersionOver
}
}

// matchedImages holds the collection of images, reset per-component
matchedImages := make(k8s.ImageMap)
maybeImages := make(k8s.ImageMap)

// resources are a slice of generic structs that represent parsed K8s resources
var resources []*unstructured.Unstructured

Expand All @@ -100,7 +104,7 @@ func (p *Packager) FindImages(baseDir, repoHelmChartPath string, kubeVersionOver
return fmt.Errorf("unable to create component paths: %s", err.Error())
}

chartNames := make(map[string]string)
chartOverrides := make(map[string]string)

if len(component.Charts) > 0 {
_ = utils.CreateDirectory(componentPath.Charts, 0700)
Expand All @@ -121,7 +125,7 @@ func (p *Packager) FindImages(baseDir, repoHelmChartPath string, kubeVersionOver
return fmt.Errorf("unable to download chart from git repo (%s): %w", chart.URL, err)
}
// track the actual chart path
chartNames[chart.Name] = path
chartOverrides[chart.Name] = path
} else if len(chart.URL) > 0 {
helmCfg.DownloadPublishedChart(componentPath.Charts)
} else {
Expand All @@ -141,21 +145,14 @@ func (p *Packager) FindImages(baseDir, repoHelmChartPath string, kubeVersionOver
}
}

var override string
var ok bool

if override, ok = chartNames[chart.Name]; ok {
chart.Name = "dummy"
}

// Generate helm templates to pass to gitops engine
helmCfg = helm.Helm{
BasePath: componentPath.Base,
Chart: chart,
ChartLoadOverride: override,
ChartLoadOverride: chartOverrides[chart.Name],
KubeVersion: kubeVersionOverride,
}
template, err := helmCfg.TemplateChart()
template, values, err := helmCfg.TemplateChart()

if err != nil {
message.Errorf(err, "Problem rendering the helm template for %s: %s", chart.URL, err.Error())
Expand All @@ -165,6 +162,22 @@ func (p *Packager) FindImages(baseDir, repoHelmChartPath string, kubeVersionOver
// Break the template into separate resources
yamls, _ := utils.SplitYAML([]byte(template))
resources = append(resources, yamls...)

var chartTarball string
if overridePath, ok := chartOverrides[chart.Name]; ok {
chartTarball = overridePath
} else {
chartTarball = helm.StandardName(componentPath.Charts, helmCfg.Chart) + ".tgz"
}

annotatedImages, err := helm.FindAnnotatedImagesForChart(chartTarball, values)
if err != nil {
message.Errorf(err, "Problem looking for image annotations for %s: %s", chart.URL, err.Error())
continue
}
for _, image := range annotatedImages {
matchedImages[image] = true
}
}
}

Expand Down Expand Up @@ -210,12 +223,8 @@ func (p *Packager) FindImages(baseDir, repoHelmChartPath string, kubeVersionOver
}
}

// matchedImages holds the collection of images, reset per-component
matchedImages := make(k8s.ImageMap)
maybeImages := make(k8s.ImageMap)

for _, resource := range resources {
if matchedImages, maybeImages, err = p.processUnstructured(resource, matchedImages, maybeImages); err != nil {
if matchedImages, maybeImages, err = p.processUnstructuredImages(resource, matchedImages, maybeImages); err != nil {
message.Errorf(err, "Problem processing K8s resource %s", resource.GetName())
}
}
Expand Down Expand Up @@ -262,7 +271,7 @@ func (p *Packager) FindImages(baseDir, repoHelmChartPath string, kubeVersionOver
return nil
}

func (p *Packager) processUnstructured(resource *unstructured.Unstructured, matchedImages, maybeImages k8s.ImageMap) (k8s.ImageMap, k8s.ImageMap, error) {
func (p *Packager) processUnstructuredImages(resource *unstructured.Unstructured, matchedImages, maybeImages k8s.ImageMap) (k8s.ImageMap, k8s.ImageMap, error) {
var imageSanityCheck = regexp.MustCompile(`(?mi)"image":"([^"]+)"`)
var imageFuzzyCheck = regexp.MustCompile(`(?mi)"([a-z0-9\-.\/]+:[\w][\w.\-]{0,127})"`)
var json string
Expand Down
5 changes: 5 additions & 0 deletions src/test/e2e/00_use_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func TestUseCLI(t *testing.T) {
require.NoError(t, err, stdOut, stdErr)
require.Contains(t, stdOut, "quay.io/jetstack/cert-manager-controller:v1.11.1", "The chart image should be found by Zarf")

// Test `zarf prepare find-images` with a chart that uses helm annotations
stdOut, stdErr, err = e2e.ExecZarfCommand("prepare", "find-images", "src/test/test-packages/00-helm-annotations")
require.NoError(t, err, stdOut, stdErr)
require.Contains(t, stdOut, "registry1.dso.mil/ironbank/opensource/kubernetes/kubectl:v1.26.4", "The kubectl image should be found by Zarf")

// Test for expected failure when given a bad component input
_, _, err = e2e.ExecZarfCommand("init", "--confirm", "--components=k3s,foo,logging")
assert.Error(t, err)
Expand Down
14 changes: 14 additions & 0 deletions src/test/test-packages/00-helm-annotations/zarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
kind: ZarfPackageConfig
metadata:
name: loki-repo1
description: Helm chart for Grafana Loki in simple, scalable mode
components:
- name: loki-repo1-component
description: Helm chart for Grafana Loki in simple, scalable mode
required: true
charts:
- name: loki
version: 5.0.0-bb.4
namespace: loki
url: https://repo1.dso.mil/big-bang/product/packages/loki.git
gitPath: chart

0 comments on commit 288debf

Please sign in to comment.