diff --git a/README.md b/README.md index 1b8721d3..84cf6536 100644 --- a/README.md +++ b/README.md @@ -293,8 +293,7 @@ UDS CLI includes a vendored version of Zarf inside of its binary. To use Zarf, s ## Dev Mode -> [!NOTE] -> Dev mode is a BETA feature +NOTE: Dev mode is a BETA feature Dev mode facilitates faster dev cycles when developing and testing bundles @@ -302,14 +301,18 @@ Dev mode facilitates faster dev cycles when developing and testing bundles uds dev deploy | ``` -The `dev deploy` command performs the following operations +The `dev deploy` command performs the following operations: -- If local bundle: Creates Zarf packages for all local packages in a bundle - - Creates the Zarf tarball in the same directory as the `zarf.yaml` - - Will only create the Zarf tarball if one does not already exist - - Ignores any `kind: ZarfInitConfig` packages in the bundle - - Creates a bundle from the newly created Zarf packages - Deploys the bundle in [YOLO](https://docs.zarf.dev/faq/#what-is-yolo-mode-and-why-would-i-use-it) mode, eliminating the need to do a `zarf init` + - Any `kind: ZarfInitConfig` packages in the bundle will be ignored +- For local bundles: + - For local packages: + - Creates the Zarf tarball if one does not already exist or the `--force-create` flag can be used to force the creation of a new Zarf package + - The Zarf tarball is created in the same directory as the `zarf.yaml` + - The `--flavor` flag can be used to specify what flavor of a package you want to create (example: `--flavor podinfo=upstream` to specify the flavor for the `podinfo` package or `--flavor upstream` to specify the flavor for all the packages in the bundle) + - For remote packages: + - The `--ref` flag can be used to specify what package ref you want to deploy (example: `--ref podinfo=0.2.0`) + - Creates a bundle from the newly created Zarf packages ## Scan diff --git a/hack/push-test-artifacts.sh b/hack/push-test-artifacts.sh index 23c465ac..731875b1 100755 --- a/hack/push-test-artifacts.sh +++ b/hack/push-test-artifacts.sh @@ -12,8 +12,14 @@ set -e cd ./../src/test/packages/nginx zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a amd64 zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a arm64 +cd ./refs +zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a amd64 +zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a arm64 -cd ../podinfo +cd ../../podinfo +zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a amd64 +zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a arm64 +cd ./refs zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a amd64 zarf package create -o oci://ghcr.io/defenseunicorns/uds-cli --confirm -a arm64 diff --git a/src/cmd/dev.go b/src/cmd/dev.go index 4425791d..3fd5fdc3 100644 --- a/src/cmd/dev.go +++ b/src/cmd/dev.go @@ -5,6 +5,9 @@ package cmd import ( + "fmt" + "strings" + "github.com/defenseunicorns/pkg/helpers/v2" "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/config/lang" @@ -26,6 +29,7 @@ var devDeployCmd = &cobra.Command{ Long: lang.CmdDevDeployLong, Run: func(_ *cobra.Command, args []string) { config.Dev = true + config.CommonOptions.Confirm = true // Get bundle source src := "" @@ -34,13 +38,24 @@ var devDeployCmd = &cobra.Command{ } // Check if source is a local bundle - localBundle := helpers.IsDir(src) + isLocalBundle := isLocalBundle(src) + + // Validate flags + err := validateDevDeployFlags(isLocalBundle) + if err != nil { + message.Fatalf(err, "Failed to validate flags: %s", err.Error()) + } + + if isLocalBundle { + // Populate flavor map + err = populateFlavorMap() + if err != nil { + message.Fatalf(err, "Failed to populate flavor map: %s", err.Error()) + } - if localBundle { // Create Bundle setBundleFile(args) - config.CommonOptions.Confirm = true bundleCfg.CreateOpts.SourceDirectory = src } @@ -58,17 +73,13 @@ var devDeployCmd = &cobra.Command{ defer bndlClient.ClearPaths() // Create dev bundle - if localBundle { + if isLocalBundle { // Check if local zarf packages need to be created bndlClient.CreateZarfPkgs() if err := bndlClient.Create(); err != nil { message.Fatalf(err, "Failed to create bundle: %s", err.Error()) } - } - - // Set dev source - if localBundle { bndlClient.SetDeploySource(src) } else { bundleCfg.DeployOpts.Source = src @@ -79,11 +90,51 @@ var devDeployCmd = &cobra.Command{ }, } +// isLocalBundle checks if the bundle source is a local bundle +func isLocalBundle(src string) bool { + return helpers.IsDir(src) || strings.Contains(src, ".tar.zst") +} + +// validateDevDeployFlags validates the flags for dev deploy +func validateDevDeployFlags(isLocalBundle bool) error { + if !isLocalBundle { + //Throw error if trying to run with --flavor or --force-create flag with remote bundle + if len(bundleCfg.DevDeployOpts.Flavor) > 0 || bundleCfg.DevDeployOpts.ForceCreate { + return fmt.Errorf("Cannot use --flavor or --force-create flags with remote bundle") + } + } + return nil +} + +// populateFlavorMap populates the flavor map based on the string input to the --flavor flag +func populateFlavorMap() error { + if bundleCfg.DevDeployOpts.FlavorInput != "" { + bundleCfg.DevDeployOpts.Flavor = make(map[string]string) + flavorEntries := strings.Split(bundleCfg.DevDeployOpts.FlavorInput, ",") + for i, entry := range flavorEntries { + entrySplit := strings.Split(entry, "=") + if len(entrySplit) != 2 { + // check i==0 to check for invalid input (ex. key=value1,value2) + if len(entrySplit) == 1 && i == 0 { + bundleCfg.DevDeployOpts.Flavor = map[string]string{"": bundleCfg.DevDeployOpts.FlavorInput} + } else { + return fmt.Errorf("Invalid flavor entry: %s", entry) + } + } else { + bundleCfg.DevDeployOpts.Flavor[entrySplit[0]] = entrySplit[1] + } + } + } + return nil +} + func init() { initViper() rootCmd.AddCommand(devCmd) devCmd.AddCommand(devDeployCmd) devDeployCmd.Flags().StringArrayVarP(&bundleCfg.DeployOpts.Packages, "packages", "p", []string{}, lang.CmdBundleDeployFlagPackages) - devDeployCmd.Flags().BoolVarP(&config.CommonOptions.Confirm, "confirm", "c", false, lang.CmdBundleDeployFlagConfirm) + devDeployCmd.Flags().StringToStringVarP(&bundleCfg.DevDeployOpts.Ref, "ref", "r", map[string]string{}, lang.CmdBundleDeployFlagRef) + devDeployCmd.Flags().StringVarP(&bundleCfg.DevDeployOpts.FlavorInput, "flavor", "f", "", lang.CmdBundleCreateFlagFlavor) + devDeployCmd.Flags().BoolVar(&bundleCfg.DevDeployOpts.ForceCreate, "force-create", false, lang.CmdBundleCreateForceCreate) devDeployCmd.Flags().StringToStringVar(&bundleCfg.DeployOpts.SetVariables, "set", nil, lang.CmdBundleDeployFlagSet) } diff --git a/src/cmd/dev_test.go b/src/cmd/dev_test.go new file mode 100644 index 00000000..0a60ae99 --- /dev/null +++ b/src/cmd/dev_test.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "testing" + + "github.com/defenseunicorns/uds-cli/src/types" + "github.com/stretchr/testify/require" +) + +func TestValidateDevDeployFlags(t *testing.T) { + testCases := []struct { + name string + localBundle bool + DevDeployOpts types.BundleDevDeployOptions + expectError bool + }{ + { + name: "Local bundle with --ref flag", + localBundle: true, + DevDeployOpts: types.BundleDevDeployOptions{ + Ref: map[string]string{"some-key": "some-ref"}, + }, + expectError: true, + }, + { + name: "Remote bundle with --ref flag", + localBundle: false, + DevDeployOpts: types.BundleDevDeployOptions{ + Ref: map[string]string{"some-key": "some-ref"}, + }, + expectError: false, + }, + { + name: "Local bundle with --flavor flag", + localBundle: true, + DevDeployOpts: types.BundleDevDeployOptions{ + Flavor: map[string]string{"some-key": "some-flavor"}, + }, + expectError: false, + }, + { + name: "Remote bundle with --flavor flag", + localBundle: false, + DevDeployOpts: types.BundleDevDeployOptions{ + Flavor: map[string]string{"some-key": "some-flavor"}, + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bundleCfg.DevDeployOpts = tc.DevDeployOpts + + err := validateDevDeployFlags(tc.localBundle) + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestIsLocalBundle(t *testing.T) { + testCases := []struct { + name string + src string + want bool + }{ + { + name: "Test with directory", + src: "../cmd/", + want: true, + }, + { + name: "Test with .tar.zst file", + src: "/path/to/file.tar.zst", + want: true, + }, + { + name: "Test with other file", + src: "/path/to/file.txt", + want: false, + }, + { + name: "Test with registry", + src: "ghcr.io/defenseunicorns/uds-cli/nginx", + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := isLocalBundle(tc.src) + require.Equal(t, tc.want, got) + }) + } +} + +func TestPopulateFlavorMap(t *testing.T) { + testCases := []struct { + name string + FlavorInput string + expect map[string]string + expectError bool + }{ + { + name: "Test with valid flavor input", + FlavorInput: "key1=value1,key2=value2", + expect: map[string]string{"key1": "value1", "key2": "value2"}, + }, + { + name: "Test with single value", + FlavorInput: "value1", + expect: map[string]string{"": "value1"}, + }, + { + name: "Test with invalid flavor input", + FlavorInput: "key1=value1,key2", + expectError: true, + }, + { + name: "Test with empty flavor input", + FlavorInput: "", + expect: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bundleCfg.DevDeployOpts.FlavorInput = tc.FlavorInput + bundleCfg.DevDeployOpts.Flavor = nil + err := populateFlavorMap() + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expect, bundleCfg.DevDeployOpts.Flavor) + } + }) + } +} diff --git a/src/config/lang/lang.go b/src/config/lang/lang.go index 904bbfad..77196558 100644 --- a/src/config/lang/lang.go +++ b/src/config/lang/lang.go @@ -29,6 +29,7 @@ const ( CmdBundleCreateFlagOutput = "Specify the output (an oci:// URL) for the created bundle" CmdBundleCreateFlagSigningKey = "Path to private key file for signing bundles" CmdBundleCreateFlagSigningKeyPassword = "Password to the private key file used for signing bundles" + CmdBundleCreateFlagFlavor = "Specify which zarf package flavor you want to use." // bundle deploy CmdBundleDeployShort = "Deploy a bundle from a local tarball or oci:// URL" @@ -37,6 +38,7 @@ const ( CmdBundleDeployFlagResume = "Only deploys packages from the bundle which haven't already been deployed" CmdBundleDeployFlagSet = "Specify deployment variables to set on the command line (KEY=value)" CmdBundleDeployFlagRetries = "Specify the number of retries for package deployments (applies to all pkgs in a bundle)" + CmdBundleDeployFlagRef = "Specify which zarf package ref you want to deploy. By default the ref set in the bundle yaml is used." // bundle inspect CmdBundleInspectShort = "Display the metadata of a bundle" @@ -82,7 +84,8 @@ const ( CmdZarfShort = "Run a zarf command" // uds dev - CmdDevShort = "Commands useful for developing bundles" - CmdDevDeployShort = "[beta] Creates and deploys a UDS bundle from a given directory in dev mode" - CmdDevDeployLong = "[beta] Creates and deploys a UDS bundle from a given directory in dev mode, setting package options like YOLO mode for faster iteration." + CmdDevShort = "[beta] Commands useful for developing bundles" + CmdDevDeployShort = "[beta] Creates and deploys a UDS bundle from a given directory in dev mode" + CmdDevDeployLong = "[beta] Creates and deploys a UDS bundle from a given directory in dev mode, setting package options like YOLO mode for faster iteration." + CmdBundleCreateForceCreate = "For local bundles with local packages, specify whether to create a zarf package even if it already exists." ) diff --git a/src/pkg/bundle/common.go b/src/pkg/bundle/common.go index c1f31791..9467ea5e 100644 --- a/src/pkg/bundle/common.go +++ b/src/pkg/bundle/common.go @@ -344,3 +344,37 @@ func validateBundleVars(packages []types.Package) error { } return nil } + +// setPackageRef sets the package reference +func (b *Bundle) setPackageRef(pkg types.Package) types.Package { + if ref, ok := b.cfg.DevDeployOpts.Ref[pkg.Name]; ok { + // Can only set refs for remote packages + if pkg.Repository == "" { + message.Fatalf(errors.New("Invalid input"), "Cannot set ref for local packages: %s", pkg.Name) + } + + errMsg := fmt.Sprintf("Unable to access %s:%s", pkg.Repository, ref) + + // Get SHA from registry + url := fmt.Sprintf("%s:%s", pkg.Repository, ref) + + platform := ocispec.Platform{ + Architecture: config.GetArch(), + OS: oci.MultiOS, + } + remote, err := zoci.NewRemote(url, platform) + if err != nil { + message.Fatalf(err, errMsg) + } + if err := remote.Repo().Reference.ValidateReferenceAsDigest(); err != nil { + manifestDesc, err := remote.ResolveRoot(context.TODO()) + if err != nil { + message.Fatalf(err, errMsg) + } + pkg.Ref = ref + "@sha256:" + manifestDesc.Digest.Encoded() + } else { + message.Fatalf(err, errMsg) + } + } + return pkg +} diff --git a/src/pkg/bundle/create.go b/src/pkg/bundle/create.go index 3c691235..1f506750 100644 --- a/src/pkg/bundle/create.go +++ b/src/pkg/bundle/create.go @@ -34,11 +34,6 @@ func (b *Bundle) Create() error { return err } - // Populate values from valuesFiles if provided - if err := b.processValuesFiles(); err != nil { - return err - } - // confirm creation if ok := b.confirmBundleCreation(); !ok { return fmt.Errorf("bundle creation cancelled") @@ -86,6 +81,14 @@ func (b *Bundle) Create() error { } } + // for dev mode update package ref for local bundles, refs for remote bundles updated on deploy + if config.Dev && len(b.cfg.DevDeployOpts.Ref) != 0 { + for i, pkg := range b.bundle.Packages { + pkg = b.setPackageRef(pkg) + b.bundle.Packages[i] = pkg + } + } + opts := bundler.Options{ Bundle: &b.bundle, Output: b.cfg.CreateOpts.Output, @@ -93,6 +96,7 @@ func (b *Bundle) Create() error { SourceDir: b.cfg.CreateOpts.SourceDirectory, } bundlerClient := bundler.NewBundler(&opts) + return bundlerClient.Create() } diff --git a/src/pkg/bundle/deploy.go b/src/pkg/bundle/deploy.go index d75ff7e1..6f2997e4 100644 --- a/src/pkg/bundle/deploy.go +++ b/src/pkg/bundle/deploy.go @@ -57,7 +57,6 @@ func (b *Bundle) Deploy() error { } else { packagesToDeploy = b.bundle.Packages } - return deployPackages(packagesToDeploy, resume, b) } @@ -79,7 +78,12 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle) error { } // deploy each package - for _, pkg := range packagesToDeploy { + for i, pkg := range packagesToDeploy { + // for dev mode update package ref for remote bundles, refs for local bundles updated on create + if config.Dev && !strings.Contains(b.cfg.DeployOpts.Source, "tar.zst") { + pkg = b.setPackageRef(pkg) + b.bundle.Packages[i] = pkg + } sha := strings.Split(pkg.Ref, "@sha256:")[1] // using appended SHA from create! pkgTmp, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) if err != nil { @@ -126,7 +130,7 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle) error { // Automatically confirm the package deployment zarfConfig.CommonOptions.Confirm = true - source, err := sources.New(b.cfg.DeployOpts.Source, pkg, opts, sha, nsOverrides) + source, err := sources.New(*b.cfg, pkg, opts, sha, nsOverrides) if err != nil { return err } diff --git a/src/pkg/bundle/dev.go b/src/pkg/bundle/dev.go index 2ab10ebe..0b969fd9 100644 --- a/src/pkg/bundle/dev.go +++ b/src/pkg/bundle/dev.go @@ -5,6 +5,7 @@ package bundle import ( + "errors" "fmt" "os" "path/filepath" @@ -12,6 +13,7 @@ import ( "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/pkg/utils" + "github.com/defenseunicorns/uds-cli/src/types" zarfCLI "github.com/defenseunicorns/zarf/src/cmd" "github.com/defenseunicorns/zarf/src/pkg/message" @@ -27,6 +29,15 @@ func (b *Bundle) CreateZarfPkgs() { zarfPackagePattern := `^zarf-.*\.tar\.zst$` for _, pkg := range b.bundle.Packages { + // Can only set flavors for local packages + if pkg.Path == "" { + // check if attempting to apply flavor to remote package + if (len(b.cfg.DevDeployOpts.Flavor) == 1 && b.cfg.DevDeployOpts.Flavor[""] != "") || + (b.cfg.DevDeployOpts.Flavor[pkg.Name] != "") { + message.Fatalf(errors.New("Invalid input"), "Cannot set flavor for remote packages: %s", pkg.Name) + } + } + // if pkg is a local zarf package, attempt to create it if it doesn't exist if pkg.Path != "" { path := getPkgPath(pkg, config.GetArch(b.bundle.Metadata.Architecture), srcDir) @@ -47,8 +58,13 @@ func (b *Bundle) CreateZarfPkgs() { } } // create local zarf package if it doesn't exist - if !packageFound { - os.Args = []string{"zarf", "package", "create", pkgDir, "--confirm", "-o", pkgDir, "--skip-sbom"} + if !packageFound || b.cfg.DevDeployOpts.ForceCreate { + if len(b.cfg.DevDeployOpts.Flavor) != 0 { + pkg = b.setPackageFlavor(pkg) + os.Args = []string{"zarf", "package", "create", pkgDir, "--confirm", "-o", pkgDir, "--skip-sbom", "--flavor", pkg.Flavor} + } else { + os.Args = []string{"zarf", "package", "create", pkgDir, "--confirm", "-o", pkgDir, "--skip-sbom"} + } zarfCLI.Execute() if err != nil { message.Fatalf(err, "Failed to create package %s: %s", pkg.Name, err.Error()) @@ -58,6 +74,17 @@ func (b *Bundle) CreateZarfPkgs() { } } +func (b *Bundle) setPackageFlavor(pkg types.Package) types.Package { + // handle case when --flavor flag applies to all packages + // empty key references a value that is applied to all package flavors + if len(b.cfg.DevDeployOpts.Flavor) == 1 && b.cfg.DevDeployOpts.Flavor[""] != "" { + pkg.Flavor = b.cfg.DevDeployOpts.Flavor[""] + } else if flavor, ok := b.cfg.DevDeployOpts.Flavor[pkg.Name]; ok { + pkg.Flavor = flavor + } + return pkg +} + // SetDeploySource sets the source for the bundle when in dev mode func (b *Bundle) SetDeploySource(srcDir string) { filename := fmt.Sprintf("%s%s-%s-%s.tar.zst", config.BundlePrefix, b.bundle.Metadata.Name, b.bundle.Metadata.Architecture, b.bundle.Metadata.Version) diff --git a/src/pkg/bundle/remove.go b/src/pkg/bundle/remove.go index a6768064..8a7f0f52 100644 --- a/src/pkg/bundle/remove.go +++ b/src/pkg/bundle/remove.go @@ -94,7 +94,7 @@ func removePackages(packagesToRemove []types.Package, b *Bundle) error { } sha := strings.Split(pkg.Ref, "sha256:")[1] - source, err := sources.New(b.cfg.RemoveOpts.Source, pkg, opts, sha, nil) + source, err := sources.New(*b.cfg, pkg, opts, sha, nil) if err != nil { return err } diff --git a/src/pkg/bundler/localbundle.go b/src/pkg/bundler/localbundle.go index 711f55a5..30031cc2 100644 --- a/src/pkg/bundler/localbundle.go +++ b/src/pkg/bundler/localbundle.go @@ -66,7 +66,7 @@ func (lo *LocalBundle) create(signature []byte) error { message.HeaderInfof("🐕 Fetching Packages") - // create root manifest for bundle, will populate with refs to uds-bundle.yaml and zarf image manifests + // create root manifest for bundle, will populate with ref to uds-bundle.yaml and zarf image manifests rootManifest := ocispec.Manifest{ MediaType: ocispec.MediaTypeImageManifest, } diff --git a/src/pkg/sources/new.go b/src/pkg/sources/new.go index 8c902772..56681e49 100644 --- a/src/pkg/sources/new.go +++ b/src/pkg/sources/new.go @@ -5,6 +5,7 @@ package sources import ( + "fmt" "strings" "github.com/defenseunicorns/pkg/oci" @@ -17,8 +18,17 @@ import ( ) // New creates a new package source based on pkgLocation -func New(pkgLocation string, pkg types.Package, opts zarfTypes.ZarfPackageOptions, sha string, nsOverrides NamespaceOverrideMap) (zarfSources.PackageSource, error) { +func New(bundleCfg types.BundleConfig, pkg types.Package, opts zarfTypes.ZarfPackageOptions, sha string, nsOverrides NamespaceOverrideMap) (zarfSources.PackageSource, error) { var source zarfSources.PackageSource + var pkgLocation string + if bundleCfg.DeployOpts.Source != "" { + pkgLocation = bundleCfg.DeployOpts.Source + } else if bundleCfg.RemoveOpts.Source != "" { + pkgLocation = bundleCfg.RemoveOpts.Source + } else { + return nil, fmt.Errorf("no source provided for package %s", pkg.Name) + } + if strings.Contains(pkgLocation, "tar.zst") { source = &TarballBundle{ Pkg: pkg, @@ -44,6 +54,7 @@ func New(pkgLocation string, pkg types.Package, opts zarfTypes.ZarfPackageOption TmpDir: opts.PackageSource, Remote: remote.OrasRemote, nsOverrides: nsOverrides, + bundleCfg: bundleCfg, } } return source, nil diff --git a/src/pkg/sources/remote.go b/src/pkg/sources/remote.go index 7dc30537..8cfc0755 100644 --- a/src/pkg/sources/remote.go +++ b/src/pkg/sources/remote.go @@ -37,12 +37,33 @@ type RemoteBundle struct { TmpDir string Remote *oci.OrasRemote nsOverrides NamespaceOverrideMap + bundleCfg types.BundleConfig } // LoadPackage loads a Zarf package from a remote bundle func (r *RemoteBundle) LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (zarfTypes.ZarfPackage, []string, error) { // todo: progress bar?? - layers, err := r.downloadPkgFromRemoteBundle() + var layers []ocispec.Descriptor + var err error + + if config.Dev { + if _, ok := r.bundleCfg.DevDeployOpts.Ref[r.Pkg.Name]; ok { + // create new oras remote for package + platform := ocispec.Platform{ + Architecture: config.GetArch(), + OS: oci.MultiOS, + } + // get remote client + repoUrl := fmt.Sprintf("%s:%s", r.Pkg.Repository, r.Pkg.Ref) + remote, _ := zoci.NewRemote(repoUrl, platform) + layers, err = remote.PullPackage(context.TODO(), r.TmpDir, config.CommonOptions.OCIConcurrency) + } else { + layers, err = r.downloadPkgFromRemoteBundle() + } + } else { + layers, err = r.downloadPkgFromRemoteBundle() + } + if err != nil { return zarfTypes.ZarfPackage{}, nil, err } diff --git a/src/test/bundles/15-dev-deploy/uds-bundle.yaml b/src/test/bundles/15-dev-deploy/uds-bundle.yaml new file mode 100644 index 00000000..5d980e3e --- /dev/null +++ b/src/test/bundles/15-dev-deploy/uds-bundle.yaml @@ -0,0 +1,10 @@ +kind: UDSBundle +metadata: + name: dev-deploy-flavors + description: building from a local Zarf pkg with flavors + version: 0.0.1 + +packages: + - name: podinfo + path: "../../packages/podinfo/flavors" + ref: 0.0.1 diff --git a/src/test/e2e/commands_test.go b/src/test/e2e/commands_test.go index cda25a57..d93564e8 100644 --- a/src/test/e2e/commands_test.go +++ b/src/test/e2e/commands_test.go @@ -119,7 +119,7 @@ func deploy(t *testing.T, tarballPath string) (stdout string, stderr string) { } func devDeploy(t *testing.T, bundlePath string) (stdout string, stderr string) { - cmd := strings.Split(fmt.Sprintf("dev deploy %s --confirm", bundlePath), " ") + cmd := strings.Split(fmt.Sprintf("dev deploy %s", bundlePath), " ") stdout, stderr, err := e2e.UDS(cmd...) require.NoError(t, err) return stdout, stderr diff --git a/src/test/e2e/dev_test.go b/src/test/e2e/dev_test.go index b0dbf759..e9a14798 100644 --- a/src/test/e2e/dev_test.go +++ b/src/test/e2e/dev_test.go @@ -16,7 +16,6 @@ import ( func TestDevDeploy(t *testing.T) { removeZarfInit() - cmd := strings.Split("zarf tools kubectl get deployments -A -o=jsonpath='{.items[*].metadata.name}'", " ") t.Run("Test dev deploy with local and remote pkgs", func(t *testing.T) { @@ -27,6 +26,7 @@ func TestDevDeploy(t *testing.T) { devDeploy(t, bundleDir) + cmd := strings.Split("zarf tools kubectl get deployments -A -o=jsonpath='{.items[*].metadata.name}'", " ") deployments, _, _ := e2e.UDS(cmd...) require.Contains(t, deployments, "podinfo") require.Contains(t, deployments, "nginx") @@ -43,6 +43,7 @@ func TestDevDeploy(t *testing.T) { devDeployPackages(t, bundleDir, "podinfo") + cmd := strings.Split("zarf tools kubectl get deployments -A -o=jsonpath='{.items[*].metadata.name}'", " ") deployments, _, _ := e2e.UDS(cmd...) require.Contains(t, deployments, "podinfo") require.NotContains(t, deployments, "nginx") @@ -50,12 +51,90 @@ func TestDevDeploy(t *testing.T) { remove(t, bundlePath) }) + t.Run("Test dev deploy with ref flag", func(t *testing.T) { + e2e.DeleteZarfPkg(t, "src/test/packages/podinfo") + bundleDir := "src/test/bundles/03-local-and-remote" + + cmd := strings.Split(fmt.Sprintf("dev deploy %s --ref %s", bundleDir, "nginx=0.0.2"), " ") + _, _, err := e2e.UDS(cmd...) + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl get deployment -n nginx nginx-deployment -o=jsonpath='{.spec.template.spec.containers[0].image}'", " ") + ref, _, err := e2e.UDS(cmd...) + require.Contains(t, ref, "nginx:1.26.0") + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl delete ns podinfo nginx zarf", " ") + _, _, err = e2e.UDS(cmd...) + require.NoError(t, err) + }) + + t.Run("Test dev deploy with flavor flag", func(t *testing.T) { + e2e.DeleteZarfPkg(t, "src/test/packages/podinfo/flavors") + bundleDir := "src/test/bundles/15-dev-deploy" + + cmd := strings.Split(fmt.Sprintf("dev deploy %s --flavor %s", bundleDir, "podinfo=patchVersion3"), " ") + _, _, err := e2e.UDS(cmd...) + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl get deployment -n podinfo-flavor podinfo -o=jsonpath='{.spec.template.spec.containers[0].image}'", " ") + ref, _, err := e2e.UDS(cmd...) + require.Contains(t, ref, "ghcr.io/stefanprodan/podinfo:6.6.3") + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl delete ns zarf podinfo-flavor", " ") + _, _, err = e2e.UDS(cmd...) + require.NoError(t, err) + }) + t.Run("Test dev deploy with global flavor", func(t *testing.T) { + bundleDir := "src/test/bundles/15-dev-deploy" + + cmd := strings.Split(fmt.Sprintf("dev deploy %s --flavor %s --force-create", bundleDir, "patchVersion3"), " ") + _, _, err := e2e.UDS(cmd...) + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl get deployment -n podinfo-flavor podinfo -o=jsonpath='{.spec.template.spec.containers[0].image}'", " ") + ref, _, err := e2e.UDS(cmd...) + require.Contains(t, ref, "ghcr.io/stefanprodan/podinfo:6.6.3") + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl delete ns zarf podinfo-flavor", " ") + _, _, err = e2e.UDS(cmd...) + require.NoError(t, err) + }) + + t.Run("Test dev deploy with flavor and force create", func(t *testing.T) { + + bundleDir := "src/test/bundles/15-dev-deploy" + + // create flavor patchVersion3 podinfo-flavor package + pkgDir := "src/test/packages/podinfo" + cmd := strings.Split(fmt.Sprintf("zarf package create %s --flavor %s --confirm -o %s", pkgDir, "patchVersion3", pkgDir), " ") + _, _, err := e2e.UDS(cmd...) + require.NoError(t, err) + + // dev deploy with flavor patchVersion2 and --force-create + cmd = strings.Split(fmt.Sprintf("dev deploy %s --flavor %s --force-create", bundleDir, "podinfo=patchVersion2"), " ") + _, _, err = e2e.UDS(cmd...) + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl get deployment -n podinfo-flavor podinfo -o=jsonpath='{.spec.template.spec.containers[0].image}'", " ") + ref, _, err := e2e.UDS(cmd...) + // assert that podinfo package with flavor patchVersion2 was deployed. + require.Contains(t, ref, "ghcr.io/stefanprodan/podinfo:6.6.2") + require.NoError(t, err) + + cmd = strings.Split("zarf tools kubectl delete ns zarf podinfo-flavor", " ") + _, _, err = e2e.UDS(cmd...) + require.NoError(t, err) + }) t.Run("Test dev deploy with remote bundle", func(t *testing.T) { bundle := "oci://ghcr.io/defenseunicorns/packages/uds-cli/test/publish/ghcr-test:0.0.1" devDeploy(t, bundle) + cmd := strings.Split("zarf tools kubectl get deployments -A -o=jsonpath='{.items[*].metadata.name}'", " ") deployments, _, _ := e2e.UDS(cmd...) require.Contains(t, deployments, "podinfo") require.Contains(t, deployments, "nginx") @@ -66,7 +145,7 @@ func TestDevDeploy(t *testing.T) { t.Run("Test dev deploy with --set flag", func(t *testing.T) { bundleDir := "src/test/bundles/02-variables" bundleTarballPath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-variables-%s-0.0.1.tar.zst", e2e.Arch)) - _, stderr := runCmd(t, "dev deploy "+bundleDir+" --set ANIMAL=Longhorns --set COUNTRY=Texas --confirm -l=debug") + _, stderr := runCmd(t, "dev deploy "+bundleDir+" --set ANIMAL=Longhorns --set COUNTRY=Texas -l=debug") require.Contains(t, stderr, "This fun-fact was imported: Longhorns are the national animal of Texas") require.NotContains(t, stderr, "This fun-fact was imported: Unicorns are the national animal of Scotland") remove(t, bundleTarballPath) diff --git a/src/test/packages/nginx/refs/deployment.yaml b/src/test/packages/nginx/refs/deployment.yaml new file mode 100644 index 00000000..30bd1048 --- /dev/null +++ b/src/test/packages/nginx/refs/deployment.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + selector: + matchLabels: + app: nginx + replicas: 2 # tells deployment to run 2 pods matching the template + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.26.0 + ports: + - containerPort: 80 diff --git a/src/test/packages/nginx/refs/zarf.yaml b/src/test/packages/nginx/refs/zarf.yaml new file mode 100644 index 00000000..6982cd7f --- /dev/null +++ b/src/test/packages/nginx/refs/zarf.yaml @@ -0,0 +1,29 @@ +kind: ZarfPackageConfig +metadata: + name: nginx + version: 0.0.2 + description: nginx deployment using image docker.io/library/nginx:1.26.0 for testing dev deploy --refs flag + +components: + - name: nginx-remote + required: true + manifests: + - name: simple-nginx-deployment + namespace: nginx + files: + - deployment.yaml + actions: + onDeploy: + # the following checks were computed by viewing the success state of the package deployment + # and creating `wait` actions that match + after: + - wait: + cluster: + kind: deployment + name: nginx-deployment + namespace: nginx + condition: available + # image discovery is supported in all manifests and charts using: + # zarf prepare find-images + images: + - docker.io/library/nginx:1.26.0 diff --git a/src/test/packages/podinfo/flavors/zarf.yaml b/src/test/packages/podinfo/flavors/zarf.yaml new file mode 100644 index 00000000..26a4de05 --- /dev/null +++ b/src/test/packages/podinfo/flavors/zarf.yaml @@ -0,0 +1,49 @@ +kind: ZarfPackageConfig +metadata: + name: podinfo + version: 0.0.1 + +components: + - name: podinfo-flavor + required: true + only: + flavor: patchVersion2 + charts: + - name: podinfo + version: 6.6.2 + namespace: podinfo-flavor + url: https://github.com/stefanprodan/podinfo.git + gitPath: charts/podinfo + images: + - ghcr.io/stefanprodan/podinfo:6.6.2 + actions: + onDeploy: + after: + - wait: + cluster: + kind: deployment + name: podinfo + namespace: podinfo-flavor + condition: available + + - name: podinfo-flavor + required: true + only: + flavor: patchVersion3 + charts: + - name: podinfo + version: 6.6.3 + namespace: podinfo-flavor + url: https://github.com/stefanprodan/podinfo.git + gitPath: charts/podinfo + images: + - ghcr.io/stefanprodan/podinfo:6.6.3 + actions: + onDeploy: + after: + - wait: + cluster: + kind: deployment + name: podinfo + namespace: podinfo-flavor + condition: available diff --git a/src/test/packages/podinfo/refs/zarf.yaml b/src/test/packages/podinfo/refs/zarf.yaml new file mode 100644 index 00000000..691a3647 --- /dev/null +++ b/src/test/packages/podinfo/refs/zarf.yaml @@ -0,0 +1,27 @@ +kind: ZarfPackageConfig +metadata: + name: podinfo + version: 0.0.2 + description: podinfo deployment using image ghcr.io/stefanprodan/podinfo:6.6.2 for testing dev deploy --refs flag + + +components: + - name: podinfo + required: true + charts: + - name: podinfo + version: 6.6.2 + namespace: podinfo + url: https://github.com/stefanprodan/podinfo.git + gitPath: charts/podinfo + images: + - ghcr.io/stefanprodan/podinfo:6.6.2 + actions: + onDeploy: + after: + - wait: + cluster: + kind: deployment + name: podinfo + namespace: podinfo + condition: available diff --git a/src/types/bundle.go b/src/types/bundle.go index 358ba78e..7d3283be 100644 --- a/src/types/bundle.go +++ b/src/types/bundle.go @@ -36,6 +36,7 @@ type Package struct { 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"` + Flavor string `json:"flavor,omitempty" jsonschema:"description=Flavor of the Zarf package"` OptionalComponents []string `json:"optionalComponents,omitempty" jsonschema:"description=List of optional components to include from the package (required components are always included)"` PublicKey string `json:"publicKey,omitempty" jsonschema:"description=The public key to use to verify the package"` Imports []BundleVariableImport `json:"imports,omitempty" jsonschema:"description=List of Zarf variables to import from another Zarf package"` diff --git a/src/types/options.go b/src/types/options.go index e060bff3..bd1b5591 100644 --- a/src/types/options.go +++ b/src/types/options.go @@ -6,12 +6,13 @@ package types // BundleConfig is the main struct that the bundler uses to hold high-level options. type BundleConfig struct { - CreateOpts BundleCreateOptions - DeployOpts BundleDeployOptions - PublishOpts BundlePublishOptions - PullOpts BundlePullOptions - InspectOpts BundleInspectOptions - RemoveOpts BundleRemoveOptions + CreateOpts BundleCreateOptions + DeployOpts BundleDeployOptions + PublishOpts BundlePublishOptions + PullOpts BundlePullOptions + InspectOpts BundleInspectOptions + RemoveOpts BundleRemoveOptions + DevDeployOpts BundleDevDeployOptions } // BundleCreateOptions is the options for the bundler.Create() function @@ -75,6 +76,14 @@ type BundleCommonOptions struct { OCIConcurrency int `jsonschema:"description=Number of concurrent layer operations to perform when interacting with a remote package"` } +// BundleDevDeployOptions are the options for when doing a dev deploy +type BundleDevDeployOptions struct { + FlavorInput string + Flavor map[string]string + ForceCreate bool + Ref map[string]string +} + // PathMap is a map of either absolute paths to relative paths or relative paths to absolute paths // used to map filenames during local bundle tarball creation type PathMap map[string]string diff --git a/uds.schema.json b/uds.schema.json index 8ca65ba7..0ed9c07f 100644 --- a/uds.schema.json +++ b/uds.schema.json @@ -151,6 +151,10 @@ "type": "string", "description": "Ref (tag) of the Zarf package" }, + "flavor": { + "type": "string", + "description": "Flavor of the Zarf package" + }, "optionalComponents": { "items": { "type": "string"