Skip to content

Commit

Permalink
feat: add --why flag for zarf dev find-images (#2309)
Browse files Browse the repository at this point in the history
## 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 <vibhav.bobde@gmail.com>
Co-authored-by: Wayne Starr <Racer159@users.noreply.github.com>
Co-authored-by: Lucas Rodriguez <lucas.rodriguez@defenseunicorns.com>
Co-authored-by: Lucas Rodriguez <lucas.rodriguez9616@gmail.com>
  • Loading branch information
4 people committed Mar 6, 2024
1 parent 21ccaaa commit 5efcb35
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 0 deletions.
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/dev.go
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/config/lang/english.go
Expand Up @@ -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"
Expand Down
49 changes: 49 additions & 0 deletions src/pkg/packager/prepare.go
Expand Up @@ -6,6 +6,7 @@ package packager

import (
"fmt"
"github.com/goccy/go-yaml"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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...)
}
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
50 changes: 50 additions & 0 deletions 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))
})

}
@@ -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
@@ -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

@@ -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

1 change: 1 addition & 0 deletions src/types/runtime.go
Expand Up @@ -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.
Expand Down

0 comments on commit 5efcb35

Please sign in to comment.