From 44e82442260f2635ce4ddc7c848e83912dfe05bd Mon Sep 17 00:00:00 2001 From: AbrohamLincoln <64557460+AbrohamLincoln@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:41:24 -0400 Subject: [PATCH 1/4] feat: add extraEnvVars value to registry chart abd REGISTRY_EXTRA_ENVS to registry package (#1994) ## Description Alter registry Helm chart to allow the PVC mount and filesystem storage driver to be toggled off. Add extraEnvVars value and templating to helm chart to allow for arbitrary environment variables. Add REGISTRY_PVC_ENABLED variable to registry package to toggle PVC creation and filesystem storage driver off/on. Add REGISTRY_EXTRA_ENVS variable to registry package to expose extraEnvVars helm chart value. Example of how to back the registry with S3: ```bash zarf init --confirm --set REGISTRY_PVC_ENABLED=false --set REGISTRY_EXTRA_ENVS=' - name: REGISTRY_STORAGE value: s3 - name: REGISTRY_STORAGE_S3_REGION value: us-east-2 - name: REGISTRY_STORAGE_S3_BUCKET value: registry-test-bucket-zarf - name: REGISTRY_STORAGE_S3_ACCESSKEY value: AKIAIOSFODNN7EXAMPLE - name: REGISTRY_STORAGE_S3_SECRETKEY value: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY ' ``` ## Related Issue Fixes #1993 ## Type of change - [ ] 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 - [ ] Test, docs, adr added or updated as needed - [ ] [Contributor Guide Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow) followed --- packages/zarf-registry/chart/templates/deployment.yaml | 9 +++++++++ packages/zarf-registry/chart/values.yaml | 5 +++++ packages/zarf-registry/registry-values.yaml | 4 ++++ packages/zarf-registry/zarf.yaml | 9 +++++++++ 4 files changed, 27 insertions(+) diff --git a/packages/zarf-registry/chart/templates/deployment.yaml b/packages/zarf-registry/chart/templates/deployment.yaml index d91faeeeaf..f899f10c04 100644 --- a/packages/zarf-registry/chart/templates/deployment.yaml +++ b/packages/zarf-registry/chart/templates/deployment.yaml @@ -61,15 +61,22 @@ spec: value: "Registry Realm" - name: REGISTRY_AUTH_HTPASSWD_PATH value: "/etc/docker/registry/htpasswd" +{{- if .Values.persistence.enabled }} - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY value: "/var/lib/registry" +{{- end }} {{- if .Values.persistence.deleteEnabled }} - name: REGISTRY_STORAGE_DELETE_ENABLED value: "true" +{{- end }} +{{- with .Values.extraEnvVars }} +{{ toYaml . | indent 12 }} {{- end }} volumeMounts: +{{- if .Values.persistence.enabled }} - name: data mountPath: /var/lib/registry/ +{{- end }} - name: config mountPath: "/etc/docker/registry" affinity: @@ -97,6 +104,8 @@ spec: path: config.yml - key: htpasswd path: htpasswd +{{- if .Values.persistence.enabled }} - name: data persistentVolumeClaim: claimName: {{ if .Values.persistence.existingClaim }}{{ .Values.persistence.existingClaim }}{{- else }}{{ template "docker-registry.fullname" . }}{{- end }} +{{- end }} diff --git a/packages/zarf-registry/chart/values.yaml b/packages/zarf-registry/chart/values.yaml index 24e402e59e..b2018469b8 100644 --- a/packages/zarf-registry/chart/values.yaml +++ b/packages/zarf-registry/chart/values.yaml @@ -53,3 +53,8 @@ autoscaling: minReplicas: 1 maxReplicas: 5 targetCPUUtilizationPercentage: 80 + +extraEnvVars: [] +## Additional ENV variables to set +# - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY +# value: "/var/lib/example" diff --git a/packages/zarf-registry/registry-values.yaml b/packages/zarf-registry/registry-values.yaml index 9295ca04a8..7f43da9c67 100644 --- a/packages/zarf-registry/registry-values.yaml +++ b/packages/zarf-registry/registry-values.yaml @@ -1,4 +1,5 @@ persistence: + enabled: ###ZARF_VAR_REGISTRY_PVC_ENABLED### storageClass: "###ZARF_STORAGE_CLASS###" size: "###ZARF_VAR_REGISTRY_PVC_SIZE###" existingClaim: "###ZARF_VAR_REGISTRY_EXISTING_PVC###" @@ -38,3 +39,6 @@ autoscaling: minReplicas: "###ZARF_VAR_REGISTRY_HPA_MIN###" maxReplicas: "###ZARF_VAR_REGISTRY_HPA_MAX###" targetCPUUtilizationPercentage: 80 + +extraEnvVars: + ###ZARF_VAR_REGISTRY_EXTRA_ENVS### diff --git a/packages/zarf-registry/zarf.yaml b/packages/zarf-registry/zarf.yaml index dd622acadd..79255b2538 100644 --- a/packages/zarf-registry/zarf.yaml +++ b/packages/zarf-registry/zarf.yaml @@ -7,6 +7,10 @@ variables: description: "Optional: Use an existing PVC for the registry instead of creating a new one. If this is set, the REGISTRY_PVC_SIZE variable will be ignored." default: "" + - name: REGISTRY_PVC_ENABLED + description: Toggle the creation and use of a PVC off/on + default: "true" + - name: REGISTRY_PVC_SIZE description: The size of the persistent volume claim for the registry default: 20Gi @@ -43,6 +47,11 @@ variables: description: Enable the Horizontal Pod Autoscaler for the registry default: "true" + - name: REGISTRY_EXTRA_ENVS + description: Array of additional environment variables passed to the registry container + default: "" + autoIndent: true + constants: - name: REGISTRY_IMAGE value: "###ZARF_PKG_TMPL_REGISTRY_IMAGE###" From c5a9df4097f100b9da5436c438229436f4cddc86 Mon Sep 17 00:00:00 2001 From: Case Wylie Date: Mon, 28 Aug 2023 15:35:51 -0400 Subject: [PATCH 2/4] Fix potential for double transform of images (i.e with operators that self-reference) (#1989) ## Description Checks if image.Host has already been patched ## Related Issue Fixes #1988 Relates to # ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] 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: Case Wylie Co-authored-by: Wayne Starr --- src/pkg/transform/image.go | 12 ++++++++++++ src/pkg/transform/image_test.go | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/src/pkg/transform/image.go b/src/pkg/transform/image.go index af2c54a4af..2d511c1233 100644 --- a/src/pkg/transform/image.go +++ b/src/pkg/transform/image.go @@ -6,6 +6,7 @@ package transform import ( "fmt" + "strings" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/distribution/distribution/reference" @@ -29,6 +30,11 @@ func ImageTransformHost(targetHost, srcReference string) (string, error) { return "", err } + // check if image has already been transformed + if strings.HasPrefix(targetHost, image.Host) { + return srcReference, nil + } + // Generate a crc32 hash of the image host + name checksum := helpers.GetCRCHash(image.Name) @@ -46,6 +52,12 @@ func ImageTransformHostWithoutChecksum(targetHost, srcReference string) (string, if err != nil { return "", err } + + // check if image has already been transformed + if strings.HasPrefix(targetHost, image.Host) { + return srcReference, nil + } + return fmt.Sprintf("%s/%s%s", targetHost, image.Path, image.TagOrDigest), nil } diff --git a/src/pkg/transform/image_test.go b/src/pkg/transform/image_test.go index 1eaef83468..819f9f672e 100644 --- a/src/pkg/transform/image_test.go +++ b/src/pkg/transform/image_test.go @@ -17,6 +17,7 @@ var imageRefs = []string{ "defenseunicorns/zarf-agent@sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de", "ghcr.io/stefanprodan/podinfo:6.3.3", "registry1.dso.mil/ironbank/opensource/defenseunicorns/zarf/zarf-agent:v0.25.0", + "gitlab.com/project/gitea/gitea:1.19.3-rootless-zarf-3431384023", } var badImageRefs = []string{ @@ -34,6 +35,7 @@ func TestImageTransformHost(t *testing.T) { "gitlab.com/project/defenseunicorns/zarf-agent@sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de", "gitlab.com/project/stefanprodan/podinfo:6.3.3-zarf-2985051089", "gitlab.com/project/ironbank/opensource/defenseunicorns/zarf/zarf-agent:v0.25.0-zarf-2003217571", + "gitlab.com/project/gitea/gitea:1.19.3-rootless-zarf-3431384023", } for idx, ref := range imageRefs { @@ -56,6 +58,7 @@ func TestImageTransformHostWithoutChecksum(t *testing.T) { "gitlab.com/project/defenseunicorns/zarf-agent@sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de", "gitlab.com/project/stefanprodan/podinfo:6.3.3", "gitlab.com/project/ironbank/opensource/defenseunicorns/zarf/zarf-agent:v0.25.0", + "gitlab.com/project/gitea/gitea:1.19.3-rootless-zarf-3431384023", } for idx, ref := range imageRefs { @@ -78,6 +81,7 @@ func TestParseImageRef(t *testing.T) { {"docker.io/", "defenseunicorns/zarf-agent", "", "sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de"}, {"ghcr.io/", "stefanprodan/podinfo", "6.3.3", ""}, {"registry1.dso.mil/", "ironbank/opensource/defenseunicorns/zarf/zarf-agent", "v0.25.0", ""}, + {"gitlab.com/", "project/gitea/gitea", "1.19.3-rootless-zarf-3431384023", ""}, } for idx, ref := range imageRefs { From cde0069e597e2c83af8c15577e93449b26848fd9 Mon Sep 17 00:00:00 2001 From: razzle Date: Mon, 28 Aug 2023 17:17:00 -0500 Subject: [PATCH 3/4] 1773 Publish Zarf init + skeleton as OCI on release (#1990) ## Related Issue Fixes #1773 ## Type of change - [ ] 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 - [ ] Test, docs, adr added or updated as needed - [ ] [Contributor Guide Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow) followed --------- Signed-off-by: razzle Co-authored-by: Wayne Starr --- .github/workflows/release.yml | 5 +++++ Makefile | 5 +++++ src/cmd/initialize.go | 13 +++++++------ src/cmd/tools/zarf.go | 8 ++++---- src/config/lang/english.go | 8 ++++---- src/pkg/oci/utils.go | 36 +++++++++++++++++++++++++++++++++++ src/pkg/packager/common.go | 5 ----- src/pkg/packager/publish.go | 4 ++++ 8 files changed, 65 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19f4ed328b..019f5fdfbe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,6 +63,11 @@ jobs: make release-init-package ARCH=amd64 AGENT_IMAGE_TAG=$GITHUB_REF_NAME make release-init-package ARCH=arm64 AGENT_IMAGE_TAG=$GITHUB_REF_NAME + - name: Publish Init Package as OCI and Skeleton + run: | + make publish-init-package ARCH=amd64 REPOSITORY_URL=ghcr.io/defenseunicorns/packages + make publish-init-package ARCH=arm64 REPOSITORY_URL=ghcr.io/defenseunicorns/packages + # Create a CVE report based on this build - name: Create release time CVE report run: "make cve-report" diff --git a/Makefile b/Makefile index d82bf23151..cd2e099bec 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,11 @@ ib-init-package: --set REGISTRY_IMAGE="ironbank/opensource/docker/registry-v2" \ --set REGISTRY_IMAGE_TAG="2.8.2" +# INTERNAL: used to publish the init package +publish-init-package: + $(ZARF_BIN) package publish build/zarf-init-$(ARCH)-$(CLI_VERSION).tar.zst oci://$(REPOSITORY_URL) + $(ZARF_BIN) package publish . oci://$(REPOSITORY_URL) + build-examples: ## Build all of the example packages @test -s $(ZARF_BIN) || $(MAKE) build-cli diff --git a/src/cmd/initialize.go b/src/cmd/initialize.go index 27b0e202ec..dceff5b3b0 100644 --- a/src/cmd/initialize.go +++ b/src/cmd/initialize.go @@ -17,6 +17,7 @@ import ( "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/oci" "github.com/defenseunicorns/zarf/src/pkg/packager" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" @@ -114,17 +115,17 @@ func downloadInitPackage(downloadCacheTarget string) error { } var confirmDownload bool - url := packager.GetInitPackageRemote("") + url := oci.GetInitPackageURL(config.GetArch(), config.CLIVersion) // Give the user the choice to download the init-package and note that this does require an internet connection - message.Question(fmt.Sprintf(lang.CmdInitDownloadAsk, url)) + message.Question(fmt.Sprintf(lang.CmdInitPullAsk, url)) - message.Note(lang.CmdInitDownloadNote) + message.Note(lang.CmdInitPullNote) // Prompt the user if --confirm not specified if !confirmDownload { prompt := &survey.Confirm{ - Message: lang.CmdInitDownloadConfirm, + Message: lang.CmdInitPullConfirm, } if err := survey.AskOne(prompt, &confirmDownload); err != nil { return fmt.Errorf(lang.ErrConfirmCancel, err.Error()) @@ -133,10 +134,10 @@ func downloadInitPackage(downloadCacheTarget string) error { // If the user wants to download the init-package, download it if confirmDownload { - return utils.DownloadToFile(url, downloadCacheTarget, "") + return oci.DownloadPackageTarball(url, downloadCacheTarget, config.CommonOptions.OCIConcurrency) } // Otherwise, exit and tell the user to manually download the init-package - return errors.New(lang.CmdInitDownloadErrManual) + return errors.New(lang.CmdInitPullErrManual) } func validateInitFlags() error { diff --git a/src/cmd/tools/zarf.go b/src/cmd/tools/zarf.go index b09922da98..3fd96c4654 100644 --- a/src/cmd/tools/zarf.go +++ b/src/cmd/tools/zarf.go @@ -17,9 +17,9 @@ import ( "github.com/defenseunicorns/zarf/src/internal/packager/git" "github.com/defenseunicorns/zarf/src/internal/packager/helm" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/pkg/oci" "github.com/defenseunicorns/zarf/src/pkg/packager" "github.com/defenseunicorns/zarf/src/pkg/pki" - "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" "github.com/sigstore/cosign/pkg/cosign" @@ -174,9 +174,9 @@ var downloadInitCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { initPackageName := packager.GetInitPackageName("") target := filepath.Join(outputDirectory, initPackageName) - url := packager.GetInitPackageRemote("") - err := utils.DownloadToFile(url, target, "") - if err != nil { + url := oci.GetInitPackageURL(config.GetArch(), config.CLIVersion) + + if err := oci.DownloadPackageTarball(url, target, config.CommonOptions.OCIConcurrency); err != nil { message.Fatalf(err, lang.CmdToolsDownloadInitErr, err.Error()) } }, diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 10a65bb925..fed1a43312 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -144,10 +144,10 @@ const ( CmdInitErrValidateArtifact = "the 'artifact-push-username' and 'artifact-push-token' flags must be provided if the 'artifact-url' flag is provided" CmdInitErrUnableCreateCache = "Unable to create the cache directory: %s" - CmdInitDownloadAsk = "It seems the init package could not be found locally, but can be downloaded from %s" - CmdInitDownloadNote = "Note: This will require an internet connection." - CmdInitDownloadConfirm = "Do you want to download this init package?" - CmdInitDownloadErrManual = "download the init package manually and place it in the current working directory" + CmdInitPullAsk = "It seems the init package could not be found locally, but can be pulled from oci://%s" + CmdInitPullNote = "Note: This will require an internet connection." + CmdInitPullConfirm = "Do you want to pull this init package?" + CmdInitPullErrManual = "pull the init package manually and place it in the current working directory" CmdInitFlagSet = "Specify deployment variables to set on the command line (KEY=value)" diff --git a/src/pkg/oci/utils.go b/src/pkg/oci/utils.go index a74880f922..876725b15b 100644 --- a/src/pkg/oci/utils.go +++ b/src/pkg/oci/utils.go @@ -8,12 +8,15 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" "strings" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" + "github.com/mholt/archiver/v3" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/registry" ) @@ -91,3 +94,36 @@ func RemoveDuplicateDescriptors(descriptors []ocispec.Descriptor) []ocispec.Desc } return list } + +// GetInitPackageURL returns the URL for the init package for the given architecture and version. +func GetInitPackageURL(arch, version string) string { + return fmt.Sprintf("ghcr.io/defenseunicorns/packages/init:%s-%s", version, arch) +} + +// DownloadPackageTarball downloads the given OCI package and saves as a tarball. +func DownloadPackageTarball(url, destinationTarball string, concurrency int) error { + remote, err := NewOrasRemote(url) + if err != nil { + return err + } + + tmp, err := utils.MakeTempDir() + if err != nil { + return err + } + defer os.RemoveAll(tmp) + + _, err = remote.PullPackage(tmp, concurrency) + if err != nil { + return err + } + + allTheLayers, err := filepath.Glob(filepath.Join(tmp, "*")) + if err != nil { + return err + } + + _ = os.Remove(destinationTarball) + + return archiver.Archive(allTheLayers, destinationTarball) +} diff --git a/src/pkg/packager/common.go b/src/pkg/packager/common.go index 1a986ffc58..4c31f34757 100644 --- a/src/pkg/packager/common.go +++ b/src/pkg/packager/common.go @@ -157,11 +157,6 @@ func (p *Packager) GetPackageName() string { return fmt.Sprintf("%s.%s", packageFileName, suffix) } -// GetInitPackageRemote returns the URL for a remote init package for the given architecture -func GetInitPackageRemote(arch string) string { - return fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", config.GithubProject, config.CLIVersion, GetInitPackageName(arch)) -} - // ClearTempPaths removes the temp directory and any files within it. func (p *Packager) ClearTempPaths() { // Remove the temp directory, but don't throw an error if it fails diff --git a/src/pkg/packager/publish.go b/src/pkg/packager/publish.go index 1bc07147d0..09d7d763b5 100644 --- a/src/pkg/packager/publish.go +++ b/src/pkg/packager/publish.go @@ -103,6 +103,10 @@ func (p *Packager) loadSkeleton() error { return fmt.Errorf("unable to read the zarf.yaml in %s: %s", base, err.Error()) } + if p.cfg.Pkg.Kind == types.ZarfInitConfig { + p.cfg.Pkg.Metadata.Version = config.CLIVersion + } + err = p.composeComponents() if err != nil { return err From 8eac4e5815beb8c1a6c7c8b854b8c46f61ba86ce Mon Sep 17 00:00:00 2001 From: Wayne Starr Date: Mon, 28 Aug 2023 18:09:51 -0500 Subject: [PATCH 4/4] Initial inclusion of the helm repo / dependency commands in Zarf (#1991) ## Description This includes the `helm` CLI repo and dependency commands in Zarf so that they can be used by people without helm installed. ## Related Issue Fixes #1985 ## Type of change - [ ] 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 --- .../100-cli-commands/zarf_tools.md | 1 + .../100-cli-commands/zarf_tools_helm.md | 35 ++ .../zarf_tools_helm_dependency.md | 87 ++++ .../zarf_tools_helm_dependency_build.md | 54 +++ .../zarf_tools_helm_dependency_list.md | 50 +++ .../zarf_tools_helm_dependency_update.md | 58 +++ .../100-cli-commands/zarf_tools_helm_repo.md | 47 +++ .../zarf_tools_helm_repo_add.md | 49 +++ .../zarf_tools_helm_repo_index.md | 53 +++ .../zarf_tools_helm_repo_list.md | 39 ++ .../zarf_tools_helm_repo_remove.md | 38 ++ .../zarf_tools_helm_repo_update.md | 50 +++ go.mod | 7 +- go.sum | 2 + hack/check-zarf-docs-and-schema.sh | 2 +- src/cmd/common/vendor.go | 2 + src/cmd/internal.go | 24 ++ src/cmd/tools/helm.go | 29 ++ src/cmd/tools/helm/LICENSE | 202 ++++++++++ src/cmd/tools/helm/dependency.go | 127 ++++++ src/cmd/tools/helm/dependency_build.go | 98 +++++ src/cmd/tools/helm/dependency_update.go | 89 ++++ src/cmd/tools/helm/flags.go | 109 +++++ src/cmd/tools/helm/load_plugins.go | 381 ++++++++++++++++++ src/cmd/tools/helm/repo.go | 62 +++ src/cmd/tools/helm/repo_add.go | 224 ++++++++++ src/cmd/tools/helm/repo_index.go | 114 ++++++ src/cmd/tools/helm/repo_list.go | 141 +++++++ src/cmd/tools/helm/repo_remove.go | 101 +++++ src/cmd/tools/helm/repo_update.go | 172 ++++++++ src/cmd/tools/helm/root.go | 280 +++++++++++++ src/config/lang/english.go | 3 + src/internal/packager/helm/repo.go | 20 +- src/internal/packager/helm/utils.go | 3 +- src/test/e2e/25_helm_test.go | 10 +- .../packages/25-evil-chart-deps/.gitignore | 1 + .../25-evil-chart-deps/bad-chart/Chart.yaml | 16 + .../25-evil-chart-deps/good-chart/Chart.yaml | 15 + .../packages/25-evil-chart-deps/zarf.yaml | 21 + 39 files changed, 2805 insertions(+), 11 deletions(-) create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_build.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_list.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_update.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_add.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_index.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_list.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_remove.md create mode 100644 docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_update.md create mode 100644 src/cmd/tools/helm.go create mode 100644 src/cmd/tools/helm/LICENSE create mode 100644 src/cmd/tools/helm/dependency.go create mode 100644 src/cmd/tools/helm/dependency_build.go create mode 100644 src/cmd/tools/helm/dependency_update.go create mode 100644 src/cmd/tools/helm/flags.go create mode 100644 src/cmd/tools/helm/load_plugins.go create mode 100644 src/cmd/tools/helm/repo.go create mode 100644 src/cmd/tools/helm/repo_add.go create mode 100644 src/cmd/tools/helm/repo_index.go create mode 100644 src/cmd/tools/helm/repo_list.go create mode 100644 src/cmd/tools/helm/repo_remove.go create mode 100644 src/cmd/tools/helm/repo_update.go create mode 100644 src/cmd/tools/helm/root.go create mode 100644 src/test/packages/25-evil-chart-deps/.gitignore create mode 100644 src/test/packages/25-evil-chart-deps/bad-chart/Chart.yaml create mode 100644 src/test/packages/25-evil-chart-deps/good-chart/Chart.yaml create mode 100644 src/test/packages/25-evil-chart-deps/zarf.yaml diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools.md index 0b5caa210a..fb8193952c 100644 --- a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools.md +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools.md @@ -31,6 +31,7 @@ Collection of additional tools to make airgap easier * [zarf tools gen-key](zarf_tools_gen-key.md) - Generates a cosign public/private keypair that can be used to sign packages * [zarf tools gen-pki](zarf_tools_gen-pki.md) - Generates a Certificate Authority and PKI chain of trust for the given host * [zarf tools get-creds](zarf_tools_get-creds.md) - Displays a table of credentials for deployed Zarf services. Pass a service key to get a single credential +* [zarf tools helm](zarf_tools_helm.md) - Subset of the Helm CLI included with Zarf to help manage helm charts. * [zarf tools kubectl](zarf_tools_kubectl.md) - Kubectl command. See https://kubernetes.io/docs/reference/kubectl/overview/ for more information. * [zarf tools monitor](zarf_tools_monitor.md) - Launches a terminal UI to monitor the connected cluster using K9s. * [zarf tools registry](zarf_tools_registry.md) - Tools for working with container registries using go-containertools diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm.md new file mode 100644 index 0000000000..a0c5884216 --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm.md @@ -0,0 +1,35 @@ +# zarf tools helm + + +Subset of the Helm CLI included with Zarf to help manage helm charts. + +## Synopsis + +Subset of the Helm CLI that includes the repo and dependency commands for managing helm charts destined for the air gap. + +## Options + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + -h, --help help for helm + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools](zarf_tools.md) - Collection of additional tools to make airgap easier +* [zarf tools helm dependency](zarf_tools_helm_dependency.md) - manage a chart's dependencies +* [zarf tools helm repo](zarf_tools_helm_repo.md) - add, list, remove, update, and index chart repositories diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency.md new file mode 100644 index 0000000000..88caf3abca --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency.md @@ -0,0 +1,87 @@ +# zarf tools helm dependency + + +manage a chart's dependencies + +## Synopsis + + +Manage the dependencies of a chart. + +Helm charts store their dependencies in 'charts/'. For chart developers, it is +often easier to manage dependencies in 'Chart.yaml' which declares all +dependencies. + +The dependency commands operate on that file, making it easy to synchronize +between the desired dependencies and the actual dependencies stored in the +'charts/' directory. + +For example, this Chart.yaml declares two dependencies: + + # Chart.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "https://example.com/charts" + - name: memcached + version: "3.2.1" + repository: "https://another.example.com/charts" + + +The 'name' should be the name of a chart, where that name must match the name +in that chart's 'Chart.yaml' file. + +The 'version' field should contain a semantic version or version range. + +The 'repository' URL should point to a Chart Repository. Helm expects that by +appending '/index.yaml' to the URL, it should be able to retrieve the chart +repository's index. Note: 'repository' can be an alias. The alias must start +with 'alias:' or '@'. + +Starting from 2.2.0, repository can be defined as the path to the directory of +the dependency charts stored locally. The path should start with a prefix of +"file://". For example, + + # Chart.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "file://../dependency_chart/nginx" + +If the dependency chart is retrieved locally, it is not required to have the +repository added to helm by "helm add repo". Version matching is also supported +for this case. + + +## Options + +``` + -h, --help help for dependency +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm](zarf_tools_helm.md) - Subset of the Helm CLI included with Zarf to help manage helm charts. +* [zarf tools helm dependency build](zarf_tools_helm_dependency_build.md) - rebuild the charts/ directory based on the Chart.lock file +* [zarf tools helm dependency list](zarf_tools_helm_dependency_list.md) - list the dependencies for the given chart +* [zarf tools helm dependency update](zarf_tools_helm_dependency_update.md) - update charts/ based on the contents of Chart.yaml diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_build.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_build.md new file mode 100644 index 0000000000..bc13e3939e --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_build.md @@ -0,0 +1,54 @@ +# zarf tools helm dependency build + + +rebuild the charts/ directory based on the Chart.lock file + +## Synopsis + + +Build out the charts/ directory from the Chart.lock file. + +Build is used to reconstruct a chart's dependencies to the state specified in +the lock file. This will not re-negotiate dependencies, as 'zarf tools helm dependency update' +does. + +If no lock file is found, 'zarf tools helm dependency build' will mirror the behavior +of 'zarf tools helm dependency update'. + + +``` +zarf tools helm dependency build CHART [flags] +``` + +## Options + +``` + -h, --help help for build + --keyring string keyring containing public keys + --skip-refresh do not refresh the local repository cache + --verify verify the packages against signatures +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm dependency](zarf_tools_helm_dependency.md) - manage a chart's dependencies diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_list.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_list.md new file mode 100644 index 0000000000..3473190f3f --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_list.md @@ -0,0 +1,50 @@ +# zarf tools helm dependency list + + +list the dependencies for the given chart + +## Synopsis + + +List all of the dependencies declared in a chart. + +This can take chart archives and chart directories as input. It will not alter +the contents of a chart. + +This will produce an error if the chart cannot be loaded. + + +``` +zarf tools helm dependency list CHART [flags] +``` + +## Options + +``` + -h, --help help for list + --max-col-width uint maximum column width for output table (default 80) +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm dependency](zarf_tools_helm_dependency.md) - manage a chart's dependencies diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_update.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_update.md new file mode 100644 index 0000000000..193759ad36 --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_dependency_update.md @@ -0,0 +1,58 @@ +# zarf tools helm dependency update + + +update charts/ based on the contents of Chart.yaml + +## Synopsis + + +Update the on-disk dependencies to mirror Chart.yaml. + +This command verifies that the required charts, as expressed in 'Chart.yaml', +are present in 'charts/' and are at an acceptable version. It will pull down +the latest charts that satisfy the dependencies, and clean up old dependencies. + +On successful update, this will generate a lock file that can be used to +rebuild the dependencies to an exact version. + +Dependencies are not required to be represented in 'Chart.yaml'. For that +reason, an update command will not remove charts unless they are (a) present +in the Chart.yaml file, but (b) at the wrong version. + + +``` +zarf tools helm dependency update CHART [flags] +``` + +## Options + +``` + -h, --help help for update + --keyring string keyring containing public keys + --skip-refresh do not refresh the local repository cache + --verify verify the packages against signatures +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm dependency](zarf_tools_helm_dependency.md) - manage a chart's dependencies diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo.md new file mode 100644 index 0000000000..765fc26047 --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo.md @@ -0,0 +1,47 @@ +# zarf tools helm repo + + +add, list, remove, update, and index chart repositories + +## Synopsis + + +This command consists of multiple subcommands to interact with chart repositories. + +It can be used to add, remove, list, and index chart repositories. + + +## Options + +``` + -h, --help help for repo +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm](zarf_tools_helm.md) - Subset of the Helm CLI included with Zarf to help manage helm charts. +* [zarf tools helm repo add](zarf_tools_helm_repo_add.md) - add a chart repository +* [zarf tools helm repo index](zarf_tools_helm_repo_index.md) - generate an index file given a directory containing packaged charts +* [zarf tools helm repo list](zarf_tools_helm_repo_list.md) - list chart repositories +* [zarf tools helm repo remove](zarf_tools_helm_repo_remove.md) - remove one or more chart repositories +* [zarf tools helm repo update](zarf_tools_helm_repo_update.md) - update information of available charts locally from chart repositories diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_add.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_add.md new file mode 100644 index 0000000000..aa9e348d87 --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_add.md @@ -0,0 +1,49 @@ +# zarf tools helm repo add + + +add a chart repository + +``` +zarf tools helm repo add [NAME] [URL] [flags] +``` + +## Options + +``` + --allow-deprecated-repos by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior + --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle + --cert-file string identify HTTPS client using this SSL certificate file + --force-update replace (overwrite) the repo if it already exists + -h, --help help for add + --insecure-skip-tls-verify skip tls certificate checks for the repository + --key-file string identify HTTPS client using this SSL key file + --no-update Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update. + --pass-credentials pass credentials to all domains + --password string chart repository password + --password-stdin read chart repository password from stdin + --username string chart repository username +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm repo](zarf_tools_helm_repo.md) - add, list, remove, update, and index chart repositories diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_index.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_index.md new file mode 100644 index 0000000000..2db7618084 --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_index.md @@ -0,0 +1,53 @@ +# zarf tools helm repo index + + +generate an index file given a directory containing packaged charts + +## Synopsis + + +Read the current directory and generate an index file based on the charts found. + +This tool is used for creating an 'index.yaml' file for a chart repository. To +set an absolute URL to the charts, use '--url' flag. + +To merge the generated index with an existing index file, use the '--merge' +flag. In this case, the charts found in the current directory will be merged +into the existing index, with local charts taking priority over existing charts. + + +``` +zarf tools helm repo index [DIR] [flags] +``` + +## Options + +``` + -h, --help help for index + --merge string merge the generated index into the given index + --url string url of chart repository +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm repo](zarf_tools_helm_repo.md) - add, list, remove, update, and index chart repositories diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_list.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_list.md new file mode 100644 index 0000000000..51b578ff0c --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_list.md @@ -0,0 +1,39 @@ +# zarf tools helm repo list + + +list chart repositories + +``` +zarf tools helm repo list [flags] +``` + +## Options + +``` + -h, --help help for list + -o, --output format prints the output in the specified format. Allowed values: table, json, yaml (default table) +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm repo](zarf_tools_helm_repo.md) - add, list, remove, update, and index chart repositories diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_remove.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_remove.md new file mode 100644 index 0000000000..e9240e50da --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_remove.md @@ -0,0 +1,38 @@ +# zarf tools helm repo remove + + +remove one or more chart repositories + +``` +zarf tools helm repo remove [REPO1 [REPO2 ...]] [flags] +``` + +## Options + +``` + -h, --help help for remove +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm repo](zarf_tools_helm_repo.md) - add, list, remove, update, and index chart repositories diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_update.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_update.md new file mode 100644 index 0000000000..f7de5bc583 --- /dev/null +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_update.md @@ -0,0 +1,50 @@ +# zarf tools helm repo update + + +update information of available charts locally from chart repositories + +## Synopsis + + +Update gets the latest information about charts from the respective chart repositories. +Information is cached locally, where it is used by commands like 'helm search'. + +You can optionally specify a list of repositories you want to update. + $ zarf tools helm repo update ... +To update all the repositories, use 'zarf tools helm repo update'. + + +``` +zarf tools helm repo update [REPO1 [REPO2 ...]] [flags] +``` + +## Options + +``` + --fail-on-repo-update-fail update fails if any of the repository updates fail + -h, --help help for update +``` + +## Options inherited from parent commands + +``` + --burst-limit int client-side default throttling limit (default 100) + --debug enable verbose output + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --registry-config string path to the registry config file + --repository-cache string path to the file containing cached repository indexes + --repository-config string path to the file containing repository names and URLs +``` + +## SEE ALSO + +* [zarf tools helm repo](zarf_tools_helm_repo.md) - add, list, remove, update, and index chart repositories diff --git a/go.mod b/go.mod index 5da2e88a80..2550cfce1b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,9 @@ require ( github.com/go-git/go-git/v5 v5.7.0 github.com/go-logr/logr v1.2.4 github.com/goccy/go-yaml v1.11.0 + github.com/gofrs/flock v0.8.1 github.com/google/go-containerregistry v0.15.2 + github.com/gosuri/uitable v0.0.4 github.com/mholt/archiver/v3 v3.5.1 github.com/moby/moby v24.0.2+incompatible github.com/opencontainers/image-spec v1.1.0-rc4 @@ -35,10 +37,12 @@ require ( github.com/sigstore/cosign v1.13.1 github.com/sigstore/sigstore v1.4.4 github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.12.0 golang.org/x/sync v0.3.0 + golang.org/x/term v0.11.0 helm.sh/helm/v3 v3.12.2 k8s.io/api v0.27.4 k8s.io/apimachinery v0.27.4 @@ -225,7 +229,6 @@ require ( github.com/googleapis/gax-go/v2 v2.8.0 // indirect github.com/gookit/color v1.5.3 // indirect github.com/gorilla/mux v1.8.0 // indirect - github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -344,7 +347,6 @@ require ( github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/spiffe/go-spiffe/v2 v2.1.1 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/sylabs/sif/v2 v2.8.1 // indirect @@ -389,7 +391,6 @@ require ( golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sys v0.11.0 // indirect - golang.org/x/term v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.1 // indirect diff --git a/go.sum b/go.sum index c3bbfd2e71..053b1d0b83 100644 --- a/go.sum +++ b/go.sum @@ -670,6 +670,8 @@ github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFT github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= diff --git a/hack/check-zarf-docs-and-schema.sh b/hack/check-zarf-docs-and-schema.sh index 59ddcee526..6539e88966 100755 --- a/hack/check-zarf-docs-and-schema.sh +++ b/hack/check-zarf-docs-and-schema.sh @@ -4,6 +4,6 @@ if [ -z "$(git status -s docs/ zarf.schema.json src/ui/lib/api-types.ts)" ]; the echo "Success!" exit 0 else - git status docs/ zarf.schema.json src/ui/lib/api-types.ts + git diff docs/ zarf.schema.json src/ui/lib/api-types.ts exit 1 fi diff --git a/src/cmd/common/vendor.go b/src/cmd/common/vendor.go index 53cb1cca5c..1bcf08b3b5 100644 --- a/src/cmd/common/vendor.go +++ b/src/cmd/common/vendor.go @@ -26,6 +26,8 @@ var vendorCmds = []string{ "crane", "registry", "r", + "helm", + "h", } // CheckVendorOnlyFromArgs checks if the command being run is a vendor-only command diff --git a/src/cmd/internal.go b/src/cmd/internal.go index f93848f001..b37e510a00 100644 --- a/src/cmd/internal.go +++ b/src/cmd/internal.go @@ -22,6 +22,7 @@ import ( "github.com/defenseunicorns/zarf/src/types" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" + "github.com/spf13/pflag" ) var internalCmd = &cobra.Command{ @@ -70,6 +71,29 @@ var generateCLIDocs = &cobra.Command{ addHiddenDummyFlag(toolCmd, "insecure") addHiddenDummyFlag(toolCmd, "no-color") } + + // Remove the default values from all of the helm commands during the CLI command doc generation + if toolCmd.Use == "helm" { + toolCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) { + if flag.Value.Type() == "string" { + flag.DefValue = "" + } + }) + for _, helmCmd := range toolCmd.Commands() { + helmCmd.Flags().VisitAll(func(flag *pflag.Flag) { + if flag.Value.Type() == "string" { + flag.DefValue = "" + } + }) + for _, helmSubCmd := range helmCmd.Commands() { + helmSubCmd.Flags().VisitAll(func(flag *pflag.Flag) { + if flag.Value.Type() == "string" { + flag.DefValue = "" + } + }) + } + } + } } } } diff --git a/src/cmd/tools/helm.go b/src/cmd/tools/helm.go new file mode 100644 index 0000000000..2a3b301595 --- /dev/null +++ b/src/cmd/tools/helm.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package tools contains the CLI commands for Zarf. +package tools + +import ( + "os" + + "github.com/defenseunicorns/zarf/src/cmd/tools/helm" + "github.com/defenseunicorns/zarf/src/config/lang" + "helm.sh/helm/v3/pkg/action" +) + +func init() { + actionConfig := new(action.Configuration) + + // Since helm needs args passed into it, check if we are processing things on a command with fewer args + if len(os.Args) < 3 { + return + } + + // The inclusion of Helm in this manner should be reconsidered once https://github.com/helm/helm/issues/12122 is resolved + helmCmd, _ := helm.NewRootCmd(actionConfig, os.Stdout, os.Args[3:]) + helmCmd.Short = lang.CmdToolsHelmShort + helmCmd.Long = lang.CmdToolsHelmLong + + toolsCmd.AddCommand(helmCmd) +} diff --git a/src/cmd/tools/helm/LICENSE b/src/cmd/tools/helm/LICENSE new file mode 100644 index 0000000000..e19fd8f063 --- /dev/null +++ b/src/cmd/tools/helm/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 The Kubernetes Authors All Rights Reserved + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/cmd/tools/helm/dependency.go b/src/cmd/tools/helm/dependency.go new file mode 100644 index 0000000000..b96d1e39b2 --- /dev/null +++ b/src/cmd/tools/helm/dependency.go @@ -0,0 +1,127 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const dependencyDesc = ` +Manage the dependencies of a chart. + +Helm charts store their dependencies in 'charts/'. For chart developers, it is +often easier to manage dependencies in 'Chart.yaml' which declares all +dependencies. + +The dependency commands operate on that file, making it easy to synchronize +between the desired dependencies and the actual dependencies stored in the +'charts/' directory. + +For example, this Chart.yaml declares two dependencies: + + # Chart.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "https://example.com/charts" + - name: memcached + version: "3.2.1" + repository: "https://another.example.com/charts" + + +The 'name' should be the name of a chart, where that name must match the name +in that chart's 'Chart.yaml' file. + +The 'version' field should contain a semantic version or version range. + +The 'repository' URL should point to a Chart Repository. Helm expects that by +appending '/index.yaml' to the URL, it should be able to retrieve the chart +repository's index. Note: 'repository' can be an alias. The alias must start +with 'alias:' or '@'. + +Starting from 2.2.0, repository can be defined as the path to the directory of +the dependency charts stored locally. The path should start with a prefix of +"file://". For example, + + # Chart.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "file://../dependency_chart/nginx" + +If the dependency chart is retrieved locally, it is not required to have the +repository added to helm by "helm add repo". Version matching is also supported +for this case. +` + +const dependencyListDesc = ` +List all of the dependencies declared in a chart. + +This can take chart archives and chart directories as input. It will not alter +the contents of a chart. + +This will produce an error if the chart cannot be loaded. +` + +func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "dependency update|build|list", + Aliases: []string{"dep", "dependencies"}, + Short: "manage a chart's dependencies", + Long: dependencyDesc, + Args: require.NoArgs, + } + + cmd.AddCommand(newDependencyListCmd(out)) + cmd.AddCommand(newDependencyUpdateCmd(cfg, out)) + cmd.AddCommand(newDependencyBuildCmd(cfg, out)) + + return cmd +} + +func newDependencyListCmd(out io.Writer) *cobra.Command { + client := action.NewDependency() + cmd := &cobra.Command{ + Use: "list CHART", + Aliases: []string{"ls"}, + Short: "list the dependencies for the given chart", + Long: dependencyListDesc, + Args: require.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + chartpath := "." + if len(args) > 0 { + chartpath = filepath.Clean(args[0]) + } + return client.List(chartpath, out) + }, + } + + f := cmd.Flags() + + f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table") + return cmd +} diff --git a/src/cmd/tools/helm/dependency_build.go b/src/cmd/tools/helm/dependency_build.go new file mode 100644 index 0000000000..0e84e244bb --- /dev/null +++ b/src/cmd/tools/helm/dependency_build.go @@ -0,0 +1,98 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/client-go/util/homedir" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" +) + +const dependencyBuildDesc = ` +Build out the charts/ directory from the Chart.lock file. + +Build is used to reconstruct a chart's dependencies to the state specified in +the lock file. This will not re-negotiate dependencies, as 'zarf tools helm dependency update' +does. + +If no lock file is found, 'zarf tools helm dependency build' will mirror the behavior +of 'zarf tools helm dependency update'. +` + +func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewDependency() + + cmd := &cobra.Command{ + Use: "build CHART", + Short: "rebuild the charts/ directory based on the Chart.lock file", + Long: dependencyBuildDesc, + Args: require.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + chartpath := "." + if len(args) > 0 { + chartpath = filepath.Clean(args[0]) + } + man := &downloader.Manager{ + Out: out, + ChartPath: chartpath, + Keyring: client.Keyring, + SkipUpdate: client.SkipRefresh, + Getters: getter.All(settings), + RegistryClient: cfg.RegistryClient, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + Debug: settings.Debug, + } + if client.Verify { + man.Verify = downloader.VerifyIfPossible + } + err := man.Build() + if e, ok := err.(downloader.ErrRepoNotFound); ok { + return fmt.Errorf("%s. Please add the missing repos via 'zarf tools helm repo add'", e.Error()) + } + return err + }, + } + + f := cmd.Flags() + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + + return cmd +} + +// defaultKeyring returns the expanded path to the default keyring. +func defaultKeyring() string { + if v, ok := os.LookupEnv("GNUPGHOME"); ok { + return filepath.Join(v, "pubring.gpg") + } + return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg") +} diff --git a/src/cmd/tools/helm/dependency_update.go b/src/cmd/tools/helm/dependency_update.go new file mode 100644 index 0000000000..86c9f48de3 --- /dev/null +++ b/src/cmd/tools/helm/dependency_update.go @@ -0,0 +1,89 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" +) + +const dependencyUpDesc = ` +Update the on-disk dependencies to mirror Chart.yaml. + +This command verifies that the required charts, as expressed in 'Chart.yaml', +are present in 'charts/' and are at an acceptable version. It will pull down +the latest charts that satisfy the dependencies, and clean up old dependencies. + +On successful update, this will generate a lock file that can be used to +rebuild the dependencies to an exact version. + +Dependencies are not required to be represented in 'Chart.yaml'. For that +reason, an update command will not remove charts unless they are (a) present +in the Chart.yaml file, but (b) at the wrong version. +` + +// newDependencyUpdateCmd creates a new dependency update command. +func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewDependency() + + cmd := &cobra.Command{ + Use: "update CHART", + Aliases: []string{"up"}, + Short: "update charts/ based on the contents of Chart.yaml", + Long: dependencyUpDesc, + Args: require.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + chartpath := "." + if len(args) > 0 { + chartpath = filepath.Clean(args[0]) + } + man := &downloader.Manager{ + Out: out, + ChartPath: chartpath, + Keyring: client.Keyring, + SkipUpdate: client.SkipRefresh, + Getters: getter.All(settings), + RegistryClient: cfg.RegistryClient, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + Debug: settings.Debug, + } + if client.Verify { + man.Verify = downloader.VerifyAlways + } + return man.Update() + }, + } + + f := cmd.Flags() + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + + return cmd +} diff --git a/src/cmd/tools/helm/flags.go b/src/cmd/tools/helm/flags.go new file mode 100644 index 0000000000..d567d920ec --- /dev/null +++ b/src/cmd/tools/helm/flags.go @@ -0,0 +1,109 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "flag" + "fmt" + "log" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "helm.sh/helm/v3/pkg/cli/output" + "k8s.io/klog/v2" +) + +const ( + outputFlag = "output" +) + +// bindOutputFlag will add the output flag to the given command and bind the +// value to the given format pointer +func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { + cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o", + fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", "))) + + err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var formatNames []string + for format, desc := range output.FormatsWithDesc() { + formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc)) + } + + // Sort the results to get a deterministic order for the tests + sort.Strings(formatNames) + return formatNames, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } +} + +type outputValue output.Format + +func newOutputValue(defaultValue output.Format, p *output.Format) *outputValue { + *p = defaultValue + return (*outputValue)(p) +} + +func (o *outputValue) String() string { + // It is much cleaner looking (and technically less allocations) to just + // convert to a string rather than type asserting to the underlying + // output.Format + return string(*o) +} + +func (o *outputValue) Type() string { + return "format" +} + +func (o *outputValue) Set(s string) error { + outfmt, err := output.ParseFormat(s) + if err != nil { + return err + } + *o = outputValue(outfmt) + return nil +} + +// addKlogFlags adds flags from k8s.io/klog +// marks the flags as hidden to avoid polluting the help text +func addKlogFlags(fs *pflag.FlagSet) { + local := flag.NewFlagSet("klog", flag.ExitOnError) + klog.InitFlags(local) + local.VisitAll(func(fl *flag.Flag) { + fl.Name = normalize(fl.Name) + if fs.Lookup(fl.Name) != nil { + return + } + newflag := pflag.PFlagFromGoFlag(fl) + newflag.Hidden = true + fs.AddFlag(newflag) + }) +} + +// normalize replaces underscores with hyphens +func normalize(s string) string { + return strings.ReplaceAll(s, "_", "-") +} diff --git a/src/cmd/tools/helm/load_plugins.go b/src/cmd/tools/helm/load_plugins.go new file mode 100644 index 0000000000..6890f52edc --- /dev/null +++ b/src/cmd/tools/helm/load_plugins.go @@ -0,0 +1,381 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/plugin" +) + +const ( + pluginStaticCompletionFile = "completion.yaml" + pluginDynamicCompletionExecutable = "plugin.complete" +) + +type pluginError struct { + error + code int +} + +// loadPlugins loads plugins into the command list. +// +// This follows a different pattern than the other commands because it has +// to inspect its environment and then add commands to the base command +// as it finds them. +func loadPlugins(baseCmd *cobra.Command, out io.Writer) { + + // If HELM_NO_PLUGINS is set to 1, do not load plugins. + if os.Getenv("HELM_NO_PLUGINS") == "1" { + return + } + + found, err := plugin.FindPlugins(settings.PluginsDirectory) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err) + return + } + + // Now we create commands for all of these. + for _, plug := range found { + plug := plug + md := plug.Metadata + if md.Usage == "" { + md.Usage = fmt.Sprintf("the %q plugin", md.Name) + } + + c := &cobra.Command{ + Use: md.Name, + Short: md.Usage, + Long: md.Description, + RunE: func(cmd *cobra.Command, args []string) error { + u, err := processParent(cmd, args) + if err != nil { + return err + } + + // Call setupEnv before PrepareCommand because + // PrepareCommand uses os.ExpandEnv and expects the + // setupEnv vars. + plugin.SetupPluginEnv(settings, md.Name, plug.Dir) + main, argv, prepCmdErr := plug.PrepareCommand(u) + if prepCmdErr != nil { + os.Stderr.WriteString(prepCmdErr.Error()) + return errors.Errorf("plugin %q exited with error", md.Name) + } + + return callPluginExecutable(md.Name, main, argv, out) + }, + // This passes all the flags to the subcommand. + DisableFlagParsing: true, + } + + // TODO: Make sure a command with this name does not already exist. + baseCmd.AddCommand(c) + + // For completion, we try to load more details about the plugins so as to allow for command and + // flag completion of the plugin itself. + // We only do this when necessary (for the "completion" and "__complete" commands) to avoid the + // risk of a rogue plugin affecting Helm's normal behavior. + subCmd, _, err := baseCmd.Find(os.Args[1:]) + if (err == nil && + ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || + /* for the tests */ subCmd == baseCmd.Root() { + loadCompletionForPlugin(c, plug) + } + } +} + +func processParent(cmd *cobra.Command, args []string) ([]string, error) { + k, u := manuallyProcessArgs(args) + if err := cmd.Parent().ParseFlags(k); err != nil { + return nil, err + } + return u, nil +} + +// This function is used to setup the environment for the plugin and then +// call the executable specified by the parameter 'main' +func callPluginExecutable(pluginName string, main string, argv []string, out io.Writer) error { + env := os.Environ() + for k, v := range settings.EnvVars() { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + } + + prog := exec.Command(main, argv...) + prog.Env = env + prog.Stdin = os.Stdin + prog.Stdout = out + prog.Stderr = os.Stderr + if err := prog.Run(); err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + os.Stderr.Write(eerr.Stderr) + status := eerr.Sys().(syscall.WaitStatus) + return pluginError{ + error: errors.Errorf("plugin %q exited with error", pluginName), + code: status.ExitStatus(), + } + } + return err + } + return nil +} + +// manuallyProcessArgs processes an arg array, removing special args. +// +// Returns two sets of args: known and unknown (in that order) +func manuallyProcessArgs(args []string) ([]string, []string) { + known := []string{} + unknown := []string{} + kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--kube-ca-file", "--registry-config", "--repository-cache", "--repository-config", "--insecure-skip-tls-verify", "--tls-server-name"} + knownArg := func(a string) bool { + for _, pre := range kvargs { + if strings.HasPrefix(a, pre+"=") { + return true + } + } + return false + } + + isKnown := func(v string) string { + for _, i := range kvargs { + if i == v { + return v + } + } + return "" + } + + for i := 0; i < len(args); i++ { + switch a := args[i]; a { + case "--debug": + known = append(known, a) + case isKnown(a): + known = append(known, a) + i++ + if i < len(args) { + known = append(known, args[i]) + } + default: + if knownArg(a) { + known = append(known, a) + continue + } + unknown = append(unknown, a) + } + } + return known, unknown +} + +// pluginCommand represents the optional completion.yaml file of a plugin +type pluginCommand struct { + Name string `json:"name"` + ValidArgs []string `json:"validArgs"` + Flags []string `json:"flags"` + Commands []pluginCommand `json:"commands"` +} + +// loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin +// and add the dynamic completion hook to call the optional plugin.complete +func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *plugin.Plugin) { + // Parse the yaml file providing the plugin's sub-commands and flags + cmds, err := loadFile(strings.Join( + []string{plugin.Dir, pluginStaticCompletionFile}, string(filepath.Separator))) + + if err != nil { + // The file could be missing or invalid. No static completion for this plugin. + if settings.Debug { + log.Output(2, fmt.Sprintf("[info] %s\n", err.Error())) + } + // Continue to setup dynamic completion. + cmds = &pluginCommand{} + } + + // Preserve the Usage string specified for the plugin + cmds.Name = pluginCmd.Use + + addPluginCommands(plugin, pluginCmd, cmds) +} + +// addPluginCommands is a recursive method that adds each different level +// of sub-commands and flags for the plugins that have provided such information +func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { + if cmds == nil { + return + } + + if len(cmds.Name) == 0 { + // Missing name for a command + if settings.Debug { + log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath())) + } + return + } + + baseCmd.Use = cmds.Name + baseCmd.ValidArgs = cmds.ValidArgs + // Setup the same dynamic completion for each plugin sub-command. + // This is because if dynamic completion is triggered, there is a single executable + // to call (plugin.complete), so every sub-commands calls it in the same fashion. + if cmds.Commands == nil { + // Only setup dynamic completion if there are no sub-commands. This avoids + // calling plugin.complete at every completion, which greatly simplifies + // development of plugin.complete for plugin developers. + baseCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return pluginDynamicComp(plugin, cmd, args, toComplete) + } + } + + // Create fake flags. + if len(cmds.Flags) > 0 { + // The flags can be created with any type, since we only need them for completion. + // pflag does not allow to create short flags without a corresponding long form + // so we look for all short flags and match them to any long flag. This will allow + // plugins to provide short flags without a long form. + // If there are more short-flags than long ones, we'll create an extra long flag with + // the same single letter as the short form. + shorts := []string{} + longs := []string{} + for _, flag := range cmds.Flags { + if len(flag) == 1 { + shorts = append(shorts, flag) + } else { + longs = append(longs, flag) + } + } + + f := baseCmd.Flags() + if len(longs) >= len(shorts) { + for i := range longs { + if i < len(shorts) { + f.BoolP(longs[i], shorts[i], false, "") + } else { + f.Bool(longs[i], false, "") + } + } + } else { + for i := range shorts { + if i < len(longs) { + f.BoolP(longs[i], shorts[i], false, "") + } else { + // Create a long flag with the same name as the short flag. + // Not a perfect solution, but its better than ignoring the extra short flags. + f.BoolP(shorts[i], shorts[i], false, "") + } + } + } + } + + // Recursively add any sub-commands + for _, cmd := range cmds.Commands { + // Create a fake command so that completion can be done for the sub-commands of the plugin + subCmd := &cobra.Command{ + // This prevents Cobra from removing the flags. We want to keep the flags to pass them + // to the dynamic completion script of the plugin. + DisableFlagParsing: true, + // A Run is required for it to be a valid command without subcommands + Run: func(cmd *cobra.Command, args []string) {}, + } + baseCmd.AddCommand(subCmd) + addPluginCommands(plugin, subCmd, &cmd) + } +} + +// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object +func loadFile(path string) (*pluginCommand, error) { + cmds := new(pluginCommand) + b, err := os.ReadFile(path) + if err != nil { + return cmds, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path) + } + + err = yaml.Unmarshal(b, cmds) + return cmds, err +} + +// pluginDynamicComp call the plugin.complete script of the plugin (if available) +// to obtain the dynamic completion choices. It must pass all the flags and sub-commands +// specified in the command-line to the plugin.complete executable (except helm's global flags) +func pluginDynamicComp(plug *plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + md := plug.Metadata + + u, err := processParent(cmd, args) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + // We will call the dynamic completion script of the plugin + main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator)) + + // We must include all sub-commands passed on the command-line. + // To do that, we pass-in the entire CommandPath, except the first two elements + // which are 'helm' and 'pluginName'. + argv := strings.Split(cmd.CommandPath(), " ")[2:] + if !md.IgnoreFlags { + argv = append(argv, u...) + argv = append(argv, toComplete) + } + plugin.SetupPluginEnv(settings, md.Name, plug.Dir) + + cobra.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv), settings.Debug) + buf := new(bytes.Buffer) + if err := callPluginExecutable(md.Name, main, argv, buf); err != nil { + // The dynamic completion file is optional for a plugin, so this error is ok. + cobra.CompDebugln(fmt.Sprintf("Unable to call %s: %v", main, err.Error()), settings.Debug) + return nil, cobra.ShellCompDirectiveDefault + } + + var completions []string + for _, comp := range strings.Split(buf.String(), "\n") { + // Remove any empty lines + if len(comp) > 0 { + completions = append(completions, comp) + } + } + + // Check if the last line of output is of the form :, which + // indicates the BashCompletionDirective. + directive := cobra.ShellCompDirectiveDefault + if len(completions) > 0 { + lastLine := completions[len(completions)-1] + if len(lastLine) > 1 && lastLine[0] == ':' { + if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { + directive = cobra.ShellCompDirective(strInt) + completions = completions[:len(completions)-1] + } + } + } + + return completions, directive +} diff --git a/src/cmd/tools/helm/repo.go b/src/cmd/tools/helm/repo.go new file mode 100644 index 0000000000..b0ac9fb350 --- /dev/null +++ b/src/cmd/tools/helm/repo.go @@ -0,0 +1,62 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "io" + "os" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/cli" +) + +var settings = cli.New() + +var repoHelm = ` +This command consists of multiple subcommands to interact with chart repositories. + +It can be used to add, remove, list, and index chart repositories. +` + +func newRepoCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "repo add|remove|list|index|update [ARGS]", + Short: "add, list, remove, update, and index chart repositories", + Long: repoHelm, + Args: require.NoArgs, + } + + cmd.AddCommand(newRepoAddCmd(out)) + cmd.AddCommand(newRepoListCmd(out)) + cmd.AddCommand(newRepoRemoveCmd(out)) + cmd.AddCommand(newRepoIndexCmd(out)) + cmd.AddCommand(newRepoUpdateCmd(out)) + + return cmd +} + +func isNotExist(err error) bool { + return os.IsNotExist(errors.Cause(err)) +} diff --git a/src/cmd/tools/helm/repo_add.go b/src/cmd/tools/helm/repo_add.go new file mode 100644 index 0000000000..ce2e236267 --- /dev/null +++ b/src/cmd/tools/helm/repo_add.go @@ -0,0 +1,224 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/gofrs/flock" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/term" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" +) + +// Repositories that have been permanently deleted and no longer work +var deprecatedRepos = map[string]string{ + "//kubernetes-charts.storage.googleapis.com": "https://charts.helm.sh/stable", + "//kubernetes-charts-incubator.storage.googleapis.com": "https://charts.helm.sh/incubator", +} + +type repoAddOptions struct { + name string + url string + username string + password string + passwordFromStdinOpt bool + passCredentialsAll bool + forceUpdate bool + allowDeprecatedRepos bool + + certFile string + keyFile string + caFile string + insecureSkipTLSverify bool + + repoFile string + repoCache string + + // Deprecated, but cannot be removed until Helm 4 + deprecatedNoUpdate bool +} + +func newRepoAddCmd(out io.Writer) *cobra.Command { + o := &repoAddOptions{} + + cmd := &cobra.Command{ + Use: "add [NAME] [URL]", + Short: "add a chart repository", + Args: require.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + o.name = args[0] + o.url = args[1] + o.repoFile = settings.RepositoryConfig + o.repoCache = settings.RepositoryCache + + return o.run(out) + }, + } + + f := cmd.Flags() + f.StringVar(&o.username, "username", "", "chart repository username") + f.StringVar(&o.password, "password", "", "chart repository password") + f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin") + f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists") + f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.") + f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository") + f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior") + f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains") + + return cmd +} + +func (o *repoAddOptions) run(out io.Writer) error { + // Block deprecated repos + if !o.allowDeprecatedRepos { + for oldURL, newURL := range deprecatedRepos { + if strings.Contains(o.url, oldURL) { + return fmt.Errorf("repo %q is no longer available; try %q instead", o.url, newURL) + } + } + } + + // Ensure the file directory exists as it is required for file locking + err := os.MkdirAll(filepath.Dir(o.repoFile), os.ModePerm) + if err != nil && !os.IsExist(err) { + return err + } + + // Acquire a file lock for process synchronization + repoFileExt := filepath.Ext(o.repoFile) + var lockPath string + if len(repoFileExt) > 0 && len(repoFileExt) < len(o.repoFile) { + lockPath = strings.TrimSuffix(o.repoFile, repoFileExt) + ".lock" + } else { + lockPath = o.repoFile + ".lock" + } + fileLock := flock.New(lockPath) + lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + locked, err := fileLock.TryLockContext(lockCtx, time.Second) + if err == nil && locked { + defer fileLock.Unlock() + } + if err != nil { + return err + } + + b, err := os.ReadFile(o.repoFile) + if err != nil && !os.IsNotExist(err) { + return err + } + + var f repo.File + if err := yaml.Unmarshal(b, &f); err != nil { + return err + } + + if o.username != "" && o.password == "" { + if o.passwordFromStdinOpt { + passwordFromStdin, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + password := strings.TrimSuffix(string(passwordFromStdin), "\n") + password = strings.TrimSuffix(password, "\r") + o.password = password + } else { + fd := int(os.Stdin.Fd()) + fmt.Fprint(out, "Password: ") + password, err := term.ReadPassword(fd) + fmt.Fprintln(out) + if err != nil { + return err + } + o.password = string(password) + } + } + + c := repo.Entry{ + Name: o.name, + URL: o.url, + Username: o.username, + Password: o.password, + PassCredentialsAll: o.passCredentialsAll, + CertFile: o.certFile, + KeyFile: o.keyFile, + CAFile: o.caFile, + InsecureSkipTLSverify: o.insecureSkipTLSverify, + } + + // Check if the repo name is legal + if strings.Contains(o.name, "/") { + return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", o.name) + } + + // If the repo exists do one of two things: + // 1. If the configuration for the name is the same continue without error + // 2. When the config is different require --force-update + if !o.forceUpdate && f.Has(o.name) { + existing := f.Get(o.name) + if c != *existing { + + // The input coming in for the name is different from what is already + // configured. Return an error. + return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name) + } + + // The add is idempotent so do nothing + fmt.Fprintf(out, "%q already exists with the same configuration, skipping\n", o.name) + return nil + } + + r, err := repo.NewChartRepository(&c, getter.All(settings)) + if err != nil { + return err + } + + if o.repoCache != "" { + r.CachePath = o.repoCache + } + if _, err := r.DownloadIndexFile(); err != nil { + return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", o.url) + } + + f.Update(&c) + + if err := f.WriteFile(o.repoFile, 0600); err != nil { + return err + } + fmt.Fprintf(out, "%q has been added to your repositories\n", o.name) + return nil +} diff --git a/src/cmd/tools/helm/repo_index.go b/src/cmd/tools/helm/repo_index.go new file mode 100644 index 0000000000..79f941e79a --- /dev/null +++ b/src/cmd/tools/helm/repo_index.go @@ -0,0 +1,114 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/repo" +) + +const repoIndexDesc = ` +Read the current directory and generate an index file based on the charts found. + +This tool is used for creating an 'index.yaml' file for a chart repository. To +set an absolute URL to the charts, use '--url' flag. + +To merge the generated index with an existing index file, use the '--merge' +flag. In this case, the charts found in the current directory will be merged +into the existing index, with local charts taking priority over existing charts. +` + +type repoIndexOptions struct { + dir string + url string + merge string +} + +func newRepoIndexCmd(out io.Writer) *cobra.Command { + o := &repoIndexOptions{} + + cmd := &cobra.Command{ + Use: "index [DIR]", + Short: "generate an index file given a directory containing packaged charts", + Long: repoIndexDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + // Allow file completion when completing the argument for the directory + return nil, cobra.ShellCompDirectiveDefault + } + // No more completions, so disable file completion + return nil, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + o.dir = args[0] + return o.run(out) + }, + } + + f := cmd.Flags() + f.StringVar(&o.url, "url", "", "url of chart repository") + f.StringVar(&o.merge, "merge", "", "merge the generated index into the given index") + + return cmd +} + +func (i *repoIndexOptions) run(_ io.Writer) error { + path, err := filepath.Abs(i.dir) + if err != nil { + return err + } + + return index(path, i.url, i.merge) +} + +func index(dir, url, mergeTo string) error { + out := filepath.Join(dir, "index.yaml") + + i, err := repo.IndexDirectory(dir, url) + if err != nil { + return err + } + if mergeTo != "" { + // if index.yaml is missing then create an empty one to merge into + var i2 *repo.IndexFile + if _, err := os.Stat(mergeTo); os.IsNotExist(err) { + i2 = repo.NewIndexFile() + i2.WriteFile(mergeTo, 0644) + } else { + i2, err = repo.LoadIndexFile(mergeTo) + if err != nil { + return errors.Wrap(err, "merge failed") + } + } + i.Merge(i2) + } + i.SortEntries() + return i.WriteFile(out, 0644) +} diff --git a/src/cmd/tools/helm/repo_list.go b/src/cmd/tools/helm/repo_list.go new file mode 100644 index 0000000000..02b4e34d86 --- /dev/null +++ b/src/cmd/tools/helm/repo_list.go @@ -0,0 +1,141 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "fmt" + "io" + + "github.com/gosuri/uitable" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/repo" +) + +func newRepoListCmd(out io.Writer) *cobra.Command { + var outfmt output.Format + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "list chart repositories", + Args: require.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + f, _ := repo.LoadFile(settings.RepositoryConfig) + if len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML) { + return errors.New("no repositories to show") + } + + return outfmt.Write(out, &repoListWriter{f.Repositories}) + }, + } + + bindOutputFlag(cmd, &outfmt) + + return cmd +} + +type repositoryElement struct { + Name string `json:"name"` + URL string `json:"url"` +} + +type repoListWriter struct { + repos []*repo.Entry +} + +func (r *repoListWriter) WriteTable(out io.Writer) error { + table := uitable.New() + table.AddRow("NAME", "URL") + for _, re := range r.repos { + table.AddRow(re.Name, re.URL) + } + return output.EncodeTable(out, table) +} + +func (r *repoListWriter) WriteJSON(out io.Writer) error { + return r.encodeByFormat(out, output.JSON) +} + +func (r *repoListWriter) WriteYAML(out io.Writer) error { + return r.encodeByFormat(out, output.YAML) +} + +func (r *repoListWriter) encodeByFormat(out io.Writer, format output.Format) error { + // Initialize the array so no results returns an empty array instead of null + repolist := make([]repositoryElement, 0, len(r.repos)) + + for _, re := range r.repos { + repolist = append(repolist, repositoryElement{Name: re.Name, URL: re.URL}) + } + + switch format { + case output.JSON: + return output.EncodeJSON(out, repolist) + case output.YAML: + return output.EncodeYAML(out, repolist) + } + + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil +} + +// Returns all repos from repos, except those with names matching ignoredRepoNames +// Inspired by https://stackoverflow.com/a/28701031/893211 +func filterRepos(repos []*repo.Entry, ignoredRepoNames []string) []*repo.Entry { + // if ignoredRepoNames is nil, just return repo + if ignoredRepoNames == nil { + return repos + } + + filteredRepos := make([]*repo.Entry, 0) + + ignored := make(map[string]bool, len(ignoredRepoNames)) + for _, repoName := range ignoredRepoNames { + ignored[repoName] = true + } + + for _, repo := range repos { + if _, removed := ignored[repo.Name]; !removed { + filteredRepos = append(filteredRepos, repo) + } + } + + return filteredRepos +} + +// Provide dynamic auto-completion for repo names +func compListRepos(_ string, ignoredRepoNames []string) []string { + var rNames []string + + f, err := repo.LoadFile(settings.RepositoryConfig) + if err == nil && len(f.Repositories) > 0 { + filteredRepos := filterRepos(f.Repositories, ignoredRepoNames) + for _, repo := range filteredRepos { + rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL)) + } + } + return rNames +} diff --git a/src/cmd/tools/helm/repo_remove.go b/src/cmd/tools/helm/repo_remove.go new file mode 100644 index 0000000000..61340f8a49 --- /dev/null +++ b/src/cmd/tools/helm/repo_remove.go @@ -0,0 +1,101 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/repo" +) + +type repoRemoveOptions struct { + names []string + repoFile string + repoCache string +} + +func newRepoRemoveCmd(out io.Writer) *cobra.Command { + o := &repoRemoveOptions{} + + cmd := &cobra.Command{ + Use: "remove [REPO1 [REPO2 ...]]", + Aliases: []string{"rm"}, + Short: "remove one or more chart repositories", + Args: require.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + o.repoFile = settings.RepositoryConfig + o.repoCache = settings.RepositoryCache + o.names = args + return o.run(out) + }, + } + return cmd +} + +func (o *repoRemoveOptions) run(out io.Writer) error { + r, err := repo.LoadFile(o.repoFile) + if isNotExist(err) || len(r.Repositories) == 0 { + return errors.New("no repositories configured") + } + + for _, name := range o.names { + if !r.Remove(name) { + return errors.Errorf("no repo named %q found", name) + } + if err := r.WriteFile(o.repoFile, 0600); err != nil { + return err + } + + if err := removeRepoCache(o.repoCache, name); err != nil { + return err + } + fmt.Fprintf(out, "%q has been removed from your repositories\n", name) + } + + return nil +} + +func removeRepoCache(root, name string) error { + idx := filepath.Join(root, helmpath.CacheChartsFile(name)) + if _, err := os.Stat(idx); err == nil { + os.Remove(idx) + } + + idx = filepath.Join(root, helmpath.CacheIndexFile(name)) + if _, err := os.Stat(idx); os.IsNotExist(err) { + return nil + } else if err != nil { + return errors.Wrapf(err, "can't remove index file %s", idx) + } + return os.Remove(idx) +} diff --git a/src/cmd/tools/helm/repo_update.go b/src/cmd/tools/helm/repo_update.go new file mode 100644 index 0000000000..c74d1eae75 --- /dev/null +++ b/src/cmd/tools/helm/repo_update.go @@ -0,0 +1,172 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "fmt" + "io" + "sync" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" +) + +const updateDesc = ` +Update gets the latest information about charts from the respective chart repositories. +Information is cached locally, where it is used by commands like 'helm search'. + +You can optionally specify a list of repositories you want to update. + $ zarf tools helm repo update ... +To update all the repositories, use 'zarf tools helm repo update'. +` + +var errNoRepositories = errors.New("no repositories found. You must add one before updating") + +type repoUpdateOptions struct { + update func([]*repo.ChartRepository, io.Writer, bool) error + repoFile string + repoCache string + names []string + failOnRepoUpdateFail bool +} + +func newRepoUpdateCmd(out io.Writer) *cobra.Command { + o := &repoUpdateOptions{update: updateCharts} + + cmd := &cobra.Command{ + Use: "update [REPO1 [REPO2 ...]]", + Aliases: []string{"up"}, + Short: "update information of available charts locally from chart repositories", + Long: updateDesc, + Args: require.MinimumNArgs(0), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + o.repoFile = settings.RepositoryConfig + o.repoCache = settings.RepositoryCache + o.names = args + return o.run(out) + }, + } + + f := cmd.Flags() + + // Adding this flag for Helm 3 as stop gap functionality for https://github.com/helm/helm/issues/10016. + // This should be deprecated in Helm 4 by update to the behaviour of `helm repo update` command. + f.BoolVar(&o.failOnRepoUpdateFail, "fail-on-repo-update-fail", false, "update fails if any of the repository updates fail") + + return cmd +} + +func (o *repoUpdateOptions) run(out io.Writer) error { + f, err := repo.LoadFile(o.repoFile) + switch { + case isNotExist(err): + return errNoRepositories + case err != nil: + return errors.Wrapf(err, "failed loading file: %s", o.repoFile) + case len(f.Repositories) == 0: + return errNoRepositories + } + + var repos []*repo.ChartRepository + updateAllRepos := len(o.names) == 0 + + if !updateAllRepos { + // Fail early if the user specified an invalid repo to update + if err := checkRequestedRepos(o.names, f.Repositories); err != nil { + return err + } + } + + for _, cfg := range f.Repositories { + if updateAllRepos || isRepoRequested(cfg.Name, o.names) { + r, err := repo.NewChartRepository(cfg, getter.All(settings)) + if err != nil { + return err + } + if o.repoCache != "" { + r.CachePath = o.repoCache + } + repos = append(repos, r) + } + } + + return o.update(repos, out, o.failOnRepoUpdateFail) +} + +func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { + fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") + var wg sync.WaitGroup + var repoFailList []string + for _, re := range repos { + wg.Add(1) + go func(re *repo.ChartRepository) { + defer wg.Done() + if _, err := re.DownloadIndexFile(); err != nil { + fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) + repoFailList = append(repoFailList, re.Config.URL) + } else { + fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) + } + }(re) + } + wg.Wait() + + if len(repoFailList) > 0 && failOnRepoUpdateFail { + return fmt.Errorf("failed to update the following repositories: %s", + repoFailList) + } + + fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") + return nil +} + +func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error { + for _, requestedRepo := range requestedRepos { + found := false + for _, repo := range validRepos { + if requestedRepo == repo.Name { + found = true + break + } + } + if !found { + return errors.Errorf("no repositories found matching '%s'. Nothing will be updated", requestedRepo) + } + } + return nil +} + +func isRepoRequested(repoName string, requestedRepos []string) bool { + for _, requestedRepo := range requestedRepos { + if repoName == requestedRepo { + return true + } + } + return false +} diff --git a/src/cmd/tools/helm/root.go b/src/cmd/tools/helm/root.go new file mode 100644 index 0000000000..63b9bf8f17 --- /dev/null +++ b/src/cmd/tools/helm/root.go @@ -0,0 +1,280 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +NOTICE: This file's 'package' and some functionality has been modified / removed to fit within Zarf's package structure. +*/ + +// Package helm is a copy of the main package from helm to include a subset of the helm CLI in Zarf +package helm + +import ( + "context" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/repo" +) + +var globalUsage = `The Kubernetes package manager + +Common actions for Helm: + +- helm search: search for charts +- helm pull: download a chart to your local directory to view +- helm install: upload the chart to Kubernetes +- helm list: list releases of charts + +Environment variables: + +| Name | Description | +|------------------------------------|---------------------------------------------------------------------------------------------------| +| $HELM_CACHE_HOME | set an alternative location for storing cached files. | +| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | +| $HELM_DATA_HOME | set an alternative location for storing Helm data. | +| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | +| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. | +| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | +| $HELM_MAX_HISTORY | set the maximum number of helm release history. | +| $HELM_NAMESPACE | set the namespace used for the helm operations. | +| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | +| $HELM_PLUGINS | set the path to the plugins directory | +| $HELM_REGISTRY_CONFIG | set the path to the registry config file. | +| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory | +| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. | +| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | +| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication | +| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. | +| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. | +| $HELM_KUBEASUSER | set the Username to impersonate for the operation. | +| $HELM_KUBECONTEXT | set the name of the kubeconfig context. | +| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. | +| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) | +| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate | +| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable)| + +Helm stores cache, configuration, and data based on the following configuration order: + +- If a HELM_*_HOME environment variable is set, it will be used +- Otherwise, on systems supporting the XDG base directory specification, the XDG variables will be used +- When no other location is set a default location will be used based on the operating system + +By default, the default directories depend on the Operating System. The defaults are listed below: + +| Operating System | Cache Path | Configuration Path | Data Path | +|------------------|---------------------------|--------------------------------|-------------------------| +| Linux | $HOME/.cache/helm | $HOME/.config/helm | $HOME/.local/share/helm | +| macOS | $HOME/Library/Caches/helm | $HOME/Library/Preferences/helm | $HOME/Library/helm | +| Windows | %TEMP%\helm | %APPDATA%\helm | %APPDATA%\helm | +` + +// NewRootCmd is a modified version of newRootCmd from the Helm CLI that returns a subset of the Helm CLI +func NewRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "helm", + Short: "The Helm package manager for Kubernetes.", + Long: globalUsage, + SilenceUsage: true, + } + flags := cmd.PersistentFlags() + + settings.AddFlags(flags) + addKlogFlags(flags) + + // Setup shell completion for the namespace flag + err := cmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if client, err := actionConfig.KubernetesClientSet(); err == nil { + // Choose a long enough timeout that the user notices something is not working + // but short enough that the user is not made to wait very long + to := int64(3) + cobra.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to), settings.Debug) + + nsNames := []string{} + if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil { + for _, ns := range namespaces.Items { + nsNames = append(nsNames, ns.Name) + } + return nsNames, cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault + }) + + if err != nil { + log.Fatal(err) + } + + // Setup shell completion for the kube-context flag + err = cmd.RegisterFlagCompletionFunc("kube-context", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cobra.CompDebugln("About to get the different kube-contexts", settings.Debug) + + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if len(settings.KubeConfig) > 0 { + loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig} + } + if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loadingRules, + &clientcmd.ConfigOverrides{}).RawConfig(); err == nil { + comps := []string{} + for name, context := range config.Contexts { + comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster)) + } + return comps, cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } + + // We can safely ignore any errors that flags.Parse encounters since + // those errors will be caught later during the call to cmd.Execution. + // This call is required to gather configuration information prior to + // execution. + flags.ParseErrorsWhitelist.UnknownFlags = true + flags.Parse(args) + + registryClient, err := newDefaultRegistryClient() + if err != nil { + return nil, err + } + actionConfig.RegistryClient = registryClient + + // Add subcommands + cmd.AddCommand( + // chart commands + // newCreateCmd(out), + newDependencyCmd(actionConfig, out), + // newPullCmd(actionConfig, out), + // newShowCmd(actionConfig, out), + // newLintCmd(out), + // newPackageCmd(actionConfig, out), + newRepoCmd(out), + // newSearchCmd(out), + // newVerifyCmd(out), + + // release commands + // newGetCmd(actionConfig, out), + // newHistoryCmd(actionConfig, out), + // newInstallCmd(actionConfig, out), + // newListCmd(actionConfig, out), + // newReleaseTestCmd(actionConfig, out), + // newRollbackCmd(actionConfig, out), + // newStatusCmd(actionConfig, out), + // newTemplateCmd(actionConfig, out), + // newUninstallCmd(actionConfig, out), + // newUpgradeCmd(actionConfig, out), + + // newCompletionCmd(out), + // newEnvCmd(out), + // newPluginCmd(out), + // newVersionCmd(out), + + // Hidden documentation generator command: 'helm docs' + // newDocsCmd(out), + ) + + // cmd.AddCommand( + // newRegistryCmd(actionConfig, out), + // newPushCmd(actionConfig, out), + // ) + + // Find and add plugins + loadPlugins(cmd, out) + + // Check permissions on critical files + // checkPerms() + + // Check for expired repositories + checkForExpiredRepos(settings.RepositoryConfig) + + return cmd, nil +} + +func checkForExpiredRepos(repofile string) { + + expiredRepos := []struct { + name string + old string + new string + }{ + { + name: "stable", + old: "kubernetes-charts.storage.googleapis.com", + new: "https://charts.helm.sh/stable", + }, + { + name: "incubator", + old: "kubernetes-charts-incubator.storage.googleapis.com", + new: "https://charts.helm.sh/incubator", + }, + } + + // parse repo file. + // Ignore the error because it is okay for a repo file to be unparseable at this + // stage. Later checks will trap the error and respond accordingly. + repoFile, err := repo.LoadFile(repofile) + if err != nil { + return + } + + for _, exp := range expiredRepos { + r := repoFile.Get(exp.name) + if r == nil { + return + } + + if url := r.URL; strings.Contains(url, exp.old) { + fmt.Fprintf( + os.Stderr, + "WARNING: %q is deprecated for %q and will be deleted Nov. 13, 2020.\nWARNING: You should switch to %q via:\nWARNING: zarf tools helm repo add %q %q --force-update\n", + exp.old, + exp.name, + exp.new, + exp.name, + exp.new, + ) + } + } + +} + +func newDefaultRegistryClient() (*registry.Client, error) { + opts := []registry.ClientOption{ + registry.ClientOptDebug(settings.Debug), + registry.ClientOptEnableCache(true), + registry.ClientOptWriter(os.Stderr), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + } + + // Create a new registry client + registryClient, err := registry.NewClient(opts...) + if err != nil { + return nil, err + } + return registryClient, nil +} diff --git a/src/config/lang/english.go b/src/config/lang/english.go index fed1a43312..e3561958f3 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -404,6 +404,9 @@ $ zarf tools registry digest reg.example.com/stefanprodan/podinfo:6.4.0 CmdToolsMonitorShort = "Launches a terminal UI to monitor the connected cluster using K9s." + CmdToolsHelmShort = "Subset of the Helm CLI included with Zarf to help manage helm charts." + CmdToolsHelmLong = "Subset of the Helm CLI that includes the repo and dependency commands for managing helm charts destined for the air gap." + CmdToolsClearCacheShort = "Clears the configured git and image cache directory" CmdToolsClearCacheDir = "Cache directory set to: %s" CmdToolsClearCacheErr = "Unable to clear the cache directory %s" diff --git a/src/internal/packager/helm/repo.go b/src/internal/packager/helm/repo.go index c59ff679eb..284ccf140e 100644 --- a/src/internal/packager/helm/repo.go +++ b/src/internal/packager/helm/repo.go @@ -18,6 +18,7 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/registry" + "k8s.io/client-go/util/homedir" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/downloader" @@ -192,13 +193,20 @@ func (h *Helm) DownloadChartFromGitToTemp(spinner *message.Spinner) (string, err // buildChartDependencies builds the helm chart dependencies func (h *Helm) buildChartDependencies(spinner *message.Spinner) error { + // Download and build the specified dependencies regClient, err := registry.NewClient(registry.ClientOptEnableCache(true)) if err != nil { spinner.Fatalf(err, "Unable to create a new registry client") } + h.Settings = cli.New() + defaultKeyring := filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg") + if v, ok := os.LookupEnv("GNUPGHOME"); ok { + defaultKeyring = filepath.Join(v, "pubring.gpg") + } + man := &downloader.Manager{ - Out: os.Stdout, + Out: &message.DebugWriter{}, ChartPath: h.Chart.LocalPath, Getters: getter.All(h.Settings), RegistryClient: regClient, @@ -206,16 +214,18 @@ func (h *Helm) buildChartDependencies(spinner *message.Spinner) error { RepositoryConfig: h.Settings.RepositoryConfig, RepositoryCache: h.Settings.RepositoryCache, Debug: false, + Verify: downloader.VerifyIfPossible, + Keyring: defaultKeyring, } - // Verify the chart - man.Verify = downloader.VerifyIfPossible // Build the deps from the helm chart err = man.Build() if e, ok := err.(downloader.ErrRepoNotFound); ok { - return fmt.Errorf("%s. Please add the missing repo via 'helm repo add '", e.Error()) + // If we encounter a repo not found error point the user to `zarf tools helm repo add` + message.Warnf("%s. Please add the missing repo via 'zarf tools helm repo add '", e.Error()) } else if err != nil { - return err + // Warn the user of any issues but don't fail - any actual issues will cause a fail during packaging (e.g. the charts we are building may exist already, we just can't get updates) + message.Warnf("%s", err.Error()) } return nil diff --git a/src/internal/packager/helm/utils.go b/src/internal/packager/helm/utils.go index 472a5c4978..fb79f84229 100644 --- a/src/internal/packager/helm/utils.go +++ b/src/internal/packager/helm/utils.go @@ -13,6 +13,7 @@ import ( "github.com/defenseunicorns/zarf/src/types" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/getter" @@ -42,7 +43,7 @@ func (h *Helm) loadChartFromTarball() (*chart.Chart, error) { } // parseChartValues reads the context of the chart values into an interface if it exists. -func (h *Helm) parseChartValues() (map[string]any, error) { +func (h *Helm) parseChartValues() (chartutil.Values, error) { valueOpts := &values.Options{} for idx, file := range h.Chart.ValuesFiles { diff --git a/src/test/e2e/25_helm_test.go b/src/test/e2e/25_helm_test.go index d9dd9eac80..f0ba5d5b22 100644 --- a/src/test/e2e/25_helm_test.go +++ b/src/test/e2e/25_helm_test.go @@ -34,8 +34,16 @@ func TestHelm(t *testing.T) { func testHelmChartsExample(t *testing.T) { t.Parallel() t.Log("E2E: Helm chart example") + + // Create a package that needs dependencies + evilChartDepsPath := filepath.Join("src", "test", "packages", "25-evil-chart-deps") + stdOut, stdErr, err := e2e.Zarf("package", "create", evilChartDepsPath, "--confirm") + require.Error(t, err, stdOut, stdErr) + require.Contains(t, stdErr, "could not download https://charts.jetstack.io/charts/cert-manager-v1.11.1.tgz") + require.FileExists(t, filepath.Join(evilChartDepsPath, "good-chart", "charts", "gitlab-runner-0.55.0.tgz")) + // Create the package with a registry override - stdOut, stdErr, err := e2e.Zarf("package", "create", "examples/helm-charts", "-o", "build", "--registry-override", "ghcr.io=docker.io", "--confirm") + stdOut, stdErr, err = e2e.Zarf("package", "create", "examples/helm-charts", "-o", "build", "--registry-override", "ghcr.io=docker.io", "--confirm") require.NoError(t, err, stdOut, stdErr) // Deploy the package. diff --git a/src/test/packages/25-evil-chart-deps/.gitignore b/src/test/packages/25-evil-chart-deps/.gitignore new file mode 100644 index 0000000000..68d93c64fc --- /dev/null +++ b/src/test/packages/25-evil-chart-deps/.gitignore @@ -0,0 +1 @@ +requirements.lock diff --git a/src/test/packages/25-evil-chart-deps/bad-chart/Chart.yaml b/src/test/packages/25-evil-chart-deps/bad-chart/Chart.yaml new file mode 100644 index 0000000000..5291f4404d --- /dev/null +++ b/src/test/packages/25-evil-chart-deps/bad-chart/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +description: The Bad Chart +name: bad-chart +version: 1.0.0 + +maintainers: + - name: The Zarf Authors + url: https://zarf.dev + +dependencies: +# This chart will fail to load because the keyring won't be setup correctly +- name: cert-manager + version: 1.11.1 + repository: https://charts.jetstack.io/ + condition: certmanager.install + alias: certmanager diff --git a/src/test/packages/25-evil-chart-deps/good-chart/Chart.yaml b/src/test/packages/25-evil-chart-deps/good-chart/Chart.yaml new file mode 100644 index 0000000000..cfc2f48968 --- /dev/null +++ b/src/test/packages/25-evil-chart-deps/good-chart/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +description: The Good Chart +name: good-chart +version: 1.0.0 + +maintainers: + - name: The Zarf Authors + url: https://zarf.dev + +dependencies: +# This chart should be saved into the charts directory +- name: gitlab-runner + version: 0.55.0 + repository: https://charts.gitlab.io/ + condition: gitlab-runner.install diff --git a/src/test/packages/25-evil-chart-deps/zarf.yaml b/src/test/packages/25-evil-chart-deps/zarf.yaml new file mode 100644 index 0000000000..1199c46b66 --- /dev/null +++ b/src/test/packages/25-evil-chart-deps/zarf.yaml @@ -0,0 +1,21 @@ +kind: ZarfPackageConfig +metadata: + name: chart-deps + description: Simple example to load a chart with dependencies + +components: + - name: good + required: true + charts: + - name: dogs + localPath: good-chart + version: 1.0.0 + namespace: good + + - name: bad + required: true + charts: + - name: cats + localPath: bad-chart + version: 1.0.0 + namespace: bad