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 63426692ea..cf0e989af2 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 6a666474d7..114a7054d9 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..75c8f89a2d 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,6 +38,7 @@ 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{} @@ -158,6 +160,14 @@ 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 != "" { + _, err := p.findWhyResources(resources, whyImage, component.Name, chart.Name, true) + if err != nil { + message.WarnErrf(err, "Error finding why resources for chart %s: %s", chart.Name, err.Error()) + } + } } for _, manifest := range component.Manifests { @@ -193,6 +203,14 @@ 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 != "" { + _, err := p.findWhyResources(resources, whyImage, component.Name, manifest.Name, false) + if err != nil { + message.WarnErrf(err, "Error finding why resources for manifest %s: %s", manifest.Name, err.Error()) + } + } } } @@ -268,7 +286,9 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) { } } - fmt.Println(componentDefinition) + if whyImage == "" { + fmt.Println(componentDefinition) + } // Return to the original working directory if err := os.Chdir(cwd); err != nil { @@ -356,6 +376,31 @@ func (p *Packager) processUnstructuredImages(resource *unstructured.Unstructured return matchedImages, maybeImages, nil } +func (p *Packager) findWhyResources(resources []*unstructured.Unstructured, whyImage, componentName, resourceName string, isChart bool) ([]string, error) { + foundWhyResources := []string{} + for _, resource := range resources { + bytes, err := resource.MarshalJSON() + if err != nil { + return foundWhyResources, fmt.Errorf("could not marshal resource: %w", err) + } + json := string(bytes) + resourceTypeKey := "manifest" + if isChart { + resourceTypeKey = "chart" + } + + if strings.Contains(json, whyImage) { + yamlResource, err := yaml.Marshal(resource.Object) + if err != nil { + return foundWhyResources, fmt.Errorf("could not marshal resource: %w", err) + } + fmt.Printf("component: %s\n%s: %s\nresource: %s\n\n%s\n", componentName, resourceTypeKey, resourceName, resource.GetName(), string(yamlResource)) + 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..90ecc653d8 --- /dev/null +++ b/src/test/e2e/13_find_images_test.go @@ -0,0 +1,61 @@ +package test + +import ( + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +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 := []byte{} + // f, err := os.Open("src/test/packages/13-find-images/dos-games-find-images-expected.txt") + // defer f.Close() + // + // _, err = f.Read(expectedOutput) + // require.NoError(t, err, "Expect no error here while reading expectedOutput of the expected output file") + // + // stdout, _, err := e2e.Zarf("dev", "find-images", testPackagePath) + // require.NoError(t, err, "Expect no error here") + // 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", "helm-charts") + expectedOutput := []byte{} + f, err := os.Open("src/test/packages/13-find-images/helm-charts-find-images-why-expected.txt") + defer f.Close() + + _, err = f.Read(expectedOutput) + require.NoError(t, err, "Expect no error here while reading expectedOutput of the expected output file") + + stdout, _, err := e2e.Zarf("dev", "find-images", testPackagePath, "--why", "curlimages/curl:7.69.0") + require.NoError(t, err, "Expect no error here") + 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 a helm chart with why flag") + + testPackagePath := filepath.Join("examples", "manifests") + expectedOutput := []byte{} + f, err := os.Open("src/test/packages/13-find-images/manifests-find-images-why-expected.txt") + defer f.Close() + + _, err = f.Read(expectedOutput) + require.NoError(t, err, "Expect no error here while reading expectedOutput of the expected output file") + + stdout, _, err := e2e.Zarf("dev", "find-images", testPackagePath, "--why", "httpd:alpine3.18") + require.NoError(t, err, "Expect no error here") + 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..45d6ac3113 --- /dev/null +++ b/src/test/packages/13-find-images/helm-charts-find-images-why-expected.txt @@ -0,0 +1,197 @@ +component: demo-helm-charts +chart: podinfo-oci +resource: podinfo-oci-service-test-wlxqz + +apiVersion: v1 +kind: Pod +metadata: + annotations: + appmesh.k8s.aws/sidecarInjectorWebhook: disabled + helm.sh/hook: test-success + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + linkerd.io/inject: disabled + sidecar.istio.io/inject: "false" + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: podinfo-oci + app.kubernetes.io/version: 6.4.0 + helm.sh/chart: podinfo-6.4.0 + name: podinfo-oci-service-test-wlxqz +spec: + containers: + - command: + - sh + - -c + - | + curl -s ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: podinfo-oci.podinfo-from-oci:9898 + image: curlimages/curl:7.69.0 + name: curl + restartPolicy: Never + +component: demo-helm-charts +chart: podinfo-git +resource: podinfo-oci-service-test-wlxqz + +apiVersion: v1 +kind: Pod +metadata: + annotations: + appmesh.k8s.aws/sidecarInjectorWebhook: disabled + helm.sh/hook: test-success + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + linkerd.io/inject: disabled + sidecar.istio.io/inject: "false" + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: podinfo-oci + app.kubernetes.io/version: 6.4.0 + helm.sh/chart: podinfo-6.4.0 + name: podinfo-oci-service-test-wlxqz +spec: + containers: + - command: + - sh + - -c + - | + curl -s ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: podinfo-oci.podinfo-from-oci:9898 + image: curlimages/curl:7.69.0 + name: curl + restartPolicy: Never + +component: demo-helm-charts +chart: podinfo-git +resource: podinfo-git-service-test-gtb1y + +apiVersion: v1 +kind: Pod +metadata: + annotations: + appmesh.k8s.aws/sidecarInjectorWebhook: disabled + helm.sh/hook: test-success + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + linkerd.io/inject: disabled + sidecar.istio.io/inject: "false" + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: podinfo-git + app.kubernetes.io/version: 6.4.0 + helm.sh/chart: podinfo-6.4.0 + name: podinfo-git-service-test-gtb1y +spec: + containers: + - command: + - sh + - -c + - | + curl -s ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: podinfo-git.podinfo-from-git:9898 + image: curlimages/curl:7.69.0 + name: curl + restartPolicy: Never + +component: demo-helm-charts +chart: podinfo-repo +resource: podinfo-oci-service-test-wlxqz + +apiVersion: v1 +kind: Pod +metadata: + annotations: + appmesh.k8s.aws/sidecarInjectorWebhook: disabled + helm.sh/hook: test-success + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + linkerd.io/inject: disabled + sidecar.istio.io/inject: "false" + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: podinfo-oci + app.kubernetes.io/version: 6.4.0 + helm.sh/chart: podinfo-6.4.0 + name: podinfo-oci-service-test-wlxqz +spec: + containers: + - command: + - sh + - -c + - | + curl -s ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: podinfo-oci.podinfo-from-oci:9898 + image: curlimages/curl:7.69.0 + name: curl + restartPolicy: Never + +component: demo-helm-charts +chart: podinfo-repo +resource: podinfo-git-service-test-gtb1y + +apiVersion: v1 +kind: Pod +metadata: + annotations: + appmesh.k8s.aws/sidecarInjectorWebhook: disabled + helm.sh/hook: test-success + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + linkerd.io/inject: disabled + sidecar.istio.io/inject: "false" + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: podinfo-git + app.kubernetes.io/version: 6.4.0 + helm.sh/chart: podinfo-6.4.0 + name: podinfo-git-service-test-gtb1y +spec: + containers: + - command: + - sh + - -c + - | + curl -s ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: podinfo-git.podinfo-from-git:9898 + image: curlimages/curl:7.69.0 + name: curl + restartPolicy: Never + +component: demo-helm-charts +chart: podinfo-repo +resource: cool-release-name-podinfo-service-test-aabrx + +apiVersion: v1 +kind: Pod +metadata: + annotations: + appmesh.k8s.aws/sidecarInjectorWebhook: disabled + helm.sh/hook: test-success + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + linkerd.io/inject: disabled + sidecar.istio.io/inject: "false" + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: cool-release-name-podinfo + app.kubernetes.io/version: 6.4.0 + helm.sh/chart: podinfo-6.4.0 + name: cool-release-name-podinfo-service-test-aabrx +spec: + containers: + - command: + - sh + - -c + - | + curl -s ${PODINFO_SVC}/api/info | grep version + env: + - name: PODINFO_SVC + value: cool-release-name-podinfo.podinfo-from-repo:9898 + image: curlimages/curl:7.69.0 + name: curl + restartPolicy: Never 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..6efb7f4667 --- /dev/null +++ b/src/test/packages/13-find-images/manifests-find-images-why-expected.txt @@ -0,0 +1,23 @@ +component: httpd-local +manifest: simple-httpd-deployment +resource: httpd-deployment + +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.