Skip to content

Commit

Permalink
feat: add --why for zarf dev find-images
Browse files Browse the repository at this point in the history
Add --why flag to `zarf dev find-images` which takes a image tag as an argument.
This command with the `why` flag will output the component, manifest name and the
manifest definition which matches the given image.

Signed-off-by: Vibhav Bobade <vibhav.bobde@gmail.com>
  • Loading branch information
waveywaves committed Feb 20, 2024
1 parent 84b673e commit 55dd0ab
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 9 deletions.
2 changes: 2 additions & 0 deletions src/cmd/dev.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 = "Print the manifest which is using the image"

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
63 changes: 54 additions & 9 deletions src/pkg/packager/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package packager

import (
"fmt"
"github.com/goccy/go-yaml"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -33,14 +34,27 @@ import (
// imageMap is a map of image/boolean pairs.
type imageMap map[string]bool

// manifestResourceRelation defines which
// kubernetes kind is in which manifest
type manifestResourceRelation struct {
Manifests []types.ZarfManifest
Resources []*unstructured.Unstructured
}

// componentResourceMap defines which component contains
// which resources and manifests
type componentResourceMap map[string]manifestResourceRelation

// FindImages iterates over a Zarf.yaml and attempts to parse any images.
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{}
whyImageResources := componentResourceMap{}

cwd, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -79,6 +93,7 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
}

componentDefinition := "\ncomponents:\n"
imageDefinition := ""

for _, component := range p.cfg.Pkg.Components {

Expand Down Expand Up @@ -200,9 +215,15 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
defer spinner.Stop()

for _, resource := range resources {
if matchedImages, maybeImages, err = p.processUnstructuredImages(resource, matchedImages, maybeImages); err != nil {
var whyImageResourcesPartial []*unstructured.Unstructured
if matchedImages, maybeImages, whyImageResourcesPartial, err = p.processUnstructuredImages(resource, matchedImages, maybeImages, whyImage); err != nil {
message.WarnErrf(err, "Problem processing K8s resource %s", resource.GetName())
}
updatedResources := append(whyImageResources[component.Name].Resources, whyImageResourcesPartial...)
whyImageResources[component.Name] = manifestResourceRelation{
Manifests: component.Manifests,
Resources: updatedResources,
}
}

if sortedImages := sortImages(matchedImages, nil); len(sortedImages) > 0 {
Expand Down Expand Up @@ -268,7 +289,27 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
}
}

fmt.Println(componentDefinition)
if len(whyImage) > 0 {
for name, resources := range whyImageResources {
yamlResources := []string{}
manifestNames := []string{}
for _, resource := range resources.Resources {
yamlResource, _ := yaml.Marshal(resource.Object)
yamlResources = append(yamlResources, string(yamlResource))
}
for _, manifest := range resources.Manifests {
manifestNames = append(manifestNames, manifest.Name)
}
manifestNameString := strings.Join(manifestNames, ", ")
imageDefinition += fmt.Sprintf("component: %s\nmanifests: %s\nresources: \n", name, manifestNameString)
for _, yamlResource := range yamlResources {
imageDefinition += fmt.Sprintf("\n%s\n", yamlResource)
}
}
fmt.Println(imageDefinition)
} else {
fmt.Println(componentDefinition)
}

// Return to the original working directory
if err := os.Chdir(cwd); err != nil {
Expand All @@ -292,48 +333,49 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) {
return imagesMap, nil
}

func (p *Packager) processUnstructuredImages(resource *unstructured.Unstructured, matchedImages, maybeImages imageMap) (imageMap, imageMap, error) {
func (p *Packager) processUnstructuredImages(resource *unstructured.Unstructured, matchedImages, maybeImages imageMap, whyImage string) (imageMap, imageMap, []*unstructured.Unstructured, error) {
var imageSanityCheck = regexp.MustCompile(`(?mi)"image":"([^"]+)"`)
var imageFuzzyCheck = regexp.MustCompile(`(?mi)["|=]([a-z0-9\-.\/:]+:[\w.\-]*[a-z\.\-][\w.\-]*)"`)
var json string

contents := resource.UnstructuredContent()
bytes, _ := resource.MarshalJSON()
json = string(bytes)
whyImageResources := []*unstructured.Unstructured{}

switch resource.GetKind() {
case "Deployment":
var deployment v1.Deployment
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(contents, &deployment); err != nil {
return matchedImages, maybeImages, fmt.Errorf("could not parse deployment: %w", err)
return matchedImages, maybeImages, nil, fmt.Errorf("could not parse deployment: %w", err)
}
matchedImages = buildImageMap(matchedImages, deployment.Spec.Template.Spec)

case "DaemonSet":
var daemonSet v1.DaemonSet
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(contents, &daemonSet); err != nil {
return matchedImages, maybeImages, fmt.Errorf("could not parse daemonset: %w", err)
return matchedImages, maybeImages, nil, fmt.Errorf("could not parse daemonset: %w", err)
}
matchedImages = buildImageMap(matchedImages, daemonSet.Spec.Template.Spec)

case "StatefulSet":
var statefulSet v1.StatefulSet
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(contents, &statefulSet); err != nil {
return matchedImages, maybeImages, fmt.Errorf("could not parse statefulset: %w", err)
return matchedImages, maybeImages, nil, fmt.Errorf("could not parse statefulset: %w", err)
}
matchedImages = buildImageMap(matchedImages, statefulSet.Spec.Template.Spec)

case "ReplicaSet":
var replicaSet v1.ReplicaSet
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(contents, &replicaSet); err != nil {
return matchedImages, maybeImages, fmt.Errorf("could not parse replicaset: %w", err)
return matchedImages, maybeImages, nil, fmt.Errorf("could not parse replicaset: %w", err)
}
matchedImages = buildImageMap(matchedImages, replicaSet.Spec.Template.Spec)

case "Job":
var job batchv1.Job
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(contents, &job); err != nil {
return matchedImages, maybeImages, fmt.Errorf("could not parse job: %w", err)
return matchedImages, maybeImages, nil, fmt.Errorf("could not parse job: %w", err)
}
matchedImages = buildImageMap(matchedImages, job.Spec.Template.Spec)

Expand All @@ -350,10 +392,13 @@ func (p *Packager) processUnstructuredImages(resource *unstructured.Unstructured
matches := imageFuzzyCheck.FindAllStringSubmatch(json, -1)
for _, group := range matches {
message.Debugf("Found possible fuzzy match, Kind: %s, Value: %s", resource.GetKind(), group[1])
if whyImage == group[1] {
whyImageResources = append(whyImageResources, resource)
}
maybeImages[group[1]] = true
}

return matchedImages, maybeImages, nil
return matchedImages, maybeImages, whyImageResources, nil
}

// BuildImageMap looks for init container, ephemeral and regular container images.
Expand Down
28 changes: 28 additions & 0 deletions src/test/e2e/13_find_images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package test

import (
"github.com/stretchr/testify/require"
"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")
_, _, err := e2e.Zarf("dev", "find-images", testPackagePath)
require.NoError(t, err, "Expect no error here")
})

t.Run("zarf test find images why success", func(t *testing.T) {
t.Log("E2E: Test Find Images with why flag")

testPackagePath := filepath.Join("examples", "dos-games")
_, _, err := e2e.Zarf("dev", "find-images", testPackagePath, "--why", "defenseunicorns/zarf-game:multi-tile-dark")
require.NoError(t, err, "Expect no error here")
})

}
1 change: 1 addition & 0 deletions src/types/runtime.go
Original file line number Diff line number Diff line change
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=Print the source manifest where the image is being consumed"`
}

// ZarfDeployOptions tracks the user-defined preferences during a package deploy.
Expand Down

0 comments on commit 55dd0ab

Please sign in to comment.