diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 669f4639..60617141 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,6 @@ repos: hooks: - id: golangci-lint-full args: [--timeout=5m] - linters: - repo: local hooks: - id: check-docs-and-schema diff --git a/README.md b/README.md index 7bfe0058..1b8721d3 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,13 @@ Inspect the `uds-bundle.yaml` of a bundle 1. From an OCI registry: `uds inspect oci://ghcr.io/defenseunicorns/dev/:` 1. From your local filesystem: `uds inspect uds-bundle-.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` diff --git a/src/cmd/uds.go b/src/cmd/uds.go index 9474247f..09f7b1b5 100644 --- a/src/cmd/uds.go +++ b/src/cmd/uds.go @@ -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), @@ -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) diff --git a/src/config/lang/lang.go b/src/config/lang/lang.go index ccd3bacc..904bbfad 100644 --- a/src/config/lang/lang.go +++ b/src/config/lang/lang.go @@ -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" diff --git a/src/pkg/bundle/inspect.go b/src/pkg/bundle/inspect.go index d60592ce..bae9035f 100644 --- a/src/pkg/bundle/inspect.go +++ b/src/pkg/bundle/inspect.go @@ -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) @@ -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 +} diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 019a2ef6..5cf17699 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -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 } @@ -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 +} diff --git a/src/test/bundles/14-optional-components/uds-bundle.yaml b/src/test/bundles/14-optional-components/uds-bundle.yaml index 39073362..0ee9ead3 100644 --- a/src/test/bundles/14-optional-components/uds-bundle.yaml +++ b/src/test/bundles/14-optional-components/uds-bundle.yaml @@ -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 diff --git a/src/test/e2e/bundle_test.go b/src/test/e2e/bundle_test.go index 8db3e99c..8245237f 100644 --- a/src/test/e2e/bundle_test.go +++ b/src/test/e2e/bundle_test.go @@ -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") + }) +} diff --git a/src/types/bundle.go b/src/types/bundle.go index a64496b4..358ba78e 100644 --- a/src/types/bundle.go +++ b/src/types/bundle.go @@ -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"` diff --git a/src/types/options.go b/src/types/options.go index a88dd22e..e060bff3 100644 --- a/src/types/options.go +++ b/src/types/options.go @@ -44,6 +44,7 @@ type BundleInspectOptions struct { Source string IncludeSBOM bool ExtractSBOM bool + ListImages bool } // BundlePublishOptions is the options for the bundle.Publish() function diff --git a/uds.schema.json b/uds.schema.json index 96c03734..8ca65ba7 100644 --- a/uds.schema.json +++ b/uds.schema.json @@ -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"