From 6d2378d646507f6648e7a3047f0bc3fce93abc75 Mon Sep 17 00:00:00 2001 From: Lucas Rodriguez Date: Tue, 19 Mar 2024 13:19:19 -0500 Subject: [PATCH 1/2] fix(regression): populate `p.sbomViewFiles` on `deploy` and `mirror` (#2386) ## Description Refactoring that took place in https://github.com/defenseunicorns/zarf/pull/2223 introduced a bug on `deploy` where the user is not prompted to view SBOMs. This occurs because `p.sbomViewFiles` is not being populated correctly. This PR fixes this issue by populating `p.sbomViewFiles`. ## Related Issue N/A ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Other (security config, docs update, etc) ## Checklist before merging - [ ] Test, docs, adr added or updated as needed - [ ] [Contributor Guide Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow) followed --- src/pkg/layout/sbom.go | 11 ++++++----- src/pkg/packager/deploy.go | 3 ++- src/pkg/packager/mirror.go | 3 ++- src/test/e2e/20_zarf_init_test.go | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pkg/layout/sbom.go b/src/pkg/layout/sbom.go index 280da017b8..7ea416317c 100644 --- a/src/pkg/layout/sbom.go +++ b/src/pkg/layout/sbom.go @@ -69,15 +69,16 @@ func (s *SBOMs) Archive() (err error) { } // StageSBOMViewFiles copies SBOM viewer HTML files to the Zarf SBOM directory. -func (s *SBOMs) StageSBOMViewFiles() (warnings []string, err error) { +func (s *SBOMs) StageSBOMViewFiles() (sbomViewFiles, warnings []string, err error) { if s.IsTarball() { - return nil, fmt.Errorf("unable to process the SBOM files for this package: %s is a tarball", s.Path) + return nil, nil, fmt.Errorf("unable to process the SBOM files for this package: %s is a tarball", s.Path) } // If SBOMs were loaded, temporarily place them in the deploy directory if !helpers.InvalidPath(s.Path) { - if _, err := filepath.Glob(filepath.Join(s.Path, "sbom-viewer-*")); err != nil { - return nil, err + sbomViewFiles, err = filepath.Glob(filepath.Join(s.Path, "sbom-viewer-*")) + if err != nil { + return nil, nil, err } if _, err := s.OutputSBOMFiles(SBOMDir, ""); err != nil { @@ -87,7 +88,7 @@ func (s *SBOMs) StageSBOMViewFiles() (warnings []string, err error) { } } - return warnings, nil + return sbomViewFiles, warnings, nil } // OutputSBOMFiles outputs SBOM files into outputDir. diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 0d0d92e2b6..1bef15963e 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -54,7 +54,8 @@ func (p *Packager) Deploy() (err error) { return err } - sbomWarnings, err := p.layout.SBOMs.StageSBOMViewFiles() + var sbomWarnings []string + p.sbomViewFiles, sbomWarnings, err = p.layout.SBOMs.StageSBOMViewFiles() if err != nil { return err } diff --git a/src/pkg/packager/mirror.go b/src/pkg/packager/mirror.go index 6dabec8a7f..13122be7a6 100644 --- a/src/pkg/packager/mirror.go +++ b/src/pkg/packager/mirror.go @@ -27,7 +27,8 @@ func (p *Packager) Mirror() (err error) { return err } - sbomWarnings, err := p.layout.SBOMs.StageSBOMViewFiles() + var sbomWarnings []string + p.sbomViewFiles, sbomWarnings, err = p.layout.SBOMs.StageSBOMViewFiles() if err != nil { return err } diff --git a/src/test/e2e/20_zarf_init_test.go b/src/test/e2e/20_zarf_init_test.go index d8ae669721..d4cd716d6a 100644 --- a/src/test/e2e/20_zarf_init_test.go +++ b/src/test/e2e/20_zarf_init_test.go @@ -69,6 +69,7 @@ func TestZarfInit(t *testing.T) { _, initStdErr, err := e2e.Zarf("init", "--components="+initComponents, "--nodeport", "31337", "-l", "trace", "--confirm") require.NoError(t, err) require.Contains(t, initStdErr, "an inventory of all software contained in this package") + require.NotContains(t, initStdErr, "This package does NOT contain an SBOM. If you require an SBOM, please contact the creator of this package to request a version that includes an SBOM.") logText := e2e.GetLogFileContents(t, e2e.StripMessageFormatting(initStdErr)) From 95c42ffff467ee6a53b6cd0f581a72bdf2890ec8 Mon Sep 17 00:00:00 2001 From: razzle Date: Thu, 21 Mar 2024 14:15:15 -0500 Subject: [PATCH 2/2] feat!: filter package components with strategy interface (#2321) ## Description Consolidate component filtering logic into a `filters` package. Each filter is an implementation of ```go // ComponentFilterStrategy is a strategy interface for filtering components. type ComponentFilterStrategy interface { Apply(types.ZarfPackage) ([]types.ZarfComponent, error) } ``` Public construction functions return instances of this interface _not_ instances of their underlying structs. Consumers should be fully cognizant of which filter they are using, and should not be making a common wrapper function (eg. `NewFilter(args...)` to dynamically return a filter. ex: ```go func Empty() ComponentFilterStrategy { return &emptyFilter{} } // emptyFilter is a filter that does nothing. type emptyFilter struct{} // Apply returns the components unchanged. func (f *emptyFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, error) { return pkg.Components, nil } ``` BREAKING CHANGES: This changes the interface signatures on `sources.PackageSource` to reflect the new behavior whereupon the `zarf.yaml` is loaded into memory within the sources load operations. This allows for `filter`ing to take place during load and for the `zarf.yaml` in memory to reflect these filter operations. ## Related Issue Fixes #2320 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Other (security config, docs update, etc) ## Checklist before merging - [x] Test, docs, adr added or updated as needed - [x] [Contributor Guide Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow) followed --------- Signed-off-by: razzle Co-authored-by: Wayne Starr Co-authored-by: Lucas Rodriguez Co-authored-by: Lucas Rodriguez --- .github/workflows/nightly-eks.yml | 4 +- .../package_deploy_wordpress_suggestions.html | 2 +- docs/3-create-a-zarf-package/4-zarf-schema.md | 364 +++++++++--------- go.mod | 2 +- packages/README.md | 14 +- packages/distros/eks/zarf.yaml | 1 - src/config/config.go | 20 +- src/config/lang/english.go | 9 +- src/internal/packager/validate/validate.go | 31 +- src/pkg/interactive/components.go | 38 +- src/pkg/interactive/prompt.go | 24 +- src/pkg/layout/package.go | 15 +- src/pkg/packager/common.go | 4 +- src/pkg/packager/components.go | 227 ----------- src/pkg/packager/composer/list.go | 12 +- src/pkg/packager/composer/override.go | 4 +- src/pkg/packager/create.go | 6 + src/pkg/packager/creator/differential.go | 8 +- src/pkg/packager/creator/normal.go | 8 +- src/pkg/packager/creator/skeleton.go | 12 +- src/pkg/packager/deploy.go | 57 ++- src/pkg/packager/deprecated/common.go | 2 +- src/pkg/packager/dev.go | 34 +- src/pkg/packager/filters/deploy.go | 189 +++++++++ src/pkg/packager/filters/deploy_test.go | 230 +++++++++++ src/pkg/packager/filters/empty.go | 20 + src/pkg/packager/filters/empty_test.go | 32 ++ src/pkg/packager/filters/os.go | 39 ++ src/pkg/packager/filters/os_test.go | 40 ++ src/pkg/packager/filters/select.go | 51 +++ src/pkg/packager/filters/select_test.go | 92 +++++ src/pkg/packager/filters/strat.go | 41 ++ src/pkg/packager/filters/strat_test.go | 59 +++ src/pkg/packager/filters/utils.go | 55 +++ src/pkg/packager/filters/utils_test.go | 87 +++++ src/pkg/packager/generate.go | 2 +- src/pkg/packager/inspect.go | 6 +- src/pkg/packager/interactive.go | 2 +- src/pkg/packager/lint/lint.go | 30 +- src/pkg/packager/mirror.go | 30 +- src/pkg/packager/prepare.go | 5 + src/pkg/packager/publish.go | 21 +- src/pkg/packager/remove.go | 27 +- src/pkg/packager/sources/cluster.go | 16 +- src/pkg/packager/sources/new.go | 9 +- src/pkg/packager/sources/oci.go | 69 ++-- src/pkg/packager/sources/split.go | 11 +- src/pkg/packager/sources/tarball.go | 56 +-- src/pkg/packager/sources/url.go | 15 +- src/pkg/packager/sources/utils.go | 24 +- src/pkg/utils/helpers/misc.go | 5 + src/pkg/utils/helpers/misc_test.go | 24 +- src/pkg/zoci/pull.go | 25 +- src/test/e2e/00_use_cli_test.go | 2 +- src/test/e2e/01_component_choice_test.go | 2 +- src/types/component.go | 77 ++-- zarf.schema.json | 62 +-- 57 files changed, 1530 insertions(+), 823 deletions(-) delete mode 100644 src/pkg/packager/components.go create mode 100644 src/pkg/packager/filters/deploy.go create mode 100644 src/pkg/packager/filters/deploy_test.go create mode 100644 src/pkg/packager/filters/empty.go create mode 100644 src/pkg/packager/filters/empty_test.go create mode 100644 src/pkg/packager/filters/os.go create mode 100644 src/pkg/packager/filters/os_test.go create mode 100644 src/pkg/packager/filters/select.go create mode 100644 src/pkg/packager/filters/select_test.go create mode 100644 src/pkg/packager/filters/strat.go create mode 100644 src/pkg/packager/filters/strat_test.go create mode 100644 src/pkg/packager/filters/utils.go create mode 100644 src/pkg/packager/filters/utils_test.go diff --git a/.github/workflows/nightly-eks.yml b/.github/workflows/nightly-eks.yml index 5ec53f5a7a..c4bee3432f 100644 --- a/.github/workflows/nightly-eks.yml +++ b/.github/workflows/nightly-eks.yml @@ -48,7 +48,7 @@ jobs: - name: Deploy the eks package run: | - ./build/zarf package deploy build/zarf-package-distro-eks-multi-0.0.3.tar.zst \ + ./build/zarf package deploy build/zarf-package-distro-eks-amd64-0.0.3.tar.zst \ --components=deploy-eks-cluster \ --set=EKS_CLUSTER_NAME=${{ inputs.cluster_name || 'zarf-nightly-eks-e2e-test' }} \ --set=EKS_INSTANCE_TYPE=${{ inputs.instance_type || 't3.medium' }} \ @@ -60,7 +60,7 @@ jobs: - name: Teardown the cluster if: always() run: | - ./build/zarf package deploy build/zarf-package-distro-eks-multi-0.0.3.tar.zst \ + ./build/zarf package deploy build/zarf-package-distro-eks-amd64-0.0.3.tar.zst \ --components=teardown-eks-cluster \ --set=EKS_CLUSTER_NAME=${{ inputs.cluster_name || 'zarf-nightly-eks-e2e-test' }} \ --confirm diff --git a/docs-website/static/docs/tutorials/package_deploy_wordpress_suggestions.html b/docs-website/static/docs/tutorials/package_deploy_wordpress_suggestions.html index 5e37e51d12..f9326440b2 100644 --- a/docs-website/static/docs/tutorials/package_deploy_wordpress_suggestions.html +++ b/docs-website/static/docs/tutorials/package_deploy_wordpress_suggestions.html @@ -50,7 +50,7 @@ Saving log file to /var/folders/bk/rz1xx2sd5zn134c0_j1s2n5r0000gp/T/zarf-2023-03-23-13-18-54-4086179855.log ? Choose or type the package file zarf-package-helm-oci-chart-arm64-0.0.1.tar.zst[tab for suggestions] - zarf-package-distro-eks-multi.tar.zst + zarf-package-distro-eks-amd64.tar.zst > zarf-package-wordpress-amd64-16.0.4.tar.zst diff --git a/docs/3-create-a-zarf-package/4-zarf-schema.md b/docs/3-create-a-zarf-package/4-zarf-schema.md index 0ca39289a3..e4ff7eb157 100644 --- a/docs/3-create-a-zarf-package/4-zarf-schema.md +++ b/docs/3-create-a-zarf-package/4-zarf-schema.md @@ -823,14 +823,14 @@ Must be one of:
- files + manifests  
- ## components > files + ## components > manifests -**Description:** Files or folders to place on disk during package deployment +**Description:** Kubernetes manifests to be included in a generated Helm chart on package deploy | | | | -------- | ------- | @@ -841,24 +841,24 @@ Must be one of: ![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) ![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) - ### ZarfFile + ### ZarfManifest | | | | ------------------------- | -------------------------------------------------------------------------------------------------------- | | **Type** | `object` | | **Additional properties** | [![Not allowed](https://img.shields.io/badge/Not%20allowed-red)](# "Additional Properties not allowed.") | -| **Defined in** | #/definitions/ZarfFile | +| **Defined in** | #/definitions/ZarfManifest |
- source * + name *  
![Required](https://img.shields.io/badge/Required-red) -**Description:** Local folder or file path or remote URL to pull into the package +**Description:** A name to give this collection of manifests; this will become the name of the dynamically-created helm chart | | | | -------- | -------- | @@ -869,12 +869,12 @@ Must be one of:
- shasum + namespace  
-**Description:** (files only) Optional SHA256 checksum of the file +**Description:** The namespace to deploy the manifests to | | | | -------- | -------- | @@ -885,14 +885,23 @@ Must be one of:
- target * + files  
-![Required](https://img.shields.io/badge/Required-red) +**Description:** List of local K8s YAML files or remote URLs to deploy (in order) -**Description:** The absolute or relative path where the file or folder should be copied to during package deploy +| | | +| -------- | ----------------- | +| **Type** | `array of string` | + +![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) +![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) +![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) +![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) + + ### files items | | | | -------- | -------- | @@ -903,12 +912,12 @@ Must be one of:
- executable + kustomizeAllowAnyDirectory  
-**Description:** (files only) Determines if the file should be made executable during package deploy +**Description:** Allow traversing directory above the current directory if needed for kustomization | | | | -------- | --------- | @@ -919,12 +928,12 @@ Must be one of:
- symlinks + kustomizations  
-**Description:** List of symlinks to create during package deploy +**Description:** List of local kustomization paths or remote URLs to include in the package | | | | -------- | ----------------- | @@ -935,7 +944,7 @@ Must be one of: ![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) ![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) - ### symlinks items + ### kustomizations items | | | | -------- | -------- | @@ -946,16 +955,16 @@ Must be one of:
- extractPath + noWait  
-**Description:** Local folder or file to be extracted from a 'source' archive +**Description:** Whether to not wait for manifest resources to be ready before continuing -| | | -| -------- | -------- | -| **Type** | `string` | +| | | +| -------- | --------- | +| **Type** | `boolean` |
@@ -983,7 +992,7 @@ Must be one of: ![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) ![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) - ### ZarfChart + ### ZarfChart | | | | ------------------------- | -------------------------------------------------------------------------------------------------------- | @@ -1167,7 +1176,7 @@ Must be one of: ![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) ![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) - ### valuesFiles items + ### valuesFiles items | | | | -------- | -------- | @@ -1181,14 +1190,14 @@ Must be one of:
- manifests + dataInjections  
- ## components > manifests + ## components > dataInjections -**Description:** Kubernetes manifests to be included in a generated Helm chart on package deploy +**Description:** Datasets to inject into a container in the target cluster | | | | -------- | ------- | @@ -1199,24 +1208,24 @@ Must be one of: ![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) ![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) - ### ZarfManifest + ### ZarfDataInjection | | | | ------------------------- | -------------------------------------------------------------------------------------------------------- | | **Type** | `object` | | **Additional properties** | [![Not allowed](https://img.shields.io/badge/Not%20allowed-red)](# "Additional Properties not allowed.") | -| **Defined in** | #/definitions/ZarfManifest | +| **Defined in** | #/definitions/ZarfDataInjection |
- name * + source *  
![Required](https://img.shields.io/badge/Required-red) -**Description:** A name to give this collection of manifests; this will become the name of the dynamically-created helm chart +**Description:** Either a path to a local folder/file or a remote URL of a file to inject into the given target pod + container | | | | -------- | -------- | @@ -1225,41 +1234,34 @@ Must be one of:
-
+
- namespace + target *  
-**Description:** The namespace to deploy the manifests to + ## components > dataInjections > target +![Required](https://img.shields.io/badge/Required-red) -| | | -| -------- | -------- | -| **Type** | `string` | +**Description:** The target pod + container to inject the data into -
-
+| | | +| ------------------------- | -------------------------------------------------------------------------------------------------------- | +| **Type** | `object` | +| **Additional properties** | [![Not allowed](https://img.shields.io/badge/Not%20allowed-red)](# "Additional Properties not allowed.") | +| **Defined in** | #/definitions/ZarfContainerTarget |
- files + namespace *  
-**Description:** List of local K8s YAML files or remote URLs to deploy (in order) - -| | | -| -------- | ----------------- | -| **Type** | `array of string` | - -![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) -![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) -![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) -![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) +![Required](https://img.shields.io/badge/Required-red) - ### files items +**Description:** The namespace to target for data injection | | | | -------- | -------- | @@ -1270,85 +1272,55 @@ Must be one of:
- kustomizeAllowAnyDirectory - -  -
- -**Description:** Allow traversing directory above the current directory if needed for kustomization - -| | | -| -------- | --------- | -| **Type** | `boolean` | - -
-
- -
- - kustomizations + selector *  
-**Description:** List of local kustomization paths or remote URLs to include in the package - -| | | -| -------- | ----------------- | -| **Type** | `array of string` | - -![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) -![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) -![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) -![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) +![Required](https://img.shields.io/badge/Required-red) - ### kustomizations items +**Description:** The K8s selector to target for data injection | | | | -------- | -------- | | **Type** | `string` | +**Example:** + + +"app=data-injection" +
- noWait + container *  
-**Description:** Whether to not wait for manifest resources to be ready before continuing +![Required](https://img.shields.io/badge/Required-red) -| | | -| -------- | --------- | -| **Type** | `boolean` | +**Description:** The container name to target for data injection -
-
+| | | +| -------- | -------- | +| **Type** | `string` |
- images + path *  
-**Description:** List of OCI images to include in the package - -| | | -| -------- | ----------------- | -| **Type** | `array of string` | - -![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) -![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) -![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) -![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) +![Required](https://img.shields.io/badge/Required-red) - ### images items +**Description:** The path within the container to copy the data into | | | | -------- | -------- | @@ -1357,43 +1329,38 @@ Must be one of:
+
+
+
- repos + compress  
-**Description:** List of git repos to include in the package - -| | | -| -------- | ----------------- | -| **Type** | `array of string` | - -![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) -![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) -![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) -![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) +**Description:** Compress the data before transmitting using gzip. Note: this requires support for tar/gzip locally and in the target image. - ### repos items +| | | +| -------- | --------- | +| **Type** | `boolean` | -| | | -| -------- | -------- | -| **Type** | `string` | +
+
- dataInjections + files  
- ## components > dataInjections + ## components > files -**Description:** Datasets to inject into a container in the target cluster +**Description:** Files or folders to place on disk during package deployment | | | | -------- | ------- | @@ -1404,24 +1371,24 @@ Must be one of: ![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) ![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) - ### ZarfDataInjection + ### ZarfFile | | | | ------------------------- | -------------------------------------------------------------------------------------------------------- | | **Type** | `object` | | **Additional properties** | [![Not allowed](https://img.shields.io/badge/Not%20allowed-red)](# "Additional Properties not allowed.") | -| **Defined in** | #/definitions/ZarfDataInjection | +| **Defined in** | #/definitions/ZarfFile |
- source * + source *  
![Required](https://img.shields.io/badge/Required-red) -**Description:** Either a path to a local folder/file or a remote URL of a file to inject into the given target pod + container +**Description:** Local folder or file path or remote URL to pull into the package | | | | -------- | -------- | @@ -1430,34 +1397,32 @@ Must be one of:
-
+
- target * + shasum  
- ## components > dataInjections > target -![Required](https://img.shields.io/badge/Required-red) +**Description:** (files only) Optional SHA256 checksum of the file -**Description:** The target pod + container to inject the data into +| | | +| -------- | -------- | +| **Type** | `string` | -| | | -| ------------------------- | -------------------------------------------------------------------------------------------------------- | -| **Type** | `object` | -| **Additional properties** | [![Not allowed](https://img.shields.io/badge/Not%20allowed-red)](# "Additional Properties not allowed.") | -| **Defined in** | #/definitions/ZarfContainerTarget | +
+
- namespace * + target *  
![Required](https://img.shields.io/badge/Required-red) -**Description:** The namespace to target for data injection +**Description:** The absolute or relative path where the file or folder should be copied to during package deploy | | | | -------- | -------- | @@ -1468,37 +1433,39 @@ Must be one of:
- selector * + executable  
-![Required](https://img.shields.io/badge/Required-red) - -**Description:** The K8s selector to target for data injection - -| | | -| -------- | -------- | -| **Type** | `string` | - -**Example:** +**Description:** (files only) Determines if the file should be made executable during package deploy - -"app=data-injection" +| | | +| -------- | --------- | +| **Type** | `boolean` |
- container * + symlinks  
-![Required](https://img.shields.io/badge/Required-red) +**Description:** List of symlinks to create during package deploy -**Description:** The container name to target for data injection +| | | +| -------- | ----------------- | +| **Type** | `array of string` | + +![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) +![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) +![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) +![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) + + ### symlinks items | | | | -------- | -------- | @@ -1509,14 +1476,12 @@ Must be one of:
- path * + extractPath  
-![Required](https://img.shields.io/badge/Required-red) - -**Description:** The path within the container to copy the data into +**Description:** Local folder or file to be extracted from a 'source' archive | | | | -------- | -------- | @@ -1530,20 +1495,55 @@ Must be one of:
- compress + images  
-**Description:** Compress the data before transmitting using gzip. Note: this requires support for tar/gzip locally and in the target image. +**Description:** List of OCI images to include in the package -| | | -| -------- | --------- | -| **Type** | `boolean` | +| | | +| -------- | ----------------- | +| **Type** | `array of string` | + +![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) +![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) +![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) +![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) + + ### images items + +| | | +| -------- | -------- | +| **Type** | `string` |
+
+ + repos + +  +
+ +**Description:** List of git repos to include in the package + +| | | +| -------- | ----------------- | +| **Type** | `array of string` | + +![Min Items: N/A](https://img.shields.io/badge/Min%20Items%3A%20N/A-gold) +![Max Items: N/A](https://img.shields.io/badge/Max%20Items%3A%20N/A-gold) +![Item unicity: False](https://img.shields.io/badge/Item%20unicity%3A%20False-gold) +![Additional items: N/A](https://img.shields.io/badge/Additional%20items%3A%20N/A-gold) + + ### repos items + +| | | +| -------- | -------- | +| **Type** | `string` | +
@@ -2126,70 +2126,70 @@ Must be one of:
- sensitive + type  
-**Description:** Whether to mark this variable as sensitive to not print it in the Zarf log +**Description:** Changes the handling of a variable to load contents differently (i.e. from a file rather than as a raw variable - templated files should be kept below 1 MiB) -| | | -| -------- | --------- | -| **Type** | `boolean` | +| | | +| -------- | ------------------ | +| **Type** | `enum (of string)` | + +:::note +Must be one of: +* "raw" +* "file" +:::
- autoIndent + pattern  
-**Description:** Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_. +**Description:** An optional regex pattern that a variable value must match before a package deployment can continue. -| | | -| -------- | --------- | -| **Type** | `boolean` | +| | | +| -------- | -------- | +| **Type** | `string` |
- pattern + sensitive  
-**Description:** An optional regex pattern that a variable value must match before a package deployment can continue. +**Description:** Whether to mark this variable as sensitive to not print it in the Zarf log -| | | -| -------- | -------- | -| **Type** | `string` | +| | | +| -------- | --------- | +| **Type** | `boolean` |
- type + autoIndent  
-**Description:** Changes the handling of a variable to load contents differently (i.e. from a file rather than as a raw variable - templated files should be kept below 1 MiB) - -| | | -| -------- | ------------------ | -| **Type** | `enum (of string)` | +**Description:** Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_. -:::note -Must be one of: -* "raw" -* "file" -::: +| | | +| -------- | --------- | +| **Type** | `boolean` |
diff --git a/go.mod b/go.mod index 11962071d1..f3df371a78 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( cuelang.org/go v0.7.0 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Masterminds/semver/v3 v3.2.1 + github.com/agnivade/levenshtein v1.1.1 github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b github.com/anchore/clio v0.0.0-20240301210832-abcb7197da84 github.com/anchore/stereoscope v0.0.1 @@ -108,7 +109,6 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/adrg/xdg v0.4.0 // indirect - github.com/agnivade/levenshtein v1.1.1 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect diff --git a/packages/README.md b/packages/README.md index c5a6d1cd07..cac31b8501 100644 --- a/packages/README.md +++ b/packages/README.md @@ -3,11 +3,13 @@ This folder contains packages maintained by the [Zarf team](https://github.com/defenseunicorns/zarf/graphs/contributors). Some of these packages are used by `zarf init` for new cluster initialization. **Packages** -- [distros](#distros) -- [gitea](#gitea) -- [logging-pgl](#logging-pgl) -- [zarf-agent](#zarf-agent) -- [zarf-registry](#zarf-registry) +- [Zarf Packages](#zarf-packages) + - [Distros](#distros) + - [Usage Examples](#usage-examples) + - [Gitea](#gitea) + - [Logging PGL](#logging-pgl) + - [Zarf Agent](#zarf-agent) + - [Zarf Registry](#zarf-registry) ### Distros @@ -27,7 +29,7 @@ The distros package adds optional capabilities for spinning up and tearing down ```bash zarf package create packages/distros/eks -o build --confirm -zarf package deploy build/zarf-package-distro-eks-multi-x.x.x.tar.zst --components=deploy-eks-cluster --set=CLUSTER_NAME='zarf-nightly-eks-e2e-test',INSTANCE_TYPE='t3.medium' --confirm +zarf package deploy build/zarf-package-distro-eks-amd64-x.x.x.tar.zst --components=deploy-eks-cluster --set=CLUSTER_NAME='zarf-nightly-eks-e2e-test',INSTANCE_TYPE='t3.medium' --confirm ``` See the [nightly-eks test](../.github/workflows/nightly-eks.yml) for another example. diff --git a/packages/distros/eks/zarf.yaml b/packages/distros/eks/zarf.yaml index 2171feaff4..8172dfdf60 100644 --- a/packages/distros/eks/zarf.yaml +++ b/packages/distros/eks/zarf.yaml @@ -2,7 +2,6 @@ kind: ZarfPackageConfig metadata: name: distro-eks description: Deploy a EKS K8s cluster - architecture: multi version: 0.0.3 variables: diff --git a/src/config/config.go b/src/config/config.go index 3a1af31141..b8e3e2d386 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -52,12 +52,14 @@ const ( ZarfGitServerSecretName = "private-git-server" ZarfLoggingUser = "zarf-admin" + + UnsetCLIVersion = "unset-development-only" ) // Zarf Global Configuration Variables. var ( // CLIVersion track the version of the CLI - CLIVersion = "unset" + CLIVersion = UnsetCLIVersion // ActionsUseSystemZarf sets whether to use Zarf from the system path if Zarf is being used as a library ActionsUseSystemZarf = false @@ -157,22 +159,6 @@ func GetCraneAuthOption(username string, secret string) crane.Option { })) } -// GetValidPackageExtensions returns the valid package extensions. -func GetValidPackageExtensions() [2]string { - return [...]string{".tar.zst", ".tar"} -} - -// IsValidFileExtension returns true if the filename has a valid package extension. -func IsValidFileExtension(filename string) bool { - for _, extension := range GetValidPackageExtensions() { - if strings.HasSuffix(filename, extension) { - return true - } - } - - return false -} - // GetAbsCachePath gets the absolute cache path for images and git repos. func GetAbsCachePath() string { return GetAbsHomePath(CommonOptions.CachePath) diff --git a/src/config/lang/english.go b/src/config/lang/english.go index d919da0f9b..99fdab80e3 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -639,14 +639,6 @@ const ( PkgCreateErrDifferentialNoVersion = "unable to create differential package. Please ensure both package versions are set" ) -// Package deploy -const ( - PkgDeployErrMultipleComponentsSameGroup = "You cannot specify multiple components (%q, %q) within the same group (%q) when using the --components flag." - PkgDeployErrNoDefaultOrSelection = "You must make a selection from %q with the --components flag as there is no default in their group." - PkgDeployErrNoCompatibleComponentsForSelection = "No compatible components found that matched %q. Please check spelling and try again." - PkgDeployErrComponentSelectionCanceled = "Component selection canceled: %s" -) - // Package validate const ( PkgValidateTemplateDeprecation = "Package template %q is using the deprecated syntax ###ZARF_PKG_VAR_%s###. This will be removed in Zarf v1.0.0. Please update to ###ZARF_PKG_TMPL_%s###." @@ -663,6 +655,7 @@ const ( PkgValidateErrChartURLOrPath = "chart %q must have either a url or localPath" PkgValidateErrChartVersion = "chart %q must include a chart version" PkgValidateErrComponentName = "component name %q must be all lowercase and contain no special characters except '-' and cannot start with a '-'" + PkgValidateErrComponentLocalOS = "component %q contains a localOS value that is not supported: %s (supported: %s)" PkgValidateErrComponentNameNotUnique = "component name %q is not unique" PkgValidateErrComponent = "invalid component %q: %w" PkgValidateErrComponentReqDefault = "component %q cannot be both required and default" diff --git a/src/internal/packager/validate/validate.go b/src/internal/packager/validate/validate.go index dea997b635..fe5577901f 100644 --- a/src/internal/packager/validate/validate.go +++ b/src/internal/packager/validate/validate.go @@ -8,6 +8,7 @@ import ( "fmt" "path/filepath" "regexp" + "slices" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/config/lang" @@ -22,8 +23,20 @@ var ( // IsUppercaseNumberUnderscore is a regex for uppercase, numbers and underscores. // https://regex101.com/r/tfsEuZ/1 IsUppercaseNumberUnderscore = regexp.MustCompile(`^[A-Z0-9_]+$`).MatchString + // Define allowed OS, an empty string means it is allowed on all operating systems + // same as enums on ZarfComponentOnlyTarget + supportedOS = []string{"linux", "darwin", "windows", ""} ) +// SupportedOS returns the supported operating systems. +// +// The supported operating systems are: linux, darwin, windows. +// +// An empty string signifies no OS restrictions. +func SupportedOS() []string { + return supportedOS +} + // Run performs config validations. func Run(pkg types.ZarfPackage) error { if pkg.Kind == types.ZarfInitConfig && pkg.Metadata.YOLO { @@ -62,14 +75,14 @@ func Run(pkg types.ZarfPackage) error { } // ensure groups don't have multiple defaults or only one component - if component.Group != "" { + if component.DeprecatedGroup != "" { if component.Default { - if _, ok := groupDefault[component.Group]; ok { - return fmt.Errorf(lang.PkgValidateErrGroupMultipleDefaults, component.Group, groupDefault[component.Group], component.Name) + if _, ok := groupDefault[component.DeprecatedGroup]; ok { + return fmt.Errorf(lang.PkgValidateErrGroupMultipleDefaults, component.DeprecatedGroup, groupDefault[component.DeprecatedGroup], component.Name) } - groupDefault[component.Group] = component.Name + groupDefault[component.DeprecatedGroup] = component.Name } - groupedComponents[component.Group] = append(groupedComponents[component.Group], component.Name) + groupedComponents[component.DeprecatedGroup] = append(groupedComponents[component.DeprecatedGroup], component.Name) } } @@ -129,11 +142,15 @@ func validateComponent(pkg types.ZarfPackage, component types.ZarfComponent) err return fmt.Errorf(lang.PkgValidateErrComponentName, component.Name) } - if component.Required { + if !slices.Contains(supportedOS, component.Only.LocalOS) { + return fmt.Errorf(lang.PkgValidateErrComponentLocalOS, component.Name, component.Only.LocalOS, supportedOS) + } + + if component.Required != nil && *component.Required { if component.Default { return fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name) } - if component.Group != "" { + if component.DeprecatedGroup != "" { return fmt.Errorf(lang.PkgValidateErrComponentReqGrouped, component.Name) } } diff --git a/src/pkg/interactive/components.go b/src/pkg/interactive/components.go index bb8f244f75..4e24f3f083 100644 --- a/src/pkg/interactive/components.go +++ b/src/pkg/interactive/components.go @@ -6,11 +6,8 @@ package interactive import ( "fmt" - "strings" "github.com/AlecAivazis/survey/v2" - "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/types" @@ -18,12 +15,7 @@ import ( ) // SelectOptionalComponent prompts to confirm optional components -func SelectOptionalComponent(component types.ZarfComponent) (confirmComponent bool) { - // Confirm flag passed, just use defaults - if config.CommonOptions.Confirm { - return component.Default - } - +func SelectOptionalComponent(component types.ZarfComponent) (confirm bool, err error) { message.HorizontalRule() displayComponent := component @@ -37,30 +29,12 @@ func SelectOptionalComponent(component types.ZarfComponent) (confirmComponent bo Message: fmt.Sprintf("Deploy the %s component?", component.Name), Default: component.Default, } - if err := survey.AskOne(prompt, &confirmComponent); err != nil { - message.Fatalf(nil, lang.PkgDeployErrComponentSelectionCanceled, err.Error()) - } - return confirmComponent + return confirm, survey.AskOne(prompt, &confirm) } // SelectChoiceGroup prompts to select component groups -func SelectChoiceGroup(componentGroup []types.ZarfComponent) types.ZarfComponent { - // Confirm flag passed, just use defaults - if config.CommonOptions.Confirm { - var componentNames []string - for _, component := range componentGroup { - // If the component is default, then return it - if component.Default { - return component - } - // Add each component name to the list - componentNames = append(componentNames, component.Name) - } - // If no default component was found, give up - message.Fatalf(nil, lang.PkgDeployErrNoDefaultOrSelection, strings.Join(componentNames, ",")) - } - +func SelectChoiceGroup(componentGroup []types.ZarfComponent) (types.ZarfComponent, error) { message.HorizontalRule() var chosen int @@ -78,9 +52,5 @@ func SelectChoiceGroup(componentGroup []types.ZarfComponent) types.ZarfComponent pterm.Println() - if err := survey.AskOne(prompt, &chosen); err != nil { - message.Fatalf(nil, lang.PkgDeployErrComponentSelectionCanceled, err.Error()) - } - - return componentGroup[chosen] + return componentGroup[chosen], survey.AskOne(prompt, &chosen) } diff --git a/src/pkg/interactive/prompt.go b/src/pkg/interactive/prompt.go index a643cefc37..63aa058f1e 100644 --- a/src/pkg/interactive/prompt.go +++ b/src/pkg/interactive/prompt.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2" - "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/types" ) @@ -17,19 +16,10 @@ import ( func PromptSigPassword() ([]byte, error) { var password string - // If we're in interactive mode, prompt the user for the password to their private key - if !config.CommonOptions.Confirm { - prompt := &survey.Password{ - Message: "Private key password (empty for no password): ", - } - if err := survey.AskOne(prompt, &password); err != nil { - return nil, fmt.Errorf("unable to get password for private key: %w", err) - } - return []byte(password), nil + prompt := &survey.Password{ + Message: "Private key password (empty for no password): ", } - - // We are returning a nil error here because purposefully avoiding a password input is a valid use condition - return nil, nil + return []byte(password), survey.AskOne(prompt, &password) } // PromptVariable prompts the user for a value for a variable @@ -40,13 +30,9 @@ func PromptVariable(variable types.ZarfPackageVariable) (value string, err error } prompt := &survey.Input{ - Message: fmt.Sprintf("Please provide a value for \"%s\"", variable.Name), + Message: fmt.Sprintf("Please provide a value for %q", variable.Name), Default: variable.Default, } - if err = survey.AskOne(prompt, &value); err != nil { - return "", err - } - - return value, nil + return value, survey.AskOne(prompt, &value) } diff --git a/src/pkg/layout/package.go b/src/pkg/layout/package.go index eeb5637f5b..f04bf5c0d9 100644 --- a/src/pkg/layout/package.go +++ b/src/pkg/layout/package.go @@ -59,9 +59,9 @@ func New(baseDir string) *PackagePaths { // ReadZarfYAML reads a zarf.yaml file into memory, // checks if it's using the legacy layout, and migrates deprecated component configs. -func (pp *PackagePaths) ReadZarfYAML(path string) (pkg types.ZarfPackage, warnings []string, err error) { - if err := utils.ReadYaml(path, &pkg); err != nil { - return types.ZarfPackage{}, nil, fmt.Errorf("unable to read zarf.yaml file") +func (pp *PackagePaths) ReadZarfYAML() (pkg types.ZarfPackage, warnings []string, err error) { + if err := utils.ReadYaml(pp.ZarfYAML, &pkg); err != nil { + return types.ZarfPackage{}, nil, fmt.Errorf("unable to read zarf.yaml: %w", err) } if pp.IsLegacyLayout() { @@ -169,13 +169,20 @@ func (pp *PackagePaths) IsLegacyLayout() bool { } // SignPackage signs the zarf.yaml in a Zarf package. -func (pp *PackagePaths) SignPackage(signingKeyPath, signingKeyPassword string) error { +func (pp *PackagePaths) SignPackage(signingKeyPath, signingKeyPassword string, isInteractive bool) error { + if signingKeyPath == "" { + return nil + } + pp.Signature = filepath.Join(pp.Base, Signature) passwordFunc := func(_ bool) ([]byte, error) { if signingKeyPassword != "" { return []byte(signingKeyPassword), nil } + if !isInteractive { + return nil, nil + } return interactive.PromptSigPassword() } _, err := utils.CosignSignBlob(pp.ZarfYAML, pp.Signature, signingKeyPath, passwordFunc) diff --git a/src/pkg/packager/common.go b/src/pkg/packager/common.go index 11101ecec2..ba1335852e 100644 --- a/src/pkg/packager/common.go +++ b/src/pkg/packager/common.go @@ -217,8 +217,8 @@ func (p *Packager) attemptClusterChecks() (err error) { // validatePackageArchitecture validates that the package architecture matches the target cluster architecture. func (p *Packager) validatePackageArchitecture() error { - // Ignore this check if the architecture is explicitly "multi", we don't have a cluster connection, or the package contains no images - if p.cfg.Pkg.Metadata.Architecture == "multi" || !p.isConnectedToCluster() || !p.hasImages() { + // Ignore this check if we don't have a cluster connection, or the package contains no images + if !p.isConnectedToCluster() || !p.hasImages() { return nil } diff --git a/src/pkg/packager/components.go b/src/pkg/packager/components.go deleted file mode 100644 index 84e0b59d04..0000000000 --- a/src/pkg/packager/components.go +++ /dev/null @@ -1,227 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package packager contains functions for interacting with, managing and deploying Zarf packages. -package packager - -import ( - "path" - "runtime" - "slices" - "strings" - - "github.com/defenseunicorns/zarf/src/config/lang" - "github.com/defenseunicorns/zarf/src/pkg/interactive" - "github.com/defenseunicorns/zarf/src/pkg/message" - "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" - "github.com/defenseunicorns/zarf/src/types" -) - -type selectState int - -const ( - unknown selectState = iota - included - excluded -) - -// filterComponents removes components not matching the current OS if filterByOS is set. -func (p *Packager) filterComponents() { - // Filter each component to only compatible platforms. - filteredComponents := []types.ZarfComponent{} - for _, component := range p.cfg.Pkg.Components { - // Ignore only filters that are empty - var validArch, validOS bool - - // Test for valid architecture - if component.Only.Cluster.Architecture == "" || component.Only.Cluster.Architecture == p.cfg.Pkg.Metadata.Architecture { - validArch = true - } else { - message.Debugf("Skipping component %s, %s is not compatible with %s", component.Name, component.Only.Cluster.Architecture, p.cfg.Pkg.Metadata.Architecture) - } - - // Test for a valid OS - if component.Only.LocalOS == "" || component.Only.LocalOS == runtime.GOOS { - validOS = true - } else { - message.Debugf("Skipping component %s, %s is not compatible with %s", component.Name, component.Only.LocalOS, runtime.GOOS) - } - - // If both the OS and architecture are valid, add the component to the filtered list - if validArch && validOS { - filteredComponents = append(filteredComponents, component) - } - } - // Update the active package with the filtered components. - p.cfg.Pkg.Components = filteredComponents -} - -func (p *Packager) getSelectedComponents() []types.ZarfComponent { - var selectedComponents []types.ZarfComponent - groupedComponents := map[string][]types.ZarfComponent{} - orderedComponentGroups := []string{} - - // Group the components by Name and Group while maintaining order - for _, component := range p.cfg.Pkg.Components { - groupKey := component.Name - if component.Group != "" { - groupKey = component.Group - } - - if !slices.Contains(orderedComponentGroups, groupKey) { - orderedComponentGroups = append(orderedComponentGroups, groupKey) - } - - groupedComponents[groupKey] = append(groupedComponents[groupKey], component) - } - - // Split the --components list as a comma-delimited list - requestedComponents := helpers.StringToSlice(p.cfg.PkgOpts.OptionalComponents) - isPartial := len(requestedComponents) > 0 && requestedComponents[0] != "" - - if isPartial { - matchedRequests := map[string]bool{} - - // NOTE: This does not use forIncludedComponents as it takes group, default and required status into account. - for _, groupKey := range orderedComponentGroups { - var groupDefault *types.ZarfComponent - var groupSelected *types.ZarfComponent - - for _, component := range groupedComponents[groupKey] { - // Ensure we have a local version of the component to point to (otherwise the pointer might change on us) - component := component - - selectState, matchedRequest := includedOrExcluded(component, requestedComponents) - - if !component.Required { - if selectState == excluded { - // If the component was explicitly excluded, record the match and continue - matchedRequests[matchedRequest] = true - continue - } else if selectState == unknown && component.Default && groupDefault == nil { - // If the component is default but not included or excluded, remember the default - groupDefault = &component - } - } else { - // Force the selectState to included for Required components - selectState = included - } - - if selectState == included { - // If the component was explicitly included, record the match - matchedRequests[matchedRequest] = true - - // Then check for already selected groups - if groupSelected != nil { - message.Fatalf(nil, lang.PkgDeployErrMultipleComponentsSameGroup, groupSelected.Name, component.Name, component.Group) - } - - // Then append to the final list - selectedComponents = append(selectedComponents, component) - groupSelected = &component - } - } - - // If nothing was selected from a group, handle the default - if groupSelected == nil && groupDefault != nil { - selectedComponents = append(selectedComponents, *groupDefault) - } else if len(groupedComponents[groupKey]) > 1 && groupSelected == nil && groupDefault == nil { - // If no default component was found, give up - componentNames := []string{} - for _, component := range groupedComponents[groupKey] { - componentNames = append(componentNames, component.Name) - } - message.Fatalf(nil, lang.PkgDeployErrNoDefaultOrSelection, strings.Join(componentNames, ",")) - } - } - - // Check that we have matched against all requests - for _, requestedComponent := range requestedComponents { - if _, ok := matchedRequests[requestedComponent]; !ok { - message.Fatalf(nil, lang.PkgDeployErrNoCompatibleComponentsForSelection, requestedComponent) - } - } - } else { - for _, groupKey := range orderedComponentGroups { - if len(groupedComponents[groupKey]) > 1 { - component := interactive.SelectChoiceGroup(groupedComponents[groupKey]) - selectedComponents = append(selectedComponents, component) - } else { - component := groupedComponents[groupKey][0] - - if component.Required { - selectedComponents = append(selectedComponents, component) - } else if selected := interactive.SelectOptionalComponent(component); selected { - selectedComponents = append(selectedComponents, component) - } - } - } - } - - return selectedComponents -} - -func (p *Packager) forIncludedComponents(onIncluded func(types.ZarfComponent) error) error { - requestedComponents := helpers.StringToSlice(p.cfg.PkgOpts.OptionalComponents) - isPartial := len(requestedComponents) > 0 && requestedComponents[0] != "" - - for _, component := range p.cfg.Pkg.Components { - selectState := unknown - - if isPartial { - selectState, _ = includedOrExcluded(component, requestedComponents) - - if selectState == excluded { - continue - } - } else { - selectState = included - } - - if selectState == included { - if err := onIncluded(component); err != nil { - return err - } - } - } - - return nil -} - -func includedOrExcluded(component types.ZarfComponent, requestedComponentNames []string) (selectState, string) { - // Check if the component has a leading dash indicating it should be excluded - this is done first so that exclusions precede inclusions - for _, requestedComponent := range requestedComponentNames { - if strings.HasPrefix(requestedComponent, "-") { - // If the component glob matches one of the requested components, then return true - // This supports globbing with "path" in order to have the same behavior across OSes (if we ever allow namespaced components with /) - if matched, _ := path.Match(strings.TrimPrefix(requestedComponent, "-"), component.Name); matched { - return excluded, requestedComponent - } - } - } - // Check if the component matches a glob pattern and should be included - for _, requestedComponent := range requestedComponentNames { - // If the component glob matches one of the requested components, then return true - // This supports globbing with "path" in order to have the same behavior across OSes (if we ever allow namespaced components with /) - if matched, _ := path.Match(requestedComponent, component.Name); matched { - return included, requestedComponent - } - } - - // All other cases we don't know if we should include or exclude yet - return unknown, "" -} - -func requiresCluster(component types.ZarfComponent) bool { - hasImages := len(component.Images) > 0 - hasCharts := len(component.Charts) > 0 - hasManifests := len(component.Manifests) > 0 - hasRepos := len(component.Repos) > 0 - hasDataInjections := len(component.DataInjections) > 0 - - if hasImages || hasCharts || hasManifests || hasRepos || hasDataInjections { - return true - } - - return false -} diff --git a/src/pkg/packager/composer/list.go b/src/pkg/packager/composer/list.go index 9ab104b8b1..e641bf8929 100644 --- a/src/pkg/packager/composer/list.go +++ b/src/pkg/packager/composer/list.go @@ -35,17 +35,17 @@ type Node struct { next *Node } -// GetIndex returns the .components index location for this node's source `zarf.yaml` -func (n *Node) GetIndex() int { +// Index returns the .components index location for this node's source `zarf.yaml` +func (n *Node) Index() int { return n.index } -// GetOriginalPackageName returns the .metadata.name of the zarf package the component originated from -func (n *Node) GetOriginalPackageName() string { +// OriginalPackageName returns the .metadata.name for this node's source `zarf.yaml` +func (n *Node) OriginalPackageName() string { return n.originalPackageName } -// ImportLocation gets the path from the base zarf file to the imported zarf file +// ImportLocation gets the path from the base `zarf.yaml` to the imported `zarf.yaml` func (n *Node) ImportLocation() string { if n.prev != nil { if n.prev.ZarfComponent.Import.URL != "" { @@ -265,7 +265,7 @@ func (ic *ImportChain) Migrate(build types.ZarfBuildData) (warnings []string) { node = node.next } if len(warnings) > 0 { - final := fmt.Sprintf("migrations were performed on the import chain of: %q", ic.head.Name) + final := fmt.Sprintf("Migrations were performed on the import chain of: %q", ic.head.Name) warnings = append(warnings, final) } return warnings diff --git a/src/pkg/packager/composer/override.go b/src/pkg/packager/composer/override.go index 8d44dce935..4a07a1d936 100644 --- a/src/pkg/packager/composer/override.go +++ b/src/pkg/packager/composer/override.go @@ -22,7 +22,7 @@ func overrideMetadata(c *types.ZarfComponent, override types.ZarfComponent) erro if override.Only.LocalOS != "" { if c.Only.LocalOS != "" { - return fmt.Errorf("component %q \"only.localOS\" %q cannot be redefined as %q during compose", c.Name, c.Only.LocalOS, override.Only.LocalOS) + return fmt.Errorf("component %q: \"only.localOS\" %q cannot be redefined as %q during compose", c.Name, c.Only.LocalOS, override.Only.LocalOS) } c.Only.LocalOS = override.Only.LocalOS @@ -37,7 +37,7 @@ func overrideDeprecated(c *types.ZarfComponent, override types.ZarfComponent) { c.DeprecatedCosignKeyPath = override.DeprecatedCosignKeyPath } - c.Group = override.Group + c.DeprecatedGroup = override.DeprecatedGroup // Merge deprecated scripts for backwards compatibility with older zarf binaries. c.DeprecatedScripts.Before = append(c.DeprecatedScripts.Before, override.DeprecatedScripts.Before...) diff --git a/src/pkg/packager/create.go b/src/pkg/packager/create.go index 9b01c6fefe..5005b4626f 100755 --- a/src/pkg/packager/create.go +++ b/src/pkg/packager/create.go @@ -10,8 +10,10 @@ import ( "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/internal/packager/validate" + "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager/creator" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" ) // Create generates a Zarf package tarball for a given PackageConfig and optional base directory. @@ -29,6 +31,10 @@ func (p *Packager) Create() (err error) { pc := creator.NewPackageCreator(p.cfg.CreateOpts, p.cfg, cwd) + if err := helpers.CreatePathAndCopy(layout.ZarfYAML, p.layout.ZarfYAML); err != nil { + return err + } + p.cfg.Pkg, p.warnings, err = pc.LoadPackageDefinition(p.layout) if err != nil { return err diff --git a/src/pkg/packager/creator/differential.go b/src/pkg/packager/creator/differential.go index 4533531e03..59915eead1 100644 --- a/src/pkg/packager/creator/differential.go +++ b/src/pkg/packager/creator/differential.go @@ -35,15 +35,11 @@ func loadDifferentialData(diffPkgPath string) (diffData *types.DifferentialData, return nil, err } - if err := src.LoadPackageMetadata(diffLayout, false, false); err != nil { + diffPkg, _, err := src.LoadPackageMetadata(diffLayout, false, false) + if err != nil { return nil, err } - var diffPkg types.ZarfPackage - if err := utils.ReadYaml(diffLayout.ZarfYAML, &diffPkg); err != nil { - return nil, fmt.Errorf("error reading the differential Zarf package: %w", err) - } - allIncludedImagesMap := map[string]bool{} allIncludedReposMap := map[string]bool{} diff --git a/src/pkg/packager/creator/normal.go b/src/pkg/packager/creator/normal.go index f7fd527a68..b45a5dfdfa 100644 --- a/src/pkg/packager/creator/normal.go +++ b/src/pkg/packager/creator/normal.go @@ -60,7 +60,7 @@ func NewPackageCreator(createOpts types.ZarfCreateOptions, cfg *types.PackagerCo // LoadPackageDefinition loads and configures a zarf.yaml file during package create. func (pc *PackageCreator) LoadPackageDefinition(dst *layout.PackagePaths) (pkg types.ZarfPackage, warnings []string, err error) { - pkg, warnings, err = dst.ReadZarfYAML(layout.ZarfYAML) + pkg, warnings, err = dst.ReadZarfYAML() if err != nil { return types.ZarfPackage{}, nil, err } @@ -254,10 +254,8 @@ func (pc *PackageCreator) Output(dst *layout.PackagePaths, pkg *types.ZarfPackag } // Sign the package if a key has been provided - if pc.createOpts.SigningKeyPath != "" { - if err := dst.SignPackage(pc.createOpts.SigningKeyPath, pc.createOpts.SigningKeyPassword); err != nil { - return err - } + if err := dst.SignPackage(pc.createOpts.SigningKeyPath, pc.createOpts.SigningKeyPassword, !config.CommonOptions.Confirm); err != nil { + return err } // Create a remote ref + client for the package (if output is OCI) diff --git a/src/pkg/packager/creator/skeleton.go b/src/pkg/packager/creator/skeleton.go index 6e186af0c5..7f95585b00 100644 --- a/src/pkg/packager/creator/skeleton.go +++ b/src/pkg/packager/creator/skeleton.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/extensions/bigbang" "github.com/defenseunicorns/zarf/src/internal/packager/helm" @@ -42,7 +43,7 @@ func NewSkeletonCreator(createOpts types.ZarfCreateOptions, publishOpts types.Za // LoadPackageDefinition loads and configure a zarf.yaml file when creating and publishing a skeleton package. func (sc *SkeletonCreator) LoadPackageDefinition(dst *layout.PackagePaths) (pkg types.ZarfPackage, warnings []string, err error) { - pkg, warnings, err = dst.ReadZarfYAML(layout.ZarfYAML) + pkg, warnings, err = dst.ReadZarfYAML() if err != nil { return types.ZarfPackage{}, nil, err } @@ -114,14 +115,7 @@ func (sc *SkeletonCreator) Output(dst *layout.PackagePaths, pkg *types.ZarfPacka return fmt.Errorf("unable to write zarf.yaml: %w", err) } - // Sign the package if a key has been provided - if sc.publishOpts.SigningKeyPath != "" { - if err := dst.SignPackage(sc.publishOpts.SigningKeyPath, sc.publishOpts.SigningKeyPassword); err != nil { - return err - } - } - - return nil + return dst.SignPackage(sc.publishOpts.SigningKeyPath, sc.publishOpts.SigningKeyPassword, !config.CommonOptions.Confirm) } func (sc *SkeletonCreator) processExtensions(components []types.ZarfComponent, layout *layout.PackagePaths) (processedComponents []types.ZarfComponent, err error) { diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 1bef15963e..feb59498ee 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strconv" "strings" "sync" @@ -24,6 +25,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager/actions" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/packager/variables" "github.com/defenseunicorns/zarf/src/pkg/transform" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" @@ -41,13 +43,30 @@ func (p *Packager) resetRegistryHPA() { // Deploy attempts to deploy the given PackageConfig. func (p *Packager) Deploy() (err error) { - if err = p.source.LoadPackage(p.layout, true); err != nil { - return fmt.Errorf("unable to load the package: %w", err) - } - p.cfg.Pkg, p.warnings, err = p.layout.ReadZarfYAML(p.layout.ZarfYAML) - if err != nil { - return err + isInteractive := !config.CommonOptions.Confirm + + deployFilter := filters.Combine( + filters.ByLocalOS(runtime.GOOS), + filters.ForDeploy(p.cfg.PkgOpts.OptionalComponents, isInteractive), + ) + + if isInteractive { + filter := filters.Empty() + + p.cfg.Pkg, p.warnings, err = p.source.LoadPackage(p.layout, filter, true) + if err != nil { + return fmt.Errorf("unable to load the package: %w", err) + } + } else { + p.cfg.Pkg, p.warnings, err = p.source.LoadPackage(p.layout, deployFilter, true) + if err != nil { + return fmt.Errorf("unable to load the package: %w", err) + } + + if err := variables.SetVariableMapInConfig(p.cfg); err != nil { + return err + } } if err := p.validateLastNonBreakingVersion(); err != nil { @@ -67,9 +86,16 @@ func (p *Packager) Deploy() (err error) { return fmt.Errorf("deployment cancelled") } - // Set variables and prompt if --confirm is not set - if err := variables.SetVariableMapInConfig(p.cfg); err != nil { - return err + if isInteractive { + p.cfg.Pkg.Components, err = deployFilter.Apply(p.cfg.Pkg) + if err != nil { + return err + } + + // Set variables and prompt if --confirm is not set + if err := variables.SetVariableMapInConfig(p.cfg); err != nil { + return err + } } p.hpaModified = false @@ -77,9 +103,6 @@ func (p *Packager) Deploy() (err error) { // Reset registry HPA scale down whether an error occurs or not defer p.resetRegistryHPA() - // Filter out components that are not compatible with this system - p.filterComponents() - // Get a list of all the components we are deploying and actually deploy them deployedComponents, err := p.deployComponents() if err != nil { @@ -99,8 +122,6 @@ func (p *Packager) Deploy() (err error) { // deployComponents loops through a list of ZarfComponents and deploys them. func (p *Packager) deployComponents() (deployedComponents []types.DeployedComponent, err error) { - componentsToDeploy := p.getSelectedComponents() - // Generate a value template if p.valueTemplate, err = template.Generate(p.cfg); err != nil { return deployedComponents, fmt.Errorf("unable to generate the value template: %w", err) @@ -112,7 +133,7 @@ func (p *Packager) deployComponents() (deployedComponents []types.DeployedCompon } // Process all the components we are deploying - for _, component := range componentsToDeploy { + for _, component := range p.cfg.Pkg.Components { deployedComponent := types.DeployedComponent{ Name: component.Name, @@ -121,7 +142,7 @@ func (p *Packager) deployComponents() (deployedComponents []types.DeployedCompon } // If this component requires a cluster, connect to one - if requiresCluster(component) { + if component.RequiresCluster() { timeout := cluster.DefaultTimeout if p.cfg.Pkg.IsInitConfig() { timeout = 5 * time.Minute @@ -212,7 +233,7 @@ func (p *Packager) deployInitComponent(component types.ZarfComponent) (charts [] } // Always init the state before the first component that requires the cluster (on most deployments, the zarf-seed-registry) - if requiresCluster(component) && p.cfg.State == nil { + if component.RequiresCluster() && p.cfg.State == nil { err = p.cluster.InitZarfState(p.cfg.InitOpts) if err != nil { return charts, fmt.Errorf("unable to initialize Zarf state: %w", err) @@ -266,7 +287,7 @@ func (p *Packager) deployComponent(component types.ZarfComponent, noImgChecksum onDeploy := component.Actions.OnDeploy - if !p.valueTemplate.Ready() && requiresCluster(component) { + if !p.valueTemplate.Ready() && component.RequiresCluster() { // Setup the state in the config and get the valuesTemplate p.valueTemplate, err = p.setupStateValuesTemplate() if err != nil { diff --git a/src/pkg/packager/deprecated/common.go b/src/pkg/packager/deprecated/common.go index 2fdbfdcaf3..3583eac9a4 100644 --- a/src/pkg/packager/deprecated/common.go +++ b/src/pkg/packager/deprecated/common.go @@ -69,7 +69,7 @@ func MigrateComponent(build types.ZarfBuildData, component types.ZarfComponent) } // Show a warning if the component contains a group as that has been deprecated and will be removed. - if component.Group != "" { + if component.DeprecatedGroup != "" { warnings = append(warnings, fmt.Sprintf("Component %s is using group which has been deprecated and will be removed in v1.0.0. Please migrate to another solution.", component.Name)) } diff --git a/src/pkg/packager/dev.go b/src/pkg/packager/dev.go index 2b978ec479..6f0992cfca 100644 --- a/src/pkg/packager/dev.go +++ b/src/pkg/packager/dev.go @@ -7,12 +7,16 @@ package packager import ( "fmt" "os" + "runtime" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/internal/packager/validate" + "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager/creator" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/packager/variables" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" ) @@ -32,25 +36,32 @@ func (p *Packager) DevDeploy() error { pc := creator.NewPackageCreator(p.cfg.CreateOpts, p.cfg, cwd) + if err := helpers.CreatePathAndCopy(layout.ZarfYAML, p.layout.ZarfYAML); err != nil { + return err + } + p.cfg.Pkg, p.warnings, err = pc.LoadPackageDefinition(p.layout) if err != nil { return err } - // Filter out components that are not compatible with this system - p.filterComponents() - - // Also filter out components that are not required, nor requested via --components - // This is different from the above filter, as it is not based on the system, but rather - // the user's selection and the component's `required` field - // This is also different from regular package creation, where we still assemble and package up - // all components and their dependencies, regardless of whether they are required or not - p.cfg.Pkg.Components = p.getSelectedComponents() + filter := filters.Combine( + filters.ByLocalOS(runtime.GOOS), + filters.ForDeploy(p.cfg.PkgOpts.OptionalComponents, false), + ) + p.cfg.Pkg.Components, err = filter.Apply(p.cfg.Pkg) + if err != nil { + return err + } if err := validate.Run(p.cfg.Pkg); err != nil { return fmt.Errorf("unable to validate package: %w", err) } + if err := variables.SetVariableMapInConfig(p.cfg); err != nil { + return err + } + // If building in yolo mode, strip out all images and repos if !p.cfg.CreateOpts.NoYOLO { for idx := range p.cfg.Pkg.Components { @@ -65,11 +76,6 @@ func (p *Packager) DevDeploy() error { message.HeaderInfof("📦 PACKAGE DEPLOY %s", p.cfg.Pkg.Metadata.Name) - // Set variables and prompt if --confirm is not set - if err := variables.SetVariableMapInConfig(p.cfg); err != nil { - return err - } - p.connectStrings = make(types.ConnectStrings) if !p.cfg.CreateOpts.NoYOLO { diff --git a/src/pkg/packager/filters/deploy.go b/src/pkg/packager/filters/deploy.go new file mode 100644 index 0000000000..51b4bf074c --- /dev/null +++ b/src/pkg/packager/filters/deploy.go @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "fmt" + "slices" + "strings" + + "github.com/agnivade/levenshtein" + "github.com/defenseunicorns/zarf/src/pkg/interactive" + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" + "github.com/defenseunicorns/zarf/src/types" +) + +// ForDeploy creates a new deployment filter. +func ForDeploy(optionalComponents string, isInteractive bool) ComponentFilterStrategy { + requested := helpers.StringToSlice(optionalComponents) + + return &deploymentFilter{ + requested, + isInteractive, + } +} + +// deploymentFilter is the default filter for deployments. +type deploymentFilter struct { + requestedComponents []string + isInteractive bool +} + +// Errors for the deployment filter. +var ( + ErrMultipleSameGroup = fmt.Errorf("cannot specify multiple components from the same group") + ErrNoDefaultOrSelection = fmt.Errorf("no default or selected component found") + ErrNotFound = fmt.Errorf("no compatible components found") + ErrSelectionCanceled = fmt.Errorf("selection canceled") +) + +// Apply applies the filter. +func (f *deploymentFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, error) { + var selectedComponents []types.ZarfComponent + groupedComponents := map[string][]types.ZarfComponent{} + orderedComponentGroups := []string{} + + // Group the components by Name and Group while maintaining order + for _, component := range pkg.Components { + groupKey := component.Name + if component.DeprecatedGroup != "" { + groupKey = component.DeprecatedGroup + } + + if !slices.Contains(orderedComponentGroups, groupKey) { + orderedComponentGroups = append(orderedComponentGroups, groupKey) + } + + groupedComponents[groupKey] = append(groupedComponents[groupKey], component) + } + + isPartial := len(f.requestedComponents) > 0 && f.requestedComponents[0] != "" + + if isPartial { + matchedRequests := map[string]bool{} + + // NOTE: This does not use forIncludedComponents as it takes group, default and required status into account. + for _, groupKey := range orderedComponentGroups { + var groupDefault *types.ZarfComponent + var groupSelected *types.ZarfComponent + + for _, component := range groupedComponents[groupKey] { + // Ensure we have a local version of the component to point to (otherwise the pointer might change on us) + component := component + + selectState, matchedRequest := includedOrExcluded(component.Name, f.requestedComponents) + + if !isRequired(component) { + if selectState == excluded { + // If the component was explicitly excluded, record the match and continue + matchedRequests[matchedRequest] = true + continue + } else if selectState == unknown && component.Default && groupDefault == nil { + // If the component is default but not included or excluded, remember the default + groupDefault = &component + } + } else { + // Force the selectState to included for Required components + selectState = included + } + + if selectState == included { + // If the component was explicitly included, record the match + matchedRequests[matchedRequest] = true + + // Then check for already selected groups + if groupSelected != nil { + return nil, fmt.Errorf("%w: group: %s selected: %s, %s", ErrMultipleSameGroup, component.DeprecatedGroup, groupSelected.Name, component.Name) + } + + // Then append to the final list + selectedComponents = append(selectedComponents, component) + groupSelected = &component + } + } + + // If nothing was selected from a group, handle the default + if groupSelected == nil && groupDefault != nil { + selectedComponents = append(selectedComponents, *groupDefault) + } else if len(groupedComponents[groupKey]) > 1 && groupSelected == nil && groupDefault == nil { + // If no default component was found, give up + componentNames := []string{} + for _, component := range groupedComponents[groupKey] { + componentNames = append(componentNames, component.Name) + } + return nil, fmt.Errorf("%w: choose from %s", ErrNoDefaultOrSelection, strings.Join(componentNames, ", ")) + } + } + + // Check that we have matched against all requests + for _, requestedComponent := range f.requestedComponents { + if _, ok := matchedRequests[requestedComponent]; !ok { + closeEnough := []string{} + for _, c := range pkg.Components { + d := levenshtein.ComputeDistance(c.Name, requestedComponent) + if d <= 5 { + closeEnough = append(closeEnough, c.Name) + } + } + return nil, fmt.Errorf("%w: %s, suggestions (%s)", ErrNotFound, requestedComponent, strings.Join(closeEnough, ", ")) + } + } + } else { + for _, groupKey := range orderedComponentGroups { + group := groupedComponents[groupKey] + if len(group) > 1 { + if f.isInteractive { + component, err := interactive.SelectChoiceGroup(group) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrSelectionCanceled, err) + } + selectedComponents = append(selectedComponents, component) + } else { + foundDefault := false + componentNames := []string{} + for _, component := range group { + // If the component is default, then use it + if component.Default { + selectedComponents = append(selectedComponents, component) + foundDefault = true + break + } + // Add each component name to the list + componentNames = append(componentNames, component.Name) + } + if !foundDefault { + // If no default component was found, give up + return nil, fmt.Errorf("%w: choose from %s", ErrNoDefaultOrSelection, strings.Join(componentNames, ", ")) + } + } + } else { + component := groupedComponents[groupKey][0] + + if isRequired(component) { + selectedComponents = append(selectedComponents, component) + continue + } + + if f.isInteractive { + selected, err := interactive.SelectOptionalComponent(component) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrSelectionCanceled, err) + } + if selected { + selectedComponents = append(selectedComponents, component) + continue + } + } + + if component.Default { + selectedComponents = append(selectedComponents, component) + continue + } + } + } + } + + return selectedComponents, nil +} diff --git a/src/pkg/packager/filters/deploy_test.go b/src/pkg/packager/filters/deploy_test.go new file mode 100644 index 0000000000..e8c3ea3fe7 --- /dev/null +++ b/src/pkg/packager/filters/deploy_test.go @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" + "github.com/defenseunicorns/zarf/src/types" + "github.com/stretchr/testify/require" +) + +func componentFromQuery(t *testing.T, q string) types.ZarfComponent { + c := types.ZarfComponent{ + Name: q, + } + + conditions := strings.Split(q, "&&") + for _, cond := range conditions { + cond = strings.TrimSpace(cond) + switch cond { + case "default=true": + c.Default = true + case "default=false": + c.Default = false + case "required=": + c.Required = nil + case "required=false": + c.Required = helpers.BoolPtr(false) + case "required=true": + c.Required = helpers.BoolPtr(true) + default: + if strings.HasPrefix(cond, "group=") { + c.DeprecatedGroup = cond[6:] + continue + } + if strings.HasPrefix(cond, "idx=") { + continue + } + require.FailNow(t, "unknown condition", "unknown condition %q", cond) + } + } + + return c +} + +func componentMatrix(_ *testing.T) []types.ZarfComponent { + var components []types.ZarfComponent + + defaultValues := []bool{true, false} + requiredValues := []interface{}{nil, true, false} + // the duplicate groups are intentional + // this is to test group membership + default filtering + groupValues := []string{"", "foo", "foo", "foo", "bar", "bar", "bar"} + + for idx, groupValue := range groupValues { + for _, defaultValue := range defaultValues { + for _, requiredValue := range requiredValues { + name := strings.Builder{} + + // per validate rules, components in groups cannot be required + if requiredValue != nil && requiredValue.(bool) == true && groupValue != "" { + continue + } + + name.WriteString(fmt.Sprintf("required=%v", requiredValue)) + + if groupValue != "" { + name.WriteString(fmt.Sprintf(" && group=%s && idx=%d && default=%t", groupValue, idx, defaultValue)) + } else if defaultValue { + name.WriteString(" && default=true") + } + + if groupValue != "" { + // if there already exists a component in this group that is default, then set the default to false + // otherwise the filter will error + defaultAlreadyExists := false + if defaultValue { + for _, c := range components { + if c.DeprecatedGroup == groupValue && c.Default { + defaultAlreadyExists = true + break + } + } + } + if defaultAlreadyExists { + defaultValue = false + } + } + + c := types.ZarfComponent{ + Name: name.String(), + Default: defaultValue, + DeprecatedGroup: groupValue, + } + + if requiredValue != nil { + c.Required = helpers.BoolPtr(requiredValue.(bool)) + } + + components = append(components, c) + } + } + } + + return components +} + +func TestDeployFilter_Apply(t *testing.T) { + + possibilities := componentMatrix(t) + + testCases := map[string]struct { + pkg types.ZarfPackage + optionalComponents string + want []types.ZarfComponent + expectedErr error + }{ + "Test when version is less than v0.33.0 w/ no optional components selected": { + pkg: types.ZarfPackage{ + Build: types.ZarfBuildData{ + Version: "v0.32.0", + }, + Components: possibilities, + }, + optionalComponents: "", + want: []types.ZarfComponent{ + componentFromQuery(t, "required= && default=true"), + componentFromQuery(t, "required=true && default=true"), + componentFromQuery(t, "required=false && default=true"), + componentFromQuery(t, "required=true"), + componentFromQuery(t, "required= && group=foo && idx=1 && default=true"), + componentFromQuery(t, "required= && group=bar && idx=4 && default=true"), + }, + }, + "Test when version is less than v0.33.0 w/ some optional components selected": { + pkg: types.ZarfPackage{ + Build: types.ZarfBuildData{ + Version: "v0.32.0", + }, + Components: possibilities, + }, + optionalComponents: strings.Join([]string{"required=false", "required= && group=bar && idx=5 && default=false", "-required=true"}, ","), + want: []types.ZarfComponent{ + componentFromQuery(t, "required= && default=true"), + componentFromQuery(t, "required=true && default=true"), + componentFromQuery(t, "required=false && default=true"), + // while "required=true" was deselected, it is still required + // therefore it should be included + componentFromQuery(t, "required=true"), + componentFromQuery(t, "required=false"), + componentFromQuery(t, "required= && group=foo && idx=1 && default=true"), + componentFromQuery(t, "required= && group=bar && idx=5 && default=false"), + }, + }, + "Test failing when group has no default and no selection was made": { + pkg: types.ZarfPackage{ + Build: types.ZarfBuildData{ + Version: "v0.32.0", + }, + Components: []types.ZarfComponent{ + componentFromQuery(t, "group=foo && default=false"), + componentFromQuery(t, "group=foo && default=false"), + }, + }, + optionalComponents: "", + expectedErr: ErrNoDefaultOrSelection, + }, + "Test failing when multiple are selected from the same group": { + pkg: types.ZarfPackage{ + Build: types.ZarfBuildData{ + Version: "v0.32.0", + }, + Components: []types.ZarfComponent{ + componentFromQuery(t, "group=foo && default=true"), + componentFromQuery(t, "group=foo && default=false"), + }, + }, + optionalComponents: strings.Join([]string{"group=foo && default=false", "group=foo && default=true"}, ","), + expectedErr: ErrMultipleSameGroup, + }, + "Test failing when no components are found that match the query": { + pkg: types.ZarfPackage{ + Build: types.ZarfBuildData{ + Version: "v0.32.0", + }, + Components: possibilities, + }, + optionalComponents: "nonexistent", + expectedErr: ErrNotFound, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // we do not currently support interactive mode in unit tests + isInteractive := false + filter := ForDeploy(tc.optionalComponents, isInteractive) + + result, err := filter.Apply(tc.pkg) + if tc.expectedErr != nil { + require.ErrorIs(t, err, tc.expectedErr) + } else { + require.NoError(t, err) + } + equal := reflect.DeepEqual(tc.want, result) + if !equal { + left := []string{} + right := []string{} + + for _, c := range tc.want { + left = append(left, c.Name) + } + + for _, c := range result { + right = append(right, c.Name) + fmt.Printf("componentFromQuery(t, %q),\n", strings.TrimSpace(c.Name)) + } + + // cause the test to fail + require.FailNow(t, "expected and actual are not equal", "\n\nexpected: %#v\n\nactual: %#v", left, right) + } + }) + } +} diff --git a/src/pkg/packager/filters/empty.go b/src/pkg/packager/filters/empty.go new file mode 100644 index 0000000000..860799fb7b --- /dev/null +++ b/src/pkg/packager/filters/empty.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import "github.com/defenseunicorns/zarf/src/types" + +// Empty returns a filter that does nothing. +func Empty() ComponentFilterStrategy { + return &emptyFilter{} +} + +// emptyFilter is a filter that does nothing. +type emptyFilter struct{} + +// Apply returns the components unchanged. +func (f *emptyFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, error) { + return pkg.Components, nil +} diff --git a/src/pkg/packager/filters/empty_test.go b/src/pkg/packager/filters/empty_test.go new file mode 100644 index 0000000000..8096219ad5 --- /dev/null +++ b/src/pkg/packager/filters/empty_test.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "testing" + + "github.com/defenseunicorns/zarf/src/types" + "github.com/stretchr/testify/require" +) + +func TestEmptyFilter_Apply(t *testing.T) { + components := []types.ZarfComponent{ + { + Name: "component1", + }, + { + Name: "component2", + }, + } + pkg := types.ZarfPackage{ + Components: components, + } + filter := Empty() + + result, err := filter.Apply(pkg) + + require.NoError(t, err) + require.Equal(t, components, result) +} diff --git a/src/pkg/packager/filters/os.go b/src/pkg/packager/filters/os.go new file mode 100644 index 0000000000..b031f35bbc --- /dev/null +++ b/src/pkg/packager/filters/os.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "errors" + + "github.com/defenseunicorns/zarf/src/types" +) + +// ByLocalOS creates a new filter that filters components based on local (runtime) OS. +func ByLocalOS(localOS string) ComponentFilterStrategy { + return &localOSFilter{localOS} +} + +// localOSFilter filters components based on local (runtime) OS. +type localOSFilter struct { + localOS string +} + +// ErrLocalOSRequired is returned when localOS is not set. +var ErrLocalOSRequired = errors.New("localOS is required") + +// Apply applies the filter. +func (f *localOSFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, error) { + if f.localOS == "" { + return nil, ErrLocalOSRequired + } + + filtered := []types.ZarfComponent{} + for _, component := range pkg.Components { + if component.Only.LocalOS == "" || component.Only.LocalOS == f.localOS { + filtered = append(filtered, component) + } + } + return filtered, nil +} diff --git a/src/pkg/packager/filters/os_test.go b/src/pkg/packager/filters/os_test.go new file mode 100644 index 0000000000..9e6b9e7722 --- /dev/null +++ b/src/pkg/packager/filters/os_test.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "testing" + + "github.com/defenseunicorns/zarf/src/internal/packager/validate" + "github.com/defenseunicorns/zarf/src/types" + "github.com/stretchr/testify/require" +) + +func TestLocalOSFilter(t *testing.T) { + + pkg := types.ZarfPackage{} + for _, os := range validate.SupportedOS() { + pkg.Components = append(pkg.Components, types.ZarfComponent{ + Only: types.ZarfComponentOnlyTarget{ + LocalOS: os, + }, + }) + } + + for _, os := range validate.SupportedOS() { + filter := ByLocalOS(os) + result, err := filter.Apply(pkg) + if os == "" { + require.ErrorIs(t, err, ErrLocalOSRequired) + } else { + require.NoError(t, err) + } + for _, component := range result { + if component.Only.LocalOS != "" { + require.Equal(t, os, component.Only.LocalOS) + } + } + } +} diff --git a/src/pkg/packager/filters/select.go b/src/pkg/packager/filters/select.go new file mode 100644 index 0000000000..36ab641636 --- /dev/null +++ b/src/pkg/packager/filters/select.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" + "github.com/defenseunicorns/zarf/src/types" +) + +// BySelectState creates a new simple included filter. +func BySelectState(optionalComponents string) ComponentFilterStrategy { + requested := helpers.StringToSlice(optionalComponents) + + return &selectStateFilter{ + requested, + } +} + +// selectStateFilter sorts based purely on the internal included state of the component. +type selectStateFilter struct { + requestedComponents []string +} + +// Apply applies the filter. +func (f *selectStateFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, error) { + isPartial := len(f.requestedComponents) > 0 && f.requestedComponents[0] != "" + + result := []types.ZarfComponent{} + + for _, component := range pkg.Components { + selectState := unknown + + if isPartial { + selectState, _ = includedOrExcluded(component.Name, f.requestedComponents) + + if selectState == excluded { + continue + } + } else { + selectState = included + } + + if selectState == included { + result = append(result, component) + } + } + + return result, nil +} diff --git a/src/pkg/packager/filters/select_test.go b/src/pkg/packager/filters/select_test.go new file mode 100644 index 0000000000..9d87fbf014 --- /dev/null +++ b/src/pkg/packager/filters/select_test.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "testing" + + "github.com/defenseunicorns/zarf/src/types" + "github.com/stretchr/testify/require" +) + +func Test_selectStateFilter_Apply(t *testing.T) { + tests := []struct { + name string + requestedComponents string + components []types.ZarfComponent + expectedResult []types.ZarfComponent + expectedError error + }{ + { + name: "Test when requestedComponents is empty", + requestedComponents: "", + components: []types.ZarfComponent{ + {Name: "component1"}, + {Name: "component2"}, + {Name: "component3"}, + }, + expectedResult: []types.ZarfComponent{ + {Name: "component1"}, + {Name: "component2"}, + {Name: "component3"}, + }, + expectedError: nil, + }, + { + name: "Test when requestedComponents contains a valid component name", + requestedComponents: "component2", + components: []types.ZarfComponent{ + {Name: "component1"}, + {Name: "component2"}, + {Name: "component3"}, + }, + expectedResult: []types.ZarfComponent{ + {Name: "component2"}, + }, + expectedError: nil, + }, + { + name: "Test when requestedComponents contains an excluded component name", + requestedComponents: "comp*, -component2", + components: []types.ZarfComponent{ + {Name: "component1"}, + {Name: "component2"}, + {Name: "component3"}, + }, + expectedResult: []types.ZarfComponent{ + {Name: "component1"}, + {Name: "component3"}, + }, + expectedError: nil, + }, + { + name: "Test when requestedComponents contains a glob pattern", + requestedComponents: "comp*", + components: []types.ZarfComponent{ + {Name: "component1"}, + {Name: "component2"}, + {Name: "other"}, + }, + expectedResult: []types.ZarfComponent{ + {Name: "component1"}, + {Name: "component2"}, + }, + expectedError: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + filter := BySelectState(tc.requestedComponents) + + result, err := filter.Apply(types.ZarfPackage{ + Components: tc.components, + }) + + require.Equal(t, tc.expectedResult, result) + require.Equal(t, tc.expectedError, err) + }) + } +} diff --git a/src/pkg/packager/filters/strat.go b/src/pkg/packager/filters/strat.go new file mode 100644 index 0000000000..497778b2df --- /dev/null +++ b/src/pkg/packager/filters/strat.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "fmt" + + "github.com/defenseunicorns/zarf/src/types" +) + +// ComponentFilterStrategy is a strategy interface for filtering components. +type ComponentFilterStrategy interface { + Apply(types.ZarfPackage) ([]types.ZarfComponent, error) +} + +// comboFilter is a filter that applies a sequence of filters. +type comboFilter struct { + filters []ComponentFilterStrategy +} + +// Apply applies the filter. +func (f *comboFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, error) { + result := pkg + + for _, filter := range f.filters { + components, err := filter.Apply(result) + if err != nil { + return nil, fmt.Errorf("error applying filter %T: %w", filter, err) + } + result.Components = components + } + + return result.Components, nil +} + +// Combine creates a new filter that applies a sequence of filters. +func Combine(filters ...ComponentFilterStrategy) ComponentFilterStrategy { + return &comboFilter{filters} +} diff --git a/src/pkg/packager/filters/strat_test.go b/src/pkg/packager/filters/strat_test.go new file mode 100644 index 0000000000..5f5391153c --- /dev/null +++ b/src/pkg/packager/filters/strat_test.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "testing" + + "github.com/defenseunicorns/zarf/src/types" + "github.com/stretchr/testify/require" +) + +func TestCombine(t *testing.T) { + f1 := BySelectState("*a*") + f2 := BySelectState("*bar, foo") + f3 := Empty() + + combo := Combine(f1, f2, f3) + + pkg := types.ZarfPackage{ + Components: []types.ZarfComponent{ + { + Name: "foo", + }, + { + Name: "bar", + }, + { + Name: "baz", + }, + { + Name: "foobar", + }, + }, + } + + expected := []types.ZarfComponent{ + { + Name: "bar", + }, + { + Name: "foobar", + }, + } + + result, err := combo.Apply(pkg) + require.NoError(t, err) + require.Equal(t, expected, result) + + // Test error propagation + combo = Combine(f1, f2, ForDeploy("group with no default", false)) + pkg.Components = append(pkg.Components, types.ZarfComponent{ + Name: "group with no default", + DeprecatedGroup: "g1", + }) + _, err = combo.Apply(pkg) + require.Error(t, err) +} diff --git a/src/pkg/packager/filters/utils.go b/src/pkg/packager/filters/utils.go new file mode 100644 index 0000000000..30e1b7cced --- /dev/null +++ b/src/pkg/packager/filters/utils.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "path" + "strings" + + "github.com/defenseunicorns/zarf/src/types" +) + +type selectState int + +const ( + unknown selectState = iota + included + excluded +) + +func includedOrExcluded(componentName string, requestedComponentNames []string) (selectState, string) { + // Check if the component has a leading dash indicating it should be excluded - this is done first so that exclusions precede inclusions + for _, requestedComponent := range requestedComponentNames { + if strings.HasPrefix(requestedComponent, "-") { + // If the component glob matches one of the requested components, then return true + // This supports globbing with "path" in order to have the same behavior across OSes (if we ever allow namespaced components with /) + if matched, _ := path.Match(strings.TrimPrefix(requestedComponent, "-"), componentName); matched { + return excluded, requestedComponent + } + } + } + // Check if the component matches a glob pattern and should be included + for _, requestedComponent := range requestedComponentNames { + // If the component glob matches one of the requested components, then return true + // This supports globbing with "path" in order to have the same behavior across OSes (if we ever allow namespaced components with /) + if matched, _ := path.Match(requestedComponent, componentName); matched { + return included, requestedComponent + } + } + + // All other cases we don't know if we should include or exclude yet + return unknown, "" +} + +// isRequired returns if the component is required or not. +func isRequired(c types.ZarfComponent) bool { + requiredExists := c.Required != nil + required := requiredExists && *c.Required + + if requiredExists { + return required + } + return false +} diff --git a/src/pkg/packager/filters/utils_test.go b/src/pkg/packager/filters/utils_test.go new file mode 100644 index 0000000000..f81b8e105f --- /dev/null +++ b/src/pkg/packager/filters/utils_test.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package filters contains core implementations of the ComponentFilterStrategy interface. +package filters + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_includedOrExcluded(t *testing.T) { + tests := []struct { + name string + componentName string + requestedComponentNames []string + wantState selectState + wantRequestedComponent string + }{ + { + name: "Test when component is excluded", + componentName: "example", + requestedComponentNames: []string{"-example"}, + wantState: excluded, + wantRequestedComponent: "-example", + }, + { + name: "Test when component is included", + componentName: "example", + requestedComponentNames: []string{"example"}, + wantState: included, + wantRequestedComponent: "example", + }, + { + name: "Test when component is not included or excluded", + componentName: "example", + requestedComponentNames: []string{"other"}, + wantState: unknown, + wantRequestedComponent: "", + }, + { + name: "Test when component is excluded and included", + componentName: "example", + requestedComponentNames: []string{"-example", "example"}, + wantState: excluded, + wantRequestedComponent: "-example", + }, + // interesting case, excluded wins + { + name: "Test when component is included and excluded", + componentName: "example", + requestedComponentNames: []string{"example", "-example"}, + wantState: excluded, + wantRequestedComponent: "-example", + }, + { + name: "Test when component is included via glob", + componentName: "example", + requestedComponentNames: []string{"ex*"}, + wantState: included, + wantRequestedComponent: "ex*", + }, + { + name: "Test when component is excluded via glob", + componentName: "example", + requestedComponentNames: []string{"-ex*"}, + wantState: excluded, + wantRequestedComponent: "-ex*", + }, + { + name: "Test when component is not found via glob", + componentName: "example", + requestedComponentNames: []string{"other*"}, + wantState: unknown, + wantRequestedComponent: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotState, gotRequestedComponent := includedOrExcluded(tc.componentName, tc.requestedComponentNames) + require.Equal(t, tc.wantState, gotState) + require.Equal(t, tc.wantRequestedComponent, gotRequestedComponent) + }) + } +} diff --git a/src/pkg/packager/generate.go b/src/pkg/packager/generate.go index 7b21e96f9e..23535fc0c9 100644 --- a/src/pkg/packager/generate.go +++ b/src/pkg/packager/generate.go @@ -38,7 +38,7 @@ func (p *Packager) Generate() (err error) { generatedComponent := types.ZarfComponent{ Name: p.cfg.GenerateOpts.Name, - Required: true, + Required: helpers.BoolPtr(true), Charts: []types.ZarfChart{ { Name: p.cfg.GenerateOpts.Name, diff --git a/src/pkg/packager/inspect.go b/src/pkg/packager/inspect.go index bfc9c1aac8..6a6cc7fac9 100644 --- a/src/pkg/packager/inspect.go +++ b/src/pkg/packager/inspect.go @@ -13,11 +13,7 @@ import ( func (p *Packager) Inspect() (err error) { wantSBOM := p.cfg.InspectOpts.ViewSBOM || p.cfg.InspectOpts.SBOMOutputDir != "" - if err = p.source.LoadPackageMetadata(p.layout, wantSBOM, true); err != nil { - return err - } - - p.cfg.Pkg, p.warnings, err = p.layout.ReadZarfYAML(p.layout.ZarfYAML) + p.cfg.Pkg, p.warnings, err = p.source.LoadPackageMetadata(p.layout, wantSBOM, true) if err != nil { return err } diff --git a/src/pkg/packager/interactive.go b/src/pkg/packager/interactive.go index cdd46bf069..c033f6308f 100644 --- a/src/pkg/packager/interactive.go +++ b/src/pkg/packager/interactive.go @@ -105,7 +105,7 @@ func (p *Packager) getPackageYAMLHints(stage string) map[string]string { hints = utils.AddRootHint(hints, "metadata", "information about this package\n") hints = utils.AddRootHint(hints, "build", "info about the machine, zarf version, and user that created this package\n") - hints = utils.AddRootHint(hints, "components", "definition of capabilities this package deploys") + hints = utils.AddRootHint(hints, "components", "components selected for this operation") hints = utils.AddRootHint(hints, "constants", "static values set by the package author") hints = utils.AddRootHint(hints, "variables", "deployment-specific values that are set on each package deployment") diff --git a/src/pkg/packager/lint/lint.go b/src/pkg/packager/lint/lint.go index 8ac60076d1..94e6f21242 100644 --- a/src/pkg/packager/lint/lint.go +++ b/src/pkg/packager/lint/lint.go @@ -109,7 +109,7 @@ func fillComponentTemplate(validator *Validator, node *composer.Node, createOpts validator.addWarning(validatorMessage{ description: err.Error(), packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), }) } templateMap := map[string]string{} @@ -120,7 +120,7 @@ func fillComponentTemplate(validator *Validator, node *composer.Node, createOpts validator.addWarning(validatorMessage{ description: err.Error(), packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), }) } @@ -129,7 +129,7 @@ func fillComponentTemplate(validator *Validator, node *composer.Node, createOpts validator.addWarning(validatorMessage{ description: fmt.Sprintf(lang.PkgValidateTemplateDeprecation, key, key, key), packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), }) } _, present := createOpts.SetVariables[key] @@ -137,7 +137,7 @@ func fillComponentTemplate(validator *Validator, node *composer.Node, createOpts validator.addWarning(validatorMessage{ description: lang.UnsetVarLintWarning, packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), }) } } @@ -178,12 +178,12 @@ func lintComponent(validator *Validator, node *composer.Node) { func checkForUnpinnedRepos(validator *Validator, node *composer.Node) { for j, repo := range node.Repos { - repoYqPath := fmt.Sprintf(".components.[%d].repos.[%d]", node.GetIndex(), j) + repoYqPath := fmt.Sprintf(".components.[%d].repos.[%d]", node.Index(), j) if !isPinnedRepo(repo) { validator.addWarning(validatorMessage{ yqPath: repoYqPath, packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), description: "Unpinned repository", item: repo, }) @@ -193,13 +193,13 @@ func checkForUnpinnedRepos(validator *Validator, node *composer.Node) { func checkForUnpinnedImages(validator *Validator, node *composer.Node) { for j, image := range node.Images { - imageYqPath := fmt.Sprintf(".components.[%d].images.[%d]", node.GetIndex(), j) + imageYqPath := fmt.Sprintf(".components.[%d].images.[%d]", node.Index(), j) pinnedImage, err := isPinnedImage(image) if err != nil { validator.addError(validatorMessage{ yqPath: imageYqPath, packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), description: "Invalid image reference", item: image, }) @@ -209,7 +209,7 @@ func checkForUnpinnedImages(validator *Validator, node *composer.Node) { validator.addWarning(validatorMessage{ yqPath: imageYqPath, packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), description: "Image not pinned with digest", item: image, }) @@ -219,12 +219,12 @@ func checkForUnpinnedImages(validator *Validator, node *composer.Node) { func checkForUnpinnedFiles(validator *Validator, node *composer.Node) { for j, file := range node.Files { - fileYqPath := fmt.Sprintf(".components.[%d].files.[%d]", node.GetIndex(), j) + fileYqPath := fmt.Sprintf(".components.[%d].files.[%d]", node.Index(), j) if file.Shasum == "" && helpers.IsURL(file.Source) { validator.addWarning(validatorMessage{ yqPath: fileYqPath, packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), description: "No shasum for remote file", item: file.Source, }) @@ -235,18 +235,18 @@ func checkForUnpinnedFiles(validator *Validator, node *composer.Node) { func checkForVarInComponentImport(validator *Validator, node *composer.Node) { if strings.Contains(node.Import.Path, types.ZarfPackageTemplatePrefix) { validator.addWarning(validatorMessage{ - yqPath: fmt.Sprintf(".components.[%d].import.path", node.GetIndex()), + yqPath: fmt.Sprintf(".components.[%d].import.path", node.Index()), packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), description: "Zarf does not evaluate variables at component.x.import.path", item: node.Import.Path, }) } if strings.Contains(node.Import.URL, types.ZarfPackageTemplatePrefix) { validator.addWarning(validatorMessage{ - yqPath: fmt.Sprintf(".components.[%d].import.url", node.GetIndex()), + yqPath: fmt.Sprintf(".components.[%d].import.url", node.Index()), packageRelPath: node.ImportLocation(), - packageName: node.GetOriginalPackageName(), + packageName: node.OriginalPackageName(), description: "Zarf does not evaluate variables at component.x.import.url", item: node.Import.URL, }) diff --git a/src/pkg/packager/mirror.go b/src/pkg/packager/mirror.go index 13122be7a6..f8f90fad55 100644 --- a/src/pkg/packager/mirror.go +++ b/src/pkg/packager/mirror.go @@ -6,25 +6,25 @@ package packager import ( "fmt" + "runtime" "strings" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/types" ) // Mirror pulls resources from a package (images, git repositories, etc) and pushes them to remotes in the air gap without deploying them func (p *Packager) Mirror() (err error) { - spinner := message.NewProgressSpinner("Mirroring Zarf package %s", p.cfg.PkgOpts.PackageSource) - defer spinner.Stop() + filter := filters.Combine( + filters.ByLocalOS(runtime.GOOS), + filters.BySelectState(p.cfg.PkgOpts.OptionalComponents), + ) - if err = p.source.LoadPackage(p.layout, true); err != nil { - return fmt.Errorf("unable to load the package: %w", err) - } - - p.cfg.Pkg, p.warnings, err = p.layout.ReadZarfYAML(p.layout.ZarfYAML) + p.cfg.Pkg, p.warnings, err = p.source.LoadPackage(p.layout, filter, true) if err != nil { - return err + return fmt.Errorf("unable to load the package: %w", err) } var sbomWarnings []string @@ -40,17 +40,17 @@ func (p *Packager) Mirror() (err error) { return fmt.Errorf("mirror cancelled") } - state := &types.ZarfState{ + p.cfg.State = &types.ZarfState{ RegistryInfo: p.cfg.InitOpts.RegistryInfo, GitServer: p.cfg.InitOpts.GitServer, } - p.cfg.State = state - // Filter out components that are not compatible with this system if we have loaded from a tarball - p.filterComponents() - - // Run mirror for each requested component - return p.forIncludedComponents(p.mirrorComponent) + for _, component := range p.cfg.Pkg.Components { + if err := p.mirrorComponent(component); err != nil { + return err + } + } + return nil } // mirrorComponent mirrors a Zarf Component. diff --git a/src/pkg/packager/prepare.go b/src/pkg/packager/prepare.go index 877d882df0..0ee4dec22a 100644 --- a/src/pkg/packager/prepare.go +++ b/src/pkg/packager/prepare.go @@ -19,6 +19,7 @@ import ( "github.com/defenseunicorns/zarf/src/internal/packager/helm" "github.com/defenseunicorns/zarf/src/internal/packager/kustomize" "github.com/defenseunicorns/zarf/src/internal/packager/template" + "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager/creator" "github.com/defenseunicorns/zarf/src/pkg/packager/variables" @@ -56,6 +57,10 @@ func (p *Packager) FindImages() (imgMap map[string][]string, err error) { c := creator.NewPackageCreator(p.cfg.CreateOpts, p.cfg, cwd) + if err := helpers.CreatePathAndCopy(layout.ZarfYAML, p.layout.ZarfYAML); err != nil { + return nil, err + } + p.cfg.Pkg, p.warnings, err = c.LoadPackageDefinition(p.layout) if err != nil { return nil, err diff --git a/src/pkg/packager/publish.go b/src/pkg/packager/publish.go index c438b83456..2f5cd892ce 100644 --- a/src/pkg/packager/publish.go +++ b/src/pkg/packager/publish.go @@ -11,9 +11,11 @@ import ( "strings" "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" "github.com/defenseunicorns/zarf/src/pkg/packager/creator" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/packager/sources" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" @@ -53,6 +55,10 @@ func (p *Packager) Publish() (err error) { sc := creator.NewSkeletonCreator(p.cfg.CreateOpts, p.cfg.PublishOpts) + if err := helpers.CreatePathAndCopy(layout.ZarfYAML, p.layout.ZarfYAML); err != nil { + return err + } + p.cfg.Pkg, p.warnings, err = sc.LoadPackageDefinition(p.layout) if err != nil { return err @@ -66,20 +72,15 @@ func (p *Packager) Publish() (err error) { return err } } else { - if err := p.source.LoadPackage(p.layout, false); err != nil { - return fmt.Errorf("unable to load the package: %w", err) - } - - p.cfg.Pkg, p.warnings, err = p.layout.ReadZarfYAML(p.layout.ZarfYAML) + filter := filters.Empty() + p.cfg.Pkg, p.warnings, err = p.source.LoadPackage(p.layout, filter, false) if err != nil { - return err + return fmt.Errorf("unable to load the package: %w", err) } // Sign the package if a key has been provided - if p.cfg.PublishOpts.SigningKeyPath != "" { - if err := p.layout.SignPackage(p.cfg.PublishOpts.SigningKeyPath, p.cfg.PublishOpts.SigningKeyPassword); err != nil { - return err - } + if err := p.layout.SignPackage(p.cfg.PublishOpts.SigningKeyPath, p.cfg.PublishOpts.SigningKeyPassword, !config.CommonOptions.Confirm); err != nil { + return err } } diff --git a/src/pkg/packager/remove.go b/src/pkg/packager/remove.go index 2f7bf05e01..1204ec4f19 100644 --- a/src/pkg/packager/remove.go +++ b/src/pkg/packager/remove.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "runtime" "slices" @@ -16,6 +17,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/cluster" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager/actions" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/packager/sources" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" @@ -36,16 +38,10 @@ func (p *Packager) Remove() (err error) { // we do not want to allow removal of signed packages without a signature if there are remove actions // as this is arbitrary code execution from an untrusted source - if err = p.source.LoadPackageMetadata(p.layout, false, false); err != nil { - return err - } - - p.cfg.Pkg, p.warnings, err = p.layout.ReadZarfYAML(p.layout.ZarfYAML) + p.cfg.Pkg, p.warnings, err = p.source.LoadPackageMetadata(p.layout, false, false) if err != nil { return err } - - p.filterComponents() packageName = p.cfg.Pkg.Metadata.Name // Build a list of components to remove and determine if we need a cluster connection @@ -53,15 +49,22 @@ func (p *Packager) Remove() (err error) { packageRequiresCluster := false // If components were provided; just remove the things we were asked to remove - p.forIncludedComponents(func(component types.ZarfComponent) error { + filter := filters.Combine( + filters.ByLocalOS(runtime.GOOS), + filters.BySelectState(p.cfg.PkgOpts.OptionalComponents), + ) + included, err := filter.Apply(p.cfg.Pkg) + if err != nil { + return err + } + + for _, component := range included { componentsToRemove = append(componentsToRemove, component.Name) - if requiresCluster(component) { + if component.RequiresCluster() { packageRequiresCluster = true } - - return nil - }) + } // Get or build the secret for the deployed package deployedPackage := &types.DeployedPackage{} diff --git a/src/pkg/packager/sources/cluster.go b/src/pkg/packager/sources/cluster.go index 358a764f77..daa0d54ad0 100644 --- a/src/pkg/packager/sources/cluster.go +++ b/src/pkg/packager/sources/cluster.go @@ -6,11 +6,11 @@ package sources import ( "fmt" - "path/filepath" "github.com/defenseunicorns/zarf/src/internal/packager/validate" "github.com/defenseunicorns/zarf/src/pkg/cluster" "github.com/defenseunicorns/zarf/src/pkg/layout" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" @@ -42,8 +42,8 @@ type ClusterSource struct { // LoadPackage loads a package from a cluster. // // This is not implemented. -func (s *ClusterSource) LoadPackage(_ *layout.PackagePaths, _ bool) error { - return fmt.Errorf("not implemented") +func (s *ClusterSource) LoadPackage(_ *layout.PackagePaths, _ filters.ComponentFilterStrategy, _ bool) (types.ZarfPackage, []string, error) { + return types.ZarfPackage{}, nil, fmt.Errorf("not implemented") } // Collect collects a package from a cluster. @@ -54,13 +54,15 @@ func (s *ClusterSource) Collect(_ string) (string, error) { } // LoadPackageMetadata loads package metadata from a cluster. -func (s *ClusterSource) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ bool) (err error) { +func (s *ClusterSource) LoadPackageMetadata(dst *layout.PackagePaths, _ bool, _ bool) (types.ZarfPackage, []string, error) { dpkg, err := s.GetDeployedPackage(s.PackageSource) if err != nil { - return err + return types.ZarfPackage{}, nil, err } - dst.ZarfYAML = filepath.Join(dst.Base, layout.ZarfYAML) + if err := utils.WriteYaml(dst.ZarfYAML, dpkg.Data, helpers.ReadUser); err != nil { + return types.ZarfPackage{}, nil, err + } - return utils.WriteYaml(dst.ZarfYAML, dpkg.Data, helpers.ReadExecuteAllWriteUser) + return dpkg.Data, nil, nil } diff --git a/src/pkg/packager/sources/new.go b/src/pkg/packager/sources/new.go index 007fef5532..e77c99dd25 100644 --- a/src/pkg/packager/sources/new.go +++ b/src/pkg/packager/sources/new.go @@ -13,6 +13,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" "github.com/defenseunicorns/zarf/src/types" @@ -29,10 +30,10 @@ import ( // `sources.ValidatePackageSignature` and `sources.ValidatePackageIntegrity` can be leveraged for this purpose. type PackageSource interface { // LoadPackage loads a package from a source. - LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) error + LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (pkg types.ZarfPackage, warnings []string, err error) // LoadPackageMetadata loads a package's metadata from a source. - LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) error + LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (pkg types.ZarfPackage, warnings []string, err error) // Collect relocates a package from its source to a tarball in a given destination directory. Collect(destinationDirectory string) (tarball string, err error) @@ -49,7 +50,7 @@ func Identify(pkgSrc string) string { return "split" } - if config.IsValidFileExtension(pkgSrc) { + if IsValidFileExtension(pkgSrc) { return "tarball" } @@ -72,7 +73,7 @@ func New(pkgOpts *types.ZarfPackageOptions) (PackageSource, error) { if err != nil { return nil, err } - source = &OCISource{pkgOpts, remote} + source = &OCISource{ZarfPackageOptions: pkgOpts, Remote: remote} case "tarball": source = &TarballSource{pkgOpts} case "http", "https", "sget": diff --git a/src/pkg/packager/sources/oci.go b/src/pkg/packager/sources/oci.go index b9a0857c9d..83735a9503 100644 --- a/src/pkg/packager/sources/oci.go +++ b/src/pkg/packager/sources/oci.go @@ -15,12 +15,11 @@ import ( "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/utils" - "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" "github.com/defenseunicorns/zarf/src/types" "github.com/mholt/archiver/v3" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) var ( @@ -35,28 +34,29 @@ type OCISource struct { } // LoadPackage loads a package from an OCI registry. -func (s *OCISource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (err error) { +func (s *OCISource) LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (pkg types.ZarfPackage, warnings []string, err error) { ctx := context.TODO() - var pkg types.ZarfPackage - layersToPull := []ocispec.Descriptor{} message.Debugf("Loading package from %q", s.PackageSource) - optionalComponents := helpers.StringToSlice(s.OptionalComponents) - - // pull only needed layers if --confirm is set - if config.CommonOptions.Confirm { + pkg, err = s.FetchZarfYAML(ctx) + if err != nil { + return pkg, nil, err + } + pkg.Components, err = filter.Apply(pkg) + if err != nil { + return pkg, nil, err + } - layersToPull, err = s.LayersFromRequestedComponents(ctx, optionalComponents) - if err != nil { - return fmt.Errorf("unable to get published component image layers: %s", err.Error()) - } + layersToPull, err := s.LayersFromRequestedComponents(ctx, pkg.Components) + if err != nil { + return pkg, nil, fmt.Errorf("unable to get published component image layers: %s", err.Error()) } isPartial := true root, err := s.FetchRoot(ctx) if err != nil { - return err + return pkg, nil, err } if len(root.Layers) == len(layersToPull) { isPartial = false @@ -64,16 +64,12 @@ func (s *OCISource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (er layersFetched, err := s.PullPackage(ctx, dst.Base, config.CommonOptions.OCIConcurrency, layersToPull...) if err != nil { - return fmt.Errorf("unable to pull the package: %w", err) + return pkg, nil, fmt.Errorf("unable to pull the package: %w", err) } dst.SetFromLayers(layersFetched) - if err := utils.ReadYaml(dst.ZarfYAML, &pkg); err != nil { - return err - } - if err := dst.MigrateLegacy(); err != nil { - return err + return pkg, nil, err } if !dst.IsLegacyLayout() { @@ -81,13 +77,13 @@ func (s *OCISource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (er defer spinner.Stop() if err := ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, isPartial); err != nil { - return err + return pkg, nil, err } spinner.Success() if err := ValidatePackageSignature(dst, s.PublicKeyPath); err != nil { - return err + return pkg, nil, err } } @@ -97,28 +93,26 @@ func (s *OCISource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (er if layout.IsNotLoaded(err) { _, err := dst.Components.Create(component) if err != nil { - return err + return pkg, nil, err } } else { - return err + return pkg, nil, err } } } if dst.SBOMs.Path != "" { if err := dst.SBOMs.Unarchive(); err != nil { - return err + return pkg, nil, err } } } - return nil + return pkg, warnings, nil } // LoadPackageMetadata loads a package's metadata from an OCI registry. -func (s *OCISource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (err error) { - var pkg types.ZarfPackage - +func (s *OCISource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (pkg types.ZarfPackage, warnings []string, err error) { toPull := zoci.PackageAlwaysPull if wantSBOM { toPull = append(toPull, layout.SBOMTar) @@ -126,16 +120,17 @@ func (s *OCISource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, ctx := context.TODO() layersFetched, err := s.PullPaths(ctx, dst.Base, toPull) if err != nil { - return err + return pkg, nil, err } dst.SetFromLayers(layersFetched) - if err := utils.ReadYaml(dst.ZarfYAML, &pkg); err != nil { - return err + pkg, warnings, err = dst.ReadZarfYAML() + if err != nil { + return pkg, nil, err } if err := dst.MigrateLegacy(); err != nil { - return err + return pkg, nil, err } if !dst.IsLegacyLayout() { @@ -144,7 +139,7 @@ func (s *OCISource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, defer spinner.Stop() if err := ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, true); err != nil { - return err + return pkg, nil, err } spinner.Success() @@ -154,7 +149,7 @@ func (s *OCISource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, if errors.Is(err, ErrPkgSigButNoKey) && skipValidation { message.Warn("The package was signed but no public key was provided, skipping signature validation") } else { - return err + return pkg, nil, err } } } @@ -162,11 +157,11 @@ func (s *OCISource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, // unpack sboms.tar if wantSBOM { if err := dst.SBOMs.Unarchive(); err != nil { - return err + return pkg, nil, err } } - return nil + return pkg, warnings, nil } // Collect pulls a package from an OCI registry and writes it to a tarball. diff --git a/src/pkg/packager/sources/split.go b/src/pkg/packager/sources/split.go index a5e09e7ec7..ad9be2ba5c 100644 --- a/src/pkg/packager/sources/split.go +++ b/src/pkg/packager/sources/split.go @@ -15,6 +15,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" ) @@ -108,10 +109,10 @@ func (s *SplitTarballSource) Collect(dir string) (string, error) { } // LoadPackage loads a package from a split tarball. -func (s *SplitTarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (err error) { +func (s *SplitTarballSource) LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (pkg types.ZarfPackage, warnings []string, err error) { tb, err := s.Collect(filepath.Dir(s.PackageSource)) if err != nil { - return err + return pkg, nil, err } // Update the package source to the reassembled tarball @@ -122,14 +123,14 @@ func (s *SplitTarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll ts := &TarballSource{ s.ZarfPackageOptions, } - return ts.LoadPackage(dst, unarchiveAll) + return ts.LoadPackage(dst, filter, unarchiveAll) } // LoadPackageMetadata loads a package's metadata from a split tarball. -func (s *SplitTarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (err error) { +func (s *SplitTarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (pkg types.ZarfPackage, warnings []string, err error) { tb, err := s.Collect(filepath.Dir(s.PackageSource)) if err != nil { - return err + return pkg, nil, err } // Update the package source to the reassembled tarball diff --git a/src/pkg/packager/sources/tarball.go b/src/pkg/packager/sources/tarball.go index 5926e513c2..33a52713ea 100644 --- a/src/pkg/packager/sources/tarball.go +++ b/src/pkg/packager/sources/tarball.go @@ -14,7 +14,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" - "github.com/defenseunicorns/zarf/src/pkg/utils" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/pkg/zoci" "github.com/defenseunicorns/zarf/src/types" @@ -32,15 +32,13 @@ type TarballSource struct { } // LoadPackage loads a package from a tarball. -func (s *TarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (err error) { - var pkg types.ZarfPackage - +func (s *TarballSource) LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (pkg types.ZarfPackage, warnings []string, err error) { spinner := message.NewProgressSpinner("Loading package from %q", s.PackageSource) defer spinner.Stop() if s.Shasum != "" { if err := helpers.SHAsMatch(s.PackageSource, s.Shasum); err != nil { - return err + return pkg, nil, err } } @@ -79,17 +77,22 @@ func (s *TarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) return nil }) if err != nil { - return err + return pkg, nil, err } dst.SetFromPaths(pathsExtracted) - if err := utils.ReadYaml(dst.ZarfYAML, &pkg); err != nil { - return err + pkg, warnings, err = dst.ReadZarfYAML() + if err != nil { + return pkg, nil, err + } + pkg.Components, err = filter.Apply(pkg) + if err != nil { + return pkg, nil, err } if err := dst.MigrateLegacy(); err != nil { - return err + return pkg, nil, err } if !dst.IsLegacyLayout() { @@ -97,13 +100,13 @@ func (s *TarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) defer spinner.Stop() if err := ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, false); err != nil { - return err + return pkg, nil, err } spinner.Success() if err := ValidatePackageSignature(dst, s.PublicKeyPath); err != nil { - return err + return pkg, nil, err } } @@ -113,33 +116,31 @@ func (s *TarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) if layout.IsNotLoaded(err) { _, err := dst.Components.Create(component) if err != nil { - return err + return pkg, nil, err } } else { - return err + return pkg, nil, err } } } if dst.SBOMs.Path != "" { if err := dst.SBOMs.Unarchive(); err != nil { - return err + return pkg, nil, err } } } spinner.Success() - return nil + return pkg, warnings, nil } // LoadPackageMetadata loads a package's metadata from a tarball. -func (s *TarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (err error) { - var pkg types.ZarfPackage - +func (s *TarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (pkg types.ZarfPackage, warnings []string, err error) { if s.Shasum != "" { if err := helpers.SHAsMatch(s.PackageSource, s.Shasum); err != nil { - return err + return pkg, nil, err } } @@ -151,7 +152,7 @@ func (s *TarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM b for _, rel := range toExtract { if err := archiver.Extract(s.PackageSource, rel, dst.Base); err != nil { - return err + return pkg, nil, err } // archiver.Extract will not return an error if the file does not exist, so we must manually check if !helpers.InvalidPath(filepath.Join(dst.Base, rel)) { @@ -161,12 +162,13 @@ func (s *TarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM b dst.SetFromPaths(pathsExtracted) - if err := utils.ReadYaml(dst.ZarfYAML, &pkg); err != nil { - return err + pkg, warnings, err = dst.ReadZarfYAML() + if err != nil { + return pkg, nil, err } if err := dst.MigrateLegacy(); err != nil { - return err + return pkg, nil, err } if !dst.IsLegacyLayout() { @@ -175,7 +177,7 @@ func (s *TarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM b defer spinner.Stop() if err := ValidatePackageIntegrity(dst, pkg.Metadata.AggregateChecksum, true); err != nil { - return err + return pkg, nil, err } spinner.Success() @@ -185,18 +187,18 @@ func (s *TarballSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM b if errors.Is(err, ErrPkgSigButNoKey) && skipValidation { message.Warn("The package was signed but no public key was provided, skipping signature validation") } else { - return err + return pkg, nil, err } } } if wantSBOM { if err := dst.SBOMs.Unarchive(); err != nil { - return err + return pkg, nil, err } } - return nil + return pkg, warnings, nil } // Collect for the TarballSource is essentially an `mv` diff --git a/src/pkg/packager/sources/url.go b/src/pkg/packager/sources/url.go index 11f8664302..4185b7fb97 100644 --- a/src/pkg/packager/sources/url.go +++ b/src/pkg/packager/sources/url.go @@ -12,6 +12,7 @@ import ( "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/layout" + "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" @@ -49,16 +50,16 @@ func (s *URLSource) Collect(dir string) (string, error) { } // LoadPackage loads a package from an http, https or sget URL. -func (s *URLSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (err error) { +func (s *URLSource) LoadPackage(dst *layout.PackagePaths, filter filters.ComponentFilterStrategy, unarchiveAll bool) (pkg types.ZarfPackage, warnings []string, err error) { tmp, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) if err != nil { - return err + return pkg, nil, err } defer os.Remove(tmp) dstTarball, err := s.Collect(tmp) if err != nil { - return err + return pkg, nil, err } s.PackageSource = dstTarball @@ -69,20 +70,20 @@ func (s *URLSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (er s.ZarfPackageOptions, } - return ts.LoadPackage(dst, unarchiveAll) + return ts.LoadPackage(dst, filter, unarchiveAll) } // LoadPackageMetadata loads a package's metadata from an http, https or sget URL. -func (s *URLSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (err error) { +func (s *URLSource) LoadPackageMetadata(dst *layout.PackagePaths, wantSBOM bool, skipValidation bool) (pkg types.ZarfPackage, warnings []string, err error) { tmp, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) if err != nil { - return err + return pkg, nil, err } defer os.Remove(tmp) dstTarball, err := s.Collect(tmp) if err != nil { - return err + return pkg, nil, err } s.PackageSource = dstTarball diff --git a/src/pkg/packager/sources/utils.go b/src/pkg/packager/sources/utils.go index e9adb824bd..0e2bcf9436 100644 --- a/src/pkg/packager/sources/utils.go +++ b/src/pkg/packager/sources/utils.go @@ -20,14 +20,30 @@ import ( "github.com/mholt/archiver/v3" ) +// GetValidPackageExtensions returns the valid package extensions. +func GetValidPackageExtensions() [2]string { + return [...]string{".tar.zst", ".tar"} +} + +// IsValidFileExtension returns true if the filename has a valid package extension. +func IsValidFileExtension(filename string) bool { + for _, extension := range GetValidPackageExtensions() { + if strings.HasSuffix(filename, extension) { + return true + } + } + + return false +} + func identifyUnknownTarball(path string) (string, error) { if helpers.InvalidPath(path) { return "", &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist} } - if filepath.Ext(path) != "" && config.IsValidFileExtension(path) { + if filepath.Ext(path) != "" && IsValidFileExtension(path) { return path, nil - } else if filepath.Ext(path) != "" && !config.IsValidFileExtension(path) { - return "", fmt.Errorf("%s is not a supported tarball format (%+v)", path, config.GetValidPackageExtensions()) + } else if filepath.Ext(path) != "" && !IsValidFileExtension(path) { + return "", fmt.Errorf("%s is not a supported tarball format (%+v)", path, GetValidPackageExtensions()) } // rename to .tar.zst and check if it's a valid tar.zst @@ -58,7 +74,7 @@ func identifyUnknownTarball(path string) (string, error) { return tb, nil } - return "", fmt.Errorf("%s is not a supported tarball format (%+v)", path, config.GetValidPackageExtensions()) + return "", fmt.Errorf("%s is not a supported tarball format (%+v)", path, GetValidPackageExtensions()) } // RenameFromMetadata renames a tarball based on its metadata. diff --git a/src/pkg/utils/helpers/misc.go b/src/pkg/utils/helpers/misc.go index f0129c710f..e421c9509e 100644 --- a/src/pkg/utils/helpers/misc.go +++ b/src/pkg/utils/helpers/misc.go @@ -12,6 +12,11 @@ import ( "time" ) +// BoolPtr returns a pointer to a bool. +func BoolPtr(b bool) *bool { + return &b +} + // Retry will retry a function until it succeeds or the timeout is reached. timeout == 2^attempt * delay. func Retry(fn func() error, retries int, delay time.Duration, logger func(format string, args ...any)) error { var err error diff --git a/src/pkg/utils/helpers/misc_test.go b/src/pkg/utils/helpers/misc_test.go index 74e42c40c5..4942cdab64 100644 --- a/src/pkg/utils/helpers/misc_test.go +++ b/src/pkg/utils/helpers/misc_test.go @@ -46,7 +46,7 @@ func (suite *TestMiscSuite) SetupSuite() { } } -func (suite *TestMiscSuite) Test_0_Retry() { +func (suite *TestMiscSuite) TestRetry() { var count int countFn := func() error { count++ @@ -75,7 +75,7 @@ func (suite *TestMiscSuite) Test_0_Retry() { suite.Equal(3, logCount) } -func (suite *TestMiscSuite) Test_1_MergeMap() { +func (suite *TestMiscSuite) TestMergeMap() { expected := map[string]interface{}{ "different": "value", "hello": "it's me", @@ -90,7 +90,7 @@ func (suite *TestMiscSuite) Test_1_MergeMap() { suite.Equal(expected, result) } -func (suite *TestMiscSuite) Test_2_TransformMapKeys() { +func (suite *TestMiscSuite) TestTransformMapKeys() { expected := map[string]interface{}{ "HELLO": "world", "UNIQUE": "value", @@ -104,7 +104,7 @@ func (suite *TestMiscSuite) Test_2_TransformMapKeys() { suite.Equal(expected, result) } -func (suite *TestMiscSuite) Test_3_TransformAndMergeMap() { +func (suite *TestMiscSuite) TestTransformAndMergeMap() { expected := map[string]interface{}{ "DIFFERENT": "value", "HELLO": "it's me", @@ -119,7 +119,7 @@ func (suite *TestMiscSuite) Test_3_TransformAndMergeMap() { suite.Equal(expected, result) } -func (suite *TestMiscSuite) Test_4_MergeMapRecursive() { +func (suite *TestMiscSuite) TestMergeMapRecursive() { expected := map[string]interface{}{ "different": "value", "hello": "it's me", @@ -135,7 +135,7 @@ func (suite *TestMiscSuite) Test_4_MergeMapRecursive() { suite.Equal(expected, result) } -func (suite *TestMiscSuite) Test_5_IsNotZeroAndNotEqual() { +func (suite *TestMiscSuite) TestIsNotZeroAndNotEqual() { original := TestMiscStruct{ Field1: "hello", Field2: 100, @@ -159,7 +159,7 @@ func (suite *TestMiscSuite) Test_5_IsNotZeroAndNotEqual() { suite.Equal(true, result) } -func (suite *TestMiscSuite) Test_6_MergeNonZero() { +func (suite *TestMiscSuite) TestMergeNonZero() { original := TestMiscStruct{ Field1: "hello", Field2: 100, @@ -187,6 +187,16 @@ func (suite *TestMiscSuite) Test_6_MergeNonZero() { suite.Equal("world", result.field3) } +func (suite *TestMiscSuite) TestBoolPtr() { + suite.Equal(true, *BoolPtr(true)) + suite.Equal(false, *BoolPtr(false)) + a := BoolPtr(true) + b := BoolPtr(true) + // This is a pointer comparison, not a value comparison + suite.False(a == b) + suite.True(*a == *b) +} + func TestMisc(t *testing.T) { suite.Run(t, new(TestMiscSuite)) } diff --git a/src/pkg/zoci/pull.go b/src/pkg/zoci/pull.go index 54aca7d218..1724127ebc 100644 --- a/src/pkg/zoci/pull.go +++ b/src/pkg/zoci/pull.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "path/filepath" - "slices" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/oci" @@ -75,10 +74,9 @@ func (r *Remote) PullPackage(ctx context.Context, destinationDir string, concurr } // LayersFromRequestedComponents returns the descriptors for the given components from the root manifest. -// It also retrieves the descriptors for all image layers that are required by the components. // -// It also respects the `required` flag on components, and will retrieve all necessary layers for required components. -func (r *Remote) LayersFromRequestedComponents(ctx context.Context, requestedComponents []string) (layers []ocispec.Descriptor, err error) { +// It also retrieves the descriptors for all image layers that are required by the components. +func (r *Remote) LayersFromRequestedComponents(ctx context.Context, requestedComponents []types.ZarfComponent) (layers []ocispec.Descriptor, err error) { root, err := r.FetchRoot(ctx) if err != nil { return nil, err @@ -89,23 +87,18 @@ func (r *Remote) LayersFromRequestedComponents(ctx context.Context, requestedCom return nil, err } tarballFormat := "%s.tar" - for _, name := range requestedComponents { + images := map[string]bool{} + for _, rc := range requestedComponents { component := helpers.Find(pkg.Components, func(component types.ZarfComponent) bool { - return component.Name == name + return component.Name == rc.Name }) if component.Name == "" { - return nil, fmt.Errorf("component %s does not exist in this package", name) + return nil, fmt.Errorf("component %s does not exist in this package", rc.Name) } - } - images := map[string]bool{} - for _, component := range pkg.Components { - // If we requested this component, or it is required, we need to pull its images and tarball - if slices.Contains(requestedComponents, component.Name) || component.Required { - for _, image := range component.Images { - images[image] = true - } - layers = append(layers, root.Locate(filepath.Join(layout.ComponentsDir, fmt.Sprintf(tarballFormat, component.Name)))) + for _, image := range component.Images { + images[image] = true } + layers = append(layers, root.Locate(filepath.Join(layout.ComponentsDir, fmt.Sprintf(tarballFormat, component.Name)))) } // Append the sboms.tar layer if it exists // diff --git a/src/test/e2e/00_use_cli_test.go b/src/test/e2e/00_use_cli_test.go index df9ece1db3..5a4df32e8f 100644 --- a/src/test/e2e/00_use_cli_test.go +++ b/src/test/e2e/00_use_cli_test.go @@ -125,7 +125,7 @@ func TestUseCLI(t *testing.T) { stdOut, stdErr, err := e2e.Zarf("package", "create", "packages/distros/eks", "--confirm") require.NoError(t, err, stdOut, stdErr) - path := "zarf-package-distro-eks-multi-0.0.3.tar.zst" + path := fmt.Sprintf("zarf-package-distro-eks-%s-0.0.3.tar.zst", e2e.Arch) stdOut, stdErr, err = e2e.Zarf("package", "deploy", path, "--confirm") require.NoError(t, err, stdOut, stdErr) diff --git a/src/test/e2e/01_component_choice_test.go b/src/test/e2e/01_component_choice_test.go index 9d5030b0c8..8930094f24 100644 --- a/src/test/e2e/01_component_choice_test.go +++ b/src/test/e2e/01_component_choice_test.go @@ -28,11 +28,11 @@ func TestComponentChoice(t *testing.T) { // We currently don't have a pattern to actually test the interactive prompt, so just testing automation for now stdOut, stdErr, err := e2e.Zarf("package", "deploy", path, "--components=first-choice,second-choice", "--confirm") require.Error(t, err, stdOut, stdErr) - require.Contains(t, stdErr, "Component first-choice is using group which has been deprecated", "output should show a warning for group being deprecated.") // Deploy a single choice and expect success stdOut, stdErr, err = e2e.Zarf("package", "deploy", path, "--components=first-choice", "--confirm") require.NoError(t, err, stdOut, stdErr) + require.Contains(t, stdErr, "Component first-choice is using group which has been deprecated", "output should show a warning for group being deprecated.") // Verify the file was created require.FileExists(t, firstFile) diff --git a/src/types/component.go b/src/types/component.go index 82b931b868..028f25202c 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -5,8 +5,6 @@ package types import ( - "reflect" - "github.com/defenseunicorns/zarf/src/pkg/utils/exec" "github.com/defenseunicorns/zarf/src/types/extensions" ) @@ -23,32 +21,33 @@ type ZarfComponent struct { Default bool `json:"default,omitempty" jsonschema:"description=Determines the default Y/N state for installing this component on package deploy"` // Required makes this component mandatory for package deployment - Required bool `json:"required,omitempty" jsonschema:"description=Do not prompt user to install this component, always install on package deploy"` + Required *bool `json:"required,omitempty" jsonschema:"description=Do not prompt user to install this component, always install on package deploy."` // Only include compatible components during package deployment Only ZarfComponentOnlyTarget `json:"only,omitempty" jsonschema:"description=Filter when this component is included in package creation or deployment"` - // Key to match other components to produce a user selector field, used to create a BOOLEAN XOR for a set of components + // DeprecatedGroup is a key to match other components to produce a user selector field, used to create a BOOLEAN XOR for a set of components + // // Note: ignores default and required flags - Group string `json:"group,omitempty" jsonschema:"description=[Deprecated] Create a user selector field based on all components in the same group. This will be removed in Zarf v1.0.0. Consider using 'only.flavor' instead.,deprecated=true"` + DeprecatedGroup string `json:"group,omitempty" jsonschema:"description=[Deprecated] Create a user selector field based on all components in the same group. This will be removed in Zarf v1.0.0. Consider using 'only.flavor' instead.,deprecated=true"` - // (Deprecated) Path to cosign public key for signed online resources + // DeprecatedCosignKeyPath to cosign public key for signed online resources DeprecatedCosignKeyPath string `json:"cosignKeyPath,omitempty" jsonschema:"description=[Deprecated] Specify a path to a public key to validate signed online resources. This will be removed in Zarf v1.0.0.,deprecated=true"` // Import refers to another zarf.yaml package component. Import ZarfComponentImport `json:"import,omitempty" jsonschema:"description=Import a component from another Zarf package"` - // (Deprecated) DeprecatedScripts are custom commands that run before or after package deployment - DeprecatedScripts DeprecatedZarfComponentScripts `json:"scripts,omitempty" jsonschema:"description=[Deprecated] (replaced by actions) Custom commands to run before or after package deployment. This will be removed in Zarf v1.0.0.,deprecated=true"` - - // Files are files to place on disk during deploy - Files []ZarfFile `json:"files,omitempty" jsonschema:"description=Files or folders to place on disk during package deployment"` + // Manifests are raw manifests that get converted into zarf-generated helm charts during deploy + Manifests []ZarfManifest `json:"manifests,omitempty" jsonschema:"description=Kubernetes manifests to be included in a generated Helm chart on package deploy"` // Charts are helm charts to install during package deploy Charts []ZarfChart `json:"charts,omitempty" jsonschema:"description=Helm charts to install during package deploy"` - // Manifests are raw manifests that get converted into zarf-generated helm charts during deploy - Manifests []ZarfManifest `json:"manifests,omitempty" jsonschema:"description=Kubernetes manifests to be included in a generated Helm chart on package deploy"` + // Data packages to push into a running cluster + DataInjections []ZarfDataInjection `json:"dataInjections,omitempty" jsonschema:"description=Datasets to inject into a container in the target cluster"` + + // Files are files to place on disk during deploy + Files []ZarfFile `json:"files,omitempty" jsonschema:"description=Files or folders to place on disk during package deployment"` // Images are the online images needed to be included in the zarf package Images []string `json:"images,omitempty" jsonschema:"description=List of OCI images to include in the package"` @@ -56,16 +55,31 @@ type ZarfComponent struct { // Repos are any git repos that need to be pushed into the git server Repos []string `json:"repos,omitempty" jsonschema:"description=List of git repos to include in the package"` - // Data packages to push into a running cluster - DataInjections []ZarfDataInjection `json:"dataInjections,omitempty" jsonschema:"description=Datasets to inject into a container in the target cluster"` - // Extensions provide additional functionality to a component Extensions extensions.ZarfComponentExtensions `json:"extensions,omitempty" jsonschema:"description=Extend component functionality with additional features"` + // DeprecatedScripts are custom commands that run before or after package deployment + DeprecatedScripts DeprecatedZarfComponentScripts `json:"scripts,omitempty" jsonschema:"description=[Deprecated] (replaced by actions) Custom commands to run before or after package deployment. This will be removed in Zarf v1.0.0.,deprecated=true"` + // Replaces scripts, fine-grained control over commands to run at various stages of a package lifecycle Actions ZarfComponentActions `json:"actions,omitempty" jsonschema:"description=Custom commands to run at various stages of a package lifecycle"` } +// RequiresCluster returns if the component requires a cluster connection to deploy +func (c ZarfComponent) RequiresCluster() bool { + hasImages := len(c.Images) > 0 + hasCharts := len(c.Charts) > 0 + hasManifests := len(c.Manifests) > 0 + hasRepos := len(c.Repos) > 0 + hasDataInjections := len(c.DataInjections) > 0 + + if hasImages || hasCharts || hasManifests || hasRepos || hasDataInjections { + return true + } + + return false +} + // ZarfComponentOnlyTarget filters a component to only show it for a given local OS and cluster. type ZarfComponentOnlyTarget struct { LocalOS string `json:"localOS,omitempty" jsonschema:"description=Only deploy component to specified OS,enum=linux,enum=darwin,enum=windows"` @@ -167,10 +181,10 @@ type ZarfComponentAction struct { // ZarfComponentActionSetVariable represents a variable that is to be set via an action type ZarfComponentActionSetVariable struct { Name string `json:"name" jsonschema:"description=The name to be used for the variable,pattern=^[A-Z0-9_]+$"` + Type VariableType `json:"type,omitempty" jsonschema:"description=Changes the handling of a variable to load contents differently (i.e. from a file rather than as a raw variable - templated files should be kept below 1 MiB),enum=raw,enum=file"` + Pattern string `json:"pattern,omitempty" jsonschema:"description=An optional regex pattern that a variable value must match before a package deployment can continue."` Sensitive bool `json:"sensitive,omitempty" jsonschema:"description=Whether to mark this variable as sensitive to not print it in the Zarf log"` AutoIndent bool `json:"autoIndent,omitempty" jsonschema:"description=Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_."` - Pattern string `json:"pattern,omitempty" jsonschema:"description=An optional regex pattern that a variable value must match before a package deployment can continue."` - Type VariableType `json:"type,omitempty" jsonschema:"description=Changes the handling of a variable to load contents differently (i.e. from a file rather than as a raw variable - templated files should be kept below 1 MiB),enum=raw,enum=file"` } // ZarfComponentActionWait specifies a condition to wait for before continuing @@ -217,30 +231,3 @@ type ZarfComponentImport struct { // For further explanation see https://regex101.com/r/nxX8vx/1 URL string `json:"url,omitempty" jsonschema:"description=[beta] The URL to a Zarf package to import via OCI,pattern=^oci://.*$"` } - -// IsEmpty returns if the components fields (other than the fields we were told to ignore) are empty or set to the types zero-value -func (c *ZarfComponent) IsEmpty(fieldsToIgnore []string) bool { - // Make a map for the fields we are going to ignore - ignoredFieldsMap := make(map[string]bool) - for _, field := range fieldsToIgnore { - ignoredFieldsMap[field] = true - } - - // Get a value representation of the component - componentReflectValue := reflect.Indirect(reflect.ValueOf(c)) - - // Loop through all of the Components struct fields - for i := 0; i < componentReflectValue.NumField(); i++ { - // If we were told to ignore this field, continue on.. - if ignoredFieldsMap[componentReflectValue.Type().Field(i).Name] { - continue - } - - // Check if this field is empty/zero - if !componentReflectValue.Field(i).IsZero() { - return false - } - } - - return true -} diff --git a/zarf.schema.json b/zarf.schema.json index 1a31e468f8..f61482b7a4 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -304,18 +304,13 @@ "$ref": "#/definitions/ZarfComponentImport", "description": "Import a component from another Zarf package" }, - "scripts": { - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/DeprecatedZarfComponentScripts", - "description": "[Deprecated] (replaced by actions) Custom commands to run before or after package deployment. This will be removed in Zarf v1.0.0." - }, - "files": { + "manifests": { "items": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfFile" + "$ref": "#/definitions/ZarfManifest" }, "type": "array", - "description": "Files or folders to place on disk during package deployment" + "description": "Kubernetes manifests to be included in a generated Helm chart on package deploy" }, "charts": { "items": { @@ -325,13 +320,21 @@ "type": "array", "description": "Helm charts to install during package deploy" }, - "manifests": { + "dataInjections": { "items": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfManifest" + "$ref": "#/definitions/ZarfDataInjection" }, "type": "array", - "description": "Kubernetes manifests to be included in a generated Helm chart on package deploy" + "description": "Datasets to inject into a container in the target cluster" + }, + "files": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/ZarfFile" + }, + "type": "array", + "description": "Files or folders to place on disk during package deployment" }, "images": { "items": { @@ -347,19 +350,16 @@ "type": "array", "description": "List of git repos to include in the package" }, - "dataInjections": { - "items": { - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/ZarfDataInjection" - }, - "type": "array", - "description": "Datasets to inject into a container in the target cluster" - }, "extensions": { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/ZarfComponentExtensions", "description": "Extend component functionality with additional features" }, + "scripts": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/DeprecatedZarfComponentScripts", + "description": "[Deprecated] (replaced by actions) Custom commands to run before or after package deployment. This will be removed in Zarf v1.0.0." + }, "actions": { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/ZarfComponentActions", @@ -524,18 +524,6 @@ "type": "string", "description": "The name to be used for the variable" }, - "sensitive": { - "type": "boolean", - "description": "Whether to mark this variable as sensitive to not print it in the Zarf log" - }, - "autoIndent": { - "type": "boolean", - "description": "Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_." - }, - "pattern": { - "type": "string", - "description": "An optional regex pattern that a variable value must match before a package deployment can continue." - }, "type": { "enum": [ "raw", @@ -543,6 +531,18 @@ ], "type": "string", "description": "Changes the handling of a variable to load contents differently (i.e. from a file rather than as a raw variable - templated files should be kept below 1 MiB)" + }, + "pattern": { + "type": "string", + "description": "An optional regex pattern that a variable value must match before a package deployment can continue." + }, + "sensitive": { + "type": "boolean", + "description": "Whether to mark this variable as sensitive to not print it in the Zarf log" + }, + "autoIndent": { + "type": "boolean", + "description": "Whether to automatically indent the variable's value (if multiline) when templating. Based on the number of chars before the start of ###ZARF_VAR_." } }, "additionalProperties": false,