From d26a41143ffc600fde284e16d12da3c841c72745 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 17 Jun 2024 14:04:24 -0700 Subject: [PATCH] Switch Radius Helm chart pull from ACR to GHCR (#7455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description * Switch Radius Helm chart pull from ACR to GHCR * Add error handling for 403 from GHCR Example outputs: Expired credentials: ```sh ❯ go run ./cmd/rad/main.go install kubernetes --reinstall # command-line-arguments ld: warning: -bind_at_load is deprecated on macOS Reinstalling Radius version edge to namespace: radius-system... Error: failed to load Helm chart, err: recieved 403 unauthorized when downloading helm chart from the registry. you may want to perform a `docker logout ghcr.io` and re-try the command, Helm output: TraceId: dff0bdbe3da3f80fc6e8a84a9b5d38ea exit status 1 ``` ## Type of change - This pull request adds or changes features of Radius and has an approved issue (issue link required). Fixes: #6801 --------- Signed-off-by: willdavsmith --- pkg/cli/helm/helm.go | 55 ++++++++++++++++++++++++++++++++++-- pkg/cli/helm/helm_test.go | 40 ++++++++++++++++++++++++++ pkg/cli/helm/radiusclient.go | 2 +- 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 pkg/cli/helm/helm_test.go diff --git a/pkg/cli/helm/helm.go b/pkg/cli/helm/helm.go index 1ac7f38d44..322c21c1b3 100644 --- a/pkg/cli/helm/helm.go +++ b/pkg/cli/helm/helm.go @@ -20,15 +20,19 @@ import ( _ "embed" "errors" "fmt" + "net/http" "os" "path/filepath" "strings" "time" + containerderrors "github.com/containerd/containerd/remotes/errors" + "github.com/radius-project/radius/pkg/cli/clierrors" helm "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/registry" "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -79,7 +83,6 @@ func locateChartFile(dirPath string) (string, error) { func helmChartFromContainerRegistry(version string, config *helm.Configuration, repoUrl string, releaseName string) (*chart.Chart, error) { pull := helm.NewPull() - pull.RepoURL = repoUrl pull.Settings = &cli.EnvSettings{} pullopt := helm.WithConfig(config) pullopt(pull) @@ -101,9 +104,42 @@ func helmChartFromContainerRegistry(version string, config *helm.Configuration, pull.DestDir = dir - _, err = pull.Run(releaseName) + var chartRef string + + if !registry.IsOCI(repoUrl) { + // For non-OCI registries (like contour), we need to set the repo URL + // to the registry URL. The chartRef is the release name. + // ex. + // pull.RepoURL = https://charts.bitnami.com/bitnami + // pull.Run("contour") + pull.RepoURL = repoUrl + chartRef = releaseName + } else { + // For OCI registries (like radius), we will use the + // repo URL + the releaseName as the chartRef. + // pull.Run("oci://ghcr.io/radius-project/helm-chart/radius") + chartRef = fmt.Sprintf("%s/%s", repoUrl, releaseName) + + // Since we are using an OCI registry, we need to set the registry client + registryClient, err := registry.NewClient() + if err != nil { + return nil, err + } + + pull.SetRegistryClient(registryClient) + } + + _, err = pull.Run(chartRef) if err != nil { - return nil, fmt.Errorf("error downloading helm chart from the registry for version: %s, release name: %s. Error: %w", version, releaseName, err) + // Error handling for a specific case where credentials are stale. + // This happens for ghcr in particular because ghcr does not use + // subdomains - the scope of a login is all of ghcr.io. + // https://github.com/helm/helm/issues/12584 + if isHelm403Error(err) { + return nil, clierrors.Message("recieved 403 unauthorized when downloading helm chart from the registry. you may want to perform a `docker logout ghcr.io` and re-try the command") + } + + return nil, clierrors.MessageWithCause(err, fmt.Sprintf("error downloading helm chart from the registry for version: %s, release name: %s", version, releaseName)) } chartPath, err := locateChartFile(dir) @@ -136,3 +172,16 @@ func runUpgrade(upgradeClient *helm.Upgrade, releaseName string, helmChart *char } return err } + +// isHelm403Error is a helper function to determine if an error is a specific helm error +// (403 unauthorized when downloading a helm chart from ghcr.io) from a chain of errors. +func isHelm403Error(err error) bool { + var errUnexpectedStatus containerderrors.ErrUnexpectedStatus + if errors.As(err, &errUnexpectedStatus) { + if errUnexpectedStatus.StatusCode == http.StatusForbidden && strings.Contains(errUnexpectedStatus.RequestURL, "ghcr.io") { + return true + } + } + + return false +} diff --git a/pkg/cli/helm/helm_test.go b/pkg/cli/helm/helm_test.go new file mode 100644 index 0000000000..841ff2598a --- /dev/null +++ b/pkg/cli/helm/helm_test.go @@ -0,0 +1,40 @@ +package helm + +import ( + "errors" + "fmt" + "net/http" + "testing" + + containerderrors "github.com/containerd/containerd/remotes/errors" + "github.com/stretchr/testify/assert" +) + +func Test_isHelm403Error(t *testing.T) { + var err error + var result bool + + err = errors.New("error") + result = isHelm403Error(err) + assert.False(t, result) + + err = fmt.Errorf("%w: wrapped error", errors.New("error")) + result = isHelm403Error(err) + assert.False(t, result) + + err = fmt.Errorf("%w: wrapped error", containerderrors.ErrUnexpectedStatus{}) + result = isHelm403Error(err) + assert.False(t, result) + + err = fmt.Errorf("%w: wrapped error", containerderrors.ErrUnexpectedStatus{StatusCode: http.StatusForbidden, RequestURL: "ghcr.io/myregistry"}) + result = isHelm403Error(err) + assert.True(t, result) + + err = containerderrors.ErrUnexpectedStatus{StatusCode: http.StatusForbidden, RequestURL: "ghcr.io/myregistry"} + result = isHelm403Error(err) + assert.True(t, result) + + err = containerderrors.ErrUnexpectedStatus{StatusCode: http.StatusUnauthorized, RequestURL: "ghcr.io/myregistry"} + result = isHelm403Error(err) + assert.False(t, result) +} diff --git a/pkg/cli/helm/radiusclient.go b/pkg/cli/helm/radiusclient.go index d5c53e3ded..9b616b1000 100644 --- a/pkg/cli/helm/radiusclient.go +++ b/pkg/cli/helm/radiusclient.go @@ -36,7 +36,7 @@ import ( const ( radiusReleaseName = "radius" - radiusHelmRepo = "https://radius.azurecr.io/helm/v1/repo" + radiusHelmRepo = "oci://ghcr.io/radius-project/helm-chart" RadiusSystemNamespace = "radius-system" )