From 5efcb35a2378483a6c46f8eef0c2748f3a7a2dd6 Mon Sep 17 00:00:00 2001 From: Vibhav Bobade Date: Wed, 6 Mar 2024 23:21:57 +0530 Subject: [PATCH] feat: add `--why` flag for `zarf dev find-images` (#2309) ## Description Add a `--why` flag for `zarf dev find-images` ## Related Issue Fixes #2272 ## 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 - [x] Test, docs, adr added or updated as needed - [x] [Contributor Guide Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow) followed --------- Signed-off-by: Vibhav Bobade Co-authored-by: Wayne Starr Co-authored-by: Lucas Rodriguez Co-authored-by: Lucas Rodriguez --- .../100-cli-commands/zarf_dev_find-images.md | 1 + src/cmd/dev.go | 2 + src/config/lang/english.go | 1 + src/pkg/packager/prepare.go | 49 +++++ src/test/e2e/13_find_images_test.go | 50 +++++ .../dos-games-find-images-expected.txt | 7 + .../helm-charts-find-images-why-expected.txt | 187 ++++++++++++++++++ .../manifests-find-images-why-expected.txt | 24 +++ src/types/runtime.go | 1 + 9 files changed, 322 insertions(+) create mode 100644 src/test/e2e/13_find_images_test.go create mode 100644 src/test/packages/13-find-images/dos-games-find-images-expected.txt create mode 100644 src/test/packages/13-find-images/helm-charts-find-images-why-expected.txt create mode 100644 src/test/packages/13-find-images/manifests-find-images-why-expected.txt diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_dev_find-images.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_dev_find-images.md index 01e43dfbcb..fdfb58a0f2 100644 --- a/docs/2-the-zarf-cli/100-cli-commands/zarf_dev_find-images.md +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_dev_find-images.md @@ -20,6 +20,7 @@ zarf dev find-images [ PACKAGE ] [flags] --kube-version string Override the default helm template KubeVersion when performing a package chart template -p, --repo-chart-path string If git repos hold helm charts, often found with gitops tools, specify the chart path, e.g. "/" or "/chart" --set stringToString Specify package variables to set on the command line (KEY=value). Note, if using a config file, this will be set by [package.create.set]. (default []) + --why string Find the location of the image given as an argument and print it to the console. ``` ## Options inherited from parent commands diff --git a/src/cmd/dev.go b/src/cmd/dev.go index bccac8b781..a9f20b1251 100644 --- a/src/cmd/dev.go +++ b/src/cmd/dev.go @@ -267,6 +267,8 @@ func init() { devFindImagesCmd.Flags().StringToStringVar(&pkgConfig.CreateOpts.SetVariables, "set", v.GetStringMapString(common.VPkgCreateSet), lang.CmdDevFlagSet) // allow for the override of the default helm KubeVersion devFindImagesCmd.Flags().StringVar(&pkgConfig.FindImagesOpts.KubeVersionOverride, "kube-version", "", lang.CmdDevFlagKubeVersion) + // check which manifests are using this particular image + devFindImagesCmd.Flags().StringVar(&pkgConfig.FindImagesOpts.Why, "why", "", lang.CmdDevFlagFindImagesWhy) devLintCmd.Flags().StringToStringVar(&pkgConfig.CreateOpts.SetVariables, "set", v.GetStringMapString(common.VPkgCreateSet), lang.CmdPackageCreateFlagSet) devLintCmd.Flags().StringVarP(&pkgConfig.CreateOpts.Flavor, "flavor", "f", v.GetString(common.VPkgCreateFlavor), lang.CmdPackageCreateFlagFlavor) diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 76487fb44e..e4ed8c6d64 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -384,6 +384,7 @@ $ zarf package pull oci://ghcr.io/defenseunicorns/packages/dos-games:1.0.0 -a sk CmdDevFlagRepoChartPath = `If git repos hold helm charts, often found with gitops tools, specify the chart path, e.g. "/" or "/chart"` CmdDevFlagGitAccount = "User or organization name for the git account that the repos are created under." CmdDevFlagKubeVersion = "Override the default helm template KubeVersion when performing a package chart template" + CmdDevFlagFindImagesWhy = "Find the location of the image given as an argument and print it to the console." CmdDevLintShort = "Lints the given package for valid schema and recommended practices" CmdDevLintLong = "Verifies the package schema, checks if any variables won't be evaluated, and checks for unpinned images/repos/files" diff --git a/src/pkg/packager/prepare.go b/src/pkg/packager/prepare.go index 1af0ca01e9..7a1df0b982 100644 --- a/src/pkg/packager/prepare.go +++ b/src/pkg/packager/prepare.go @@ -6,6 +6,7 @@ package packager import ( "fmt" + "github.com/goccy/go-yaml" "os" "path/filepath" "regexp" @@ -37,10 +38,12 @@ type imageMap map[string]bool func (p *Packager) FindImages() (imgMap map[string][]string, err error) { repoHelmChartPath := p.cfg.FindImagesOpts.RepoHelmChartPath kubeVersionOverride := p.cfg.FindImagesOpts.KubeVersionOverride + whyImage := p.cfg.FindImagesOpts.Why imagesMap := make(map[string][]string) erroredCharts := []string{} erroredCosignLookups := []string{} + whyResources := []string{} cwd, err := os.Getwd() if err != nil { @@ -158,6 +161,15 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) { for _, image := range annotatedImages { matchedImages[image] = true } + + // Check if the --why flag is set + if whyImage != "" { + whyResourcesChart, err := findWhyResources(yamls, whyImage, component.Name, chart.Name, true) + if err != nil { + message.WarnErrf(err, "Error finding why resources for chart %s: %s", chart.Name, err.Error()) + } + whyResources = append(whyResources, whyResourcesChart...) + } } for _, manifest := range component.Manifests { @@ -193,6 +205,15 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) { message.Debugf("%s", contentString) yamls, _ := utils.SplitYAML(contents) resources = append(resources, yamls...) + + // Check if the --why flag is set and if it is process the manifests + if whyImage != "" { + whyResourcesManifest, err := findWhyResources(yamls, whyImage, component.Name, manifest.Name, false) + if err != nil { + message.WarnErrf(err, "Error finding why resources for manifest %s: %s", manifest.Name, err.Error()) + } + whyResources = append(whyResources, whyResourcesManifest...) + } } } @@ -268,6 +289,13 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) { } } + if whyImage != "" { + if len(whyResources) == 0 { + message.Warnf("image %q not found in any charts or manifests", whyImage) + } + return nil, nil + } + fmt.Println(componentDefinition) // Return to the original working directory @@ -356,6 +384,27 @@ func (p *Packager) processUnstructuredImages(resource *unstructured.Unstructured return matchedImages, maybeImages, nil } +func findWhyResources(resources []*unstructured.Unstructured, whyImage, componentName, resourceName string, isChart bool) ([]string, error) { + foundWhyResources := []string{} + for _, resource := range resources { + bytes, err := yaml.Marshal(resource.Object) + if err != nil { + return nil, err + } + yaml := string(bytes) + resourceTypeKey := "manifest" + if isChart { + resourceTypeKey = "chart" + } + + if strings.Contains(yaml, whyImage) { + fmt.Printf("component: %s\n%s: %s\nresource:\n\n%s\n", componentName, resourceTypeKey, resourceName, yaml) + foundWhyResources = append(foundWhyResources, resourceName) + } + } + return foundWhyResources, nil +} + // BuildImageMap looks for init container, ephemeral and regular container images. func buildImageMap(images imageMap, pod corev1.PodSpec) imageMap { for _, container := range pod.InitContainers { diff --git a/src/test/e2e/13_find_images_test.go b/src/test/e2e/13_find_images_test.go new file mode 100644 index 0000000000..c37e1967c8 --- /dev/null +++ b/src/test/e2e/13_find_images_test.go @@ -0,0 +1,50 @@ +package test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFindImages(t *testing.T) { + t.Log("E2E: Find Images") + + t.Run("zarf test find images success", func(t *testing.T) { + t.Log("E2E: Test Find Images") + + testPackagePath := filepath.Join("examples", "dos-games") + expectedOutput, err := os.ReadFile("src/test/packages/13-find-images/dos-games-find-images-expected.txt") + require.NoError(t, err) + + stdout, _, err := e2e.Zarf("dev", "find-images", testPackagePath) + require.NoError(t, err) + require.Contains(t, stdout, string(expectedOutput)) + }) + + t.Run("zarf test find images --why w/ helm chart success", func(t *testing.T) { + t.Log("E2E: Test Find Images against a helm chart with why flag") + + testPackagePath := filepath.Join("examples", "wordpress") + expectedOutput, err := os.ReadFile("src/test/packages/13-find-images/helm-charts-find-images-why-expected.txt") + require.NoError(t, err) + + stdout, _, err := e2e.Zarf("dev", "find-images", testPackagePath, "--why", "docker.io/bitnami/apache-exporter:0.13.3-debian-11-r2") + require.NoError(t, err) + require.Contains(t, stdout, string(expectedOutput)) + }) + + t.Run("zarf test find images --why w/ manifests success", func(t *testing.T) { + t.Log("E2E: Test Find Images against manifests with why flag") + + testPackagePath := filepath.Join("examples", "manifests") + expectedOutput, err := os.ReadFile("src/test/packages/13-find-images/manifests-find-images-why-expected.txt") + require.NoError(t, err) + + stdout, _, err := e2e.Zarf("dev", "find-images", testPackagePath, "--why", "httpd:alpine3.18") + require.NoError(t, err) + require.Contains(t, stdout, string(expectedOutput)) + }) + +} diff --git a/src/test/packages/13-find-images/dos-games-find-images-expected.txt b/src/test/packages/13-find-images/dos-games-find-images-expected.txt new file mode 100644 index 0000000000..8d661f4e52 --- /dev/null +++ b/src/test/packages/13-find-images/dos-games-find-images-expected.txt @@ -0,0 +1,7 @@ +components: + + - name: baseline + images: + - defenseunicorns/zarf-game:multi-tile-dark + # Cosign artifacts for images - dos-games - baseline + - index.docker.io/defenseunicorns/zarf-game:sha256-0b694ca1c33afae97b7471488e07968599f1d2470c629f76af67145ca64428af.sig diff --git a/src/test/packages/13-find-images/helm-charts-find-images-why-expected.txt b/src/test/packages/13-find-images/helm-charts-find-images-why-expected.txt new file mode 100644 index 0000000000..132a02e855 --- /dev/null +++ b/src/test/packages/13-find-images/helm-charts-find-images-why-expected.txt @@ -0,0 +1,187 @@ +component: wordpress +chart: wordpress +resource: + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: wordpress + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: wordpress + helm.sh/chart: wordpress-16.0.4 + name: wordpress + namespace: wordpress +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: wordpress + app.kubernetes.io/name: wordpress + strategy: + type: RollingUpdate + template: + metadata: + annotations: null + labels: + app.kubernetes.io/instance: wordpress + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: wordpress + helm.sh/chart: wordpress-16.0.4 + spec: + affinity: + nodeAffinity: null + podAffinity: null + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/instance: wordpress + app.kubernetes.io/name: wordpress + topologyKey: kubernetes.io/hostname + weight: 1 + containers: + - env: + - name: BITNAMI_DEBUG + value: "false" + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + - name: MARIADB_HOST + value: wordpress-mariadb + - name: MARIADB_PORT_NUMBER + value: "3306" + - name: WORDPRESS_DATABASE_NAME + value: bitnami_wordpress + - name: WORDPRESS_DATABASE_USER + value: bn_wordpress + - name: WORDPRESS_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + key: mariadb-password + name: wordpress-mariadb + - name: WORDPRESS_USERNAME + value: null + - name: WORDPRESS_PASSWORD + valueFrom: + secretKeyRef: + key: wordpress-password + name: wordpress + - name: WORDPRESS_EMAIL + value: null + - name: WORDPRESS_FIRST_NAME + value: null + - name: WORDPRESS_LAST_NAME + value: null + - name: WORDPRESS_HTACCESS_OVERRIDE_NONE + value: "no" + - name: WORDPRESS_ENABLE_HTACCESS_PERSISTENCE + value: "no" + - name: WORDPRESS_BLOG_NAME + value: null + - name: WORDPRESS_SKIP_BOOTSTRAP + value: "no" + - name: WORDPRESS_TABLE_PREFIX + value: wp_ + - name: WORDPRESS_SCHEME + value: http + - name: WORDPRESS_EXTRA_WP_CONFIG_CONTENT + value: "" + - name: WORDPRESS_PLUGINS + value: none + - name: APACHE_HTTP_PORT_NUMBER + value: "8080" + - name: APACHE_HTTPS_PORT_NUMBER + value: "8443" + envFrom: null + image: docker.io/bitnami/wordpress:6.2.0-debian-11-r18 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 6 + httpGet: + httpHeaders: [] + path: /wp-admin/install.php + port: http + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: wordpress + ports: + - containerPort: 8080 + name: http + - containerPort: 8443 + name: https + readinessProbe: + failureThreshold: 6 + httpGet: + httpHeaders: [] + path: /wp-login.php + port: http + scheme: HTTP + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: {} + requests: + cpu: 300m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 1001 + volumeMounts: + - mountPath: /bitnami/wordpress + name: wordpress-data + subPath: wordpress + - command: + - /bin/apache_exporter + - --scrape_uri + - http://status.localhost:8080/server-status/?auto + image: docker.io/bitnami/apache-exporter:0.13.3-debian-11-r2 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 15 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: metrics + ports: + - containerPort: 9117 + name: metrics + readinessProbe: + failureThreshold: 3 + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 3 + resources: + limits: {} + requests: {} + hostAliases: + - hostnames: + - status.localhost + ip: 127.0.0.1 + securityContext: + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault + serviceAccountName: default + volumes: + - name: wordpress-data + persistentVolumeClaim: + claimName: wordpress + diff --git a/src/test/packages/13-find-images/manifests-find-images-why-expected.txt b/src/test/packages/13-find-images/manifests-find-images-why-expected.txt new file mode 100644 index 0000000000..d3ed40d8ac --- /dev/null +++ b/src/test/packages/13-find-images/manifests-find-images-why-expected.txt @@ -0,0 +1,24 @@ +component: httpd-local +manifest: simple-httpd-deployment +resource: + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: httpd-deployment +spec: + replicas: 2 + selector: + matchLabels: + app: httpd + template: + metadata: + labels: + app: httpd + spec: + containers: + - image: httpd:alpine3.18 + name: httpd + ports: + - containerPort: 80 + diff --git a/src/types/runtime.go b/src/types/runtime.go index 3878bb0aef..af4fd8ae55 100644 --- a/src/types/runtime.go +++ b/src/types/runtime.go @@ -55,6 +55,7 @@ type ZarfInspectOptions struct { type ZarfFindImagesOptions struct { RepoHelmChartPath string `json:"repoHelmChartPath" jsonschema:"description=Path to the helm chart directory"` KubeVersionOverride string `json:"kubeVersionOverride" jsonschema:"description=Kubernetes version to use for the helm chart"` + Why string `json:"why" jsonschema:"description=Find the location of the image given as an argument and print it to the console."` } // ZarfDeployOptions tracks the user-defined preferences during a package deploy.