Skip to content

Commit

Permalink
feat: adds --list-images arg to inspect (#599)
Browse files Browse the repository at this point in the history
Co-authored-by: unclegedd <gedd@defenseunicorns.com>
  • Loading branch information
eFAILution and UncleGedd committed Jun 6, 2024
1 parent 08b2da1 commit 1862f4f
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 7 deletions.
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ repos:
hooks:
- id: golangci-lint-full
args: [--timeout=5m]
linters:
- repo: local
hooks:
- id: check-docs-and-schema
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ Inspect the `uds-bundle.yaml` of a bundle
1. From an OCI registry: `uds inspect oci://ghcr.io/defenseunicorns/dev/<name>:<tag>`
1. From your local filesystem: `uds inspect uds-bundle-<name>.tar.zst`

#### Viewing Images in a Bundle
It is possible derive images from a `uds-bundle.yaml`. This can be useful for situations where you need to know what images will be bundled before you actually create the bundle. This is accomplished with the `--list-images`. For example:

`uds inspect ./uds-bundle.yaml --list-images`

This command will return a list of images derived from the bundle's packages and taking into account optional and required package components.

#### Viewing SBOMs
There are 2 additional flags for the `uds inspect` command you can use to extract and view SBOMs:
- Output the SBOMs as a tar file: `uds inspect ... --sbom`
Expand Down
3 changes: 2 additions & 1 deletion src/cmd/uds.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ var deployCmd = &cobra.Command{
}

var inspectCmd = &cobra.Command{
Use: "inspect [BUNDLE_TARBALL|OCI_REF]",
Use: "inspect [BUNDLE_TARBALL|OCI_REF|BUNDLE_YAML_FILE]",
Aliases: []string{"i"},
Short: lang.CmdBundleInspectShort,
Args: cobra.MaximumNArgs(1),
Expand Down Expand Up @@ -203,6 +203,7 @@ func init() {
inspectCmd.Flags().BoolVarP(&bundleCfg.InspectOpts.IncludeSBOM, "sbom", "s", false, lang.CmdPackageInspectFlagSBOM)
inspectCmd.Flags().BoolVarP(&bundleCfg.InspectOpts.ExtractSBOM, "extract", "e", false, lang.CmdPackageInspectFlagExtractSBOM)
inspectCmd.Flags().StringVarP(&bundleCfg.InspectOpts.PublicKeyPath, "key", "k", v.GetString(V_BNDL_INSPECT_KEY), lang.CmdBundleInspectFlagKey)
inspectCmd.Flags().BoolVarP(&bundleCfg.InspectOpts.ListImages, "list-images", "i", false, lang.CmdBundleInspectFlagFindImages)

// remove cmd flags
rootCmd.AddCommand(removeCmd)
Expand Down
1 change: 1 addition & 0 deletions src/config/lang/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
CmdBundleInspectFlagKey = "Path to a public key file that will be used to validate a signed bundle"
CmdPackageInspectFlagSBOM = "Create a tarball of SBOMs contained in the bundle"
CmdPackageInspectFlagExtractSBOM = "Create a folder of SBOMs contained in the bundle"
CmdBundleInspectFlagFindImages = "Derive images from a uds-bundle.yaml file and list them"

// bundle remove
CmdBundleRemoveShort = "Remove a bundle that has been deployed already"
Expand Down
116 changes: 114 additions & 2 deletions src/pkg/bundle/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,48 @@
package bundle

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

"github.com/defenseunicorns/pkg/oci"
"github.com/defenseunicorns/uds-cli/src/config"
"github.com/defenseunicorns/uds-cli/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/pkg/layout"
"github.com/defenseunicorns/zarf/src/pkg/packager/filters"
zarfSources "github.com/defenseunicorns/zarf/src/pkg/packager/sources"
zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/pkg/zoci"
zarfTypes "github.com/defenseunicorns/zarf/src/types"
"github.com/fatih/color"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pterm/pterm"
)

// Inspect pulls/unpacks a bundle's metadata and shows it
func (b *Bundle) Inspect() error {
// handle --list-images flag
if b.cfg.InspectOpts.ListImages {
err := utils.CheckYAMLSourcePath(b.cfg.InspectOpts.Source)
if err != nil {
return err
}

if err := utils.ReadYAMLStrict(b.cfg.InspectOpts.Source, &b.bundle); err != nil {
return err
}

// find images in the packages taking into account optional components
imgs, err := b.getPackageImages()
if err != nil {
return err
}

formattedImgs := pterm.Color(color.FgHiMagenta).Sprintf(strings.Join(imgs, "\n"))
pterm.Printfln("\n%s\n", formattedImgs)
return nil
}

// Check that provided oci source path is valid, and update it if it's missing the full path
source, err := CheckOCISourcePath(b.cfg.InspectOpts.Source)
Expand Down Expand Up @@ -52,7 +87,84 @@ func (b *Bundle) Inspect() error {
// show the bundle's metadata
zarfUtils.ColorPrintYAML(b.bundle, nil, false)

// TODO: showing package metadata?
// TODO: could be cool to have an interactive mode that lets you select a package and show its metadata
return nil
}

func (b *Bundle) getPackageImages() ([]string, error) {
// use a map to track the images for easy de-duping
imgMap := make(map[string]string)

for _, pkg := range b.bundle.Packages {
// get package source
var source zarfSources.PackageSource
if pkg.Repository != "" {
// handle remote packages
url := fmt.Sprintf("oci://%s:%s", pkg.Repository, pkg.Ref)
platform := ocispec.Platform{
Architecture: config.GetArch(),
OS: oci.MultiOS,
}
remote, err := zoci.NewRemote(url, platform)
if err != nil {
return nil, err
}

source = &zarfSources.OCISource{
ZarfPackageOptions: &zarfTypes.ZarfPackageOptions{},
Remote: remote,
}
} else if pkg.Path != "" {
// handle local packages
err := os.Chdir(filepath.Dir(b.cfg.InspectOpts.Source)) // change to the bundle's directory
if err != nil {
return nil, err
}

bundleArch := config.GetArch(b.bundle.Metadata.Architecture)
tarballName := fmt.Sprintf("zarf-package-%s-%s-%s.tar.zst", pkg.Name, bundleArch, pkg.Ref)
source = &zarfSources.TarballSource{
ZarfPackageOptions: &zarfTypes.ZarfPackageOptions{
PackageSource: filepath.Join(pkg.Path, tarballName),
},
}
} else {
return nil, fmt.Errorf("package %s is missing a repository or path", pkg.Name)
}

tmpDir, err := zarfUtils.MakeTempDir(config.CommonOptions.TempDirectory)
if err != nil {
return nil, err
}
pkgPaths := layout.New(tmpDir)
zarfPkg, _, err := source.LoadPackageMetadata(pkgPaths, false, true)
if err != nil {
return nil, err
}

// create filter for optional components
inspectFilter := filters.Combine(
filters.ForDeploy(strings.Join(pkg.OptionalComponents, ","), false),
)

filteredComponents, err := inspectFilter.Apply(zarfPkg)
if err != nil {
return nil, err
}

// grab images from each filtered component
for _, component := range filteredComponents {
for _, img := range component.Images {
imgMap[img] = img
}
}

}

// convert img map to list of strings
var images []string
for _, img := range imgMap {
images = append(images, img)
}

return images, nil
}
21 changes: 19 additions & 2 deletions src/pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ func ConfigureLogs(cmd *cobra.Command) error {
return err
}

// use Zarf pterm output
message.Notef("Saving log file to %s", tmpLogLocation)
// don't print the note for inspect cmds because they are used in automation
if !strings.Contains(cmd.Use, "inspect") {
message.Notef("Saving log file to %s", tmpLogLocation)
}
return nil
}

Expand Down Expand Up @@ -194,3 +196,18 @@ func ReadYAMLStrict(path string, destConfig any) error {
}
return nil
}

// CheckYAMLSourcePath checks if the provided YAML source path is valid
func CheckYAMLSourcePath(source string) error {
// check if the source is a YAML file
isYaml := strings.HasSuffix(source, ".yaml") || strings.HasSuffix(source, ".yml")
if !isYaml {
return fmt.Errorf("source must have .yaml or yml file extension")
}
// Check if the file exists
if _, err := os.Stat(source); os.IsNotExist(err) {
return fmt.Errorf("file %s does not exist", source)
}

return nil
}
2 changes: 1 addition & 1 deletion src/test/bundles/14-optional-components/uds-bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ packages:
optionalComponents:
- upload-image

# deploys podinfo as an optional component and apache as a required component
# deploys podinfo as an optional component
- name: podinfo-nginx
path: ../../packages/podinfo-nginx
ref: 0.0.1
Expand Down
31 changes: 31 additions & 0 deletions src/test/e2e/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -643,3 +643,34 @@ func TestArchCheck(t *testing.T) {
_, stderr, _ := e2e.UDS(cmd...)
require.Contains(t, stderr, fmt.Sprintf("arch %s does not match cluster arch, [%s]", testArch, e2e.Arch))
}

func TestListImages(t *testing.T) {
e2e.SetupDockerRegistry(t, 888)
defer e2e.TeardownRegistry(t, 888)

zarfPkgPath := "src/test/packages/prometheus"
pkg := filepath.Join(zarfPkgPath, fmt.Sprintf("zarf-package-prometheus-%s-0.0.1.tar.zst", e2e.Arch))
e2e.CreateZarfPkg(t, zarfPkgPath, false)
zarfPublish(t, pkg, "localhost:888")

zarfPkgPath = "src/test/packages/podinfo-nginx"
e2e.CreateZarfPkg(t, zarfPkgPath, false)

bundleDir := "src/test/bundles/14-optional-components"

t.Run("list images on bundle YAML only", func(t *testing.T) {
cmd := strings.Split(fmt.Sprintf("inspect %s --list-images --insecure", filepath.Join(bundleDir, config.BundleYAML)), " ")
_, stderr, err := e2e.UDS(cmd...)
require.NoError(t, err)
require.Contains(t, stderr, "library/registry")
require.Contains(t, stderr, "ghcr.io/defenseunicorns/zarf/agent")
require.Contains(t, stderr, "ghcr.io/stefanprodan/podinfo")
require.Contains(t, stderr, "quay.io/prometheus/node-exporter")

// ensure non-req'd components got filtered
require.NotContains(t, stderr, "grafana")
require.NotContains(t, stderr, "gitea")
require.NotContains(t, stderr, "kiwix")
require.NotContains(t, stderr, "nginx")
})
}
1 change: 1 addition & 0 deletions src/types/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type UDSBundle struct {
type Package struct {
Name string `json:"name" jsonschema:"name=Name of the Zarf package"`
Description string `json:"description,omitempty" jsonschema:"description=Description of the Zarf package"`
Images []string `json:"images,omitempty" jsonschema:"description=List of images included in the Zarf package"`
Repository string `json:"repository,omitempty" jsonschema:"description=The repository to import the package from"`
Path string `json:"path,omitempty" jsonschema:"description=The local path to import the package from"`
Ref string `json:"ref" jsonschema:"description=Ref (tag) of the Zarf package"`
Expand Down
1 change: 1 addition & 0 deletions src/types/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type BundleInspectOptions struct {
Source string
IncludeSBOM bool
ExtractSBOM bool
ListImages bool
}

// BundlePublishOptions is the options for the bundle.Publish() function
Expand Down
7 changes: 7 additions & 0 deletions uds.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@
"type": "string",
"description": "Description of the Zarf package"
},
"images": {
"items": {
"type": "string"
},
"type": "array",
"description": "List of images included in the Zarf package"
},
"repository": {
"type": "string",
"description": "The repository to import the package from"
Expand Down

0 comments on commit 1862f4f

Please sign in to comment.