Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds --list-images arg to inspect #599

Merged
merged 14 commits into from
Jun 6, 2024
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 @@ -107,6 +107,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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:
It is possible to 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 `--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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This command will return a list of images derived from the bundle's packages and taking into account optional and required package components.
This command will return a list of images derived from the bundle's packages, 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but is there a particular reason to have the err check separate on this one but "inlined" (if that's the right word) for the next call. just wondering if we can make them all look the same for consistency.

Also, do you think there's any value in extracting the list image functions into a separate function listImages() that gets called in Inspect(). Feels like it's getting a little long.

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks good to me, but also wondering if we want to leverage the pkg/helpers more. there's a InvalidPath method i used for the FileValues handling.

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"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this getting used somewhere? having trouble understanding how it's being used / set.

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
Loading