Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch Radius Helm chart pull from ACR to GHCR #7455

Merged
merged 10 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions pkg/cli/helm/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ import (
_ "embed"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"

containerderrors "github.com/containerd/containerd/remotes/errors"
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"
)

Expand Down Expand Up @@ -79,7 +82,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)
Expand All @@ -101,8 +103,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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if repoUrl is of this type oci://ghcr.io/radius-project?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should work for all oci registries if we need to add them in the future. So I don't think we need to hard code it. the registry.IsOCI() call should determine if it's an oci registry or not.

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 {
// 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
err := extractHelmError(err)
if err != nil {
return nil, err
}

return nil, fmt.Errorf("error downloading helm chart from the registry for version: %s, release name: %s. Error: %w", version, releaseName, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using clierrors.Message for this. rad has special formatting for that type.

Basically it's helpful to know the difference between:

  • You made a mistake, and it's not a bug in Radius. (clierrors.Message)
  • Something weird happened, no idea 🤷 (any other error)

}

Expand Down Expand Up @@ -136,3 +172,17 @@ func runUpgrade(upgradeClient *helm.Upgrade, releaseName string, helmChart *char
}
return err
}

// extractHelmError is a helper function to extract a specific error
// (403 unauthorized when downloading a helm chart from ghcr.io) from a chain of errors.
// If the error is not found, it returns nil.
func extractHelmError(err error) error {
var errUnexpectedStatus containerderrors.ErrUnexpectedStatus
if errors.As(err, &errUnexpectedStatus) {
if errUnexpectedStatus.StatusCode == http.StatusForbidden && strings.Contains(errUnexpectedStatus.RequestURL, "ghcr.io") {
return fmt.Errorf("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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using clierrors.Message for this.

}
}

return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to also handle the case where the error is NOT an containerderrors.ErrUnexpectedStatus. Think about a case where the user is not connected to the internet.

Every time we special case one type of error, we need to also handle the case where it's some other type of error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, nevermind. I see you did that above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
33 changes: 33 additions & 0 deletions pkg/cli/helm/helm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package helm

import (
"errors"
"fmt"
"net/http"
"testing"

containerderrors "github.com/containerd/containerd/remotes/errors"
"github.com/stretchr/testify/assert"
)

func TestExtractHelmError(t *testing.T) {
err := errors.New("error")
extractedErr := extractHelmError(err)
assert.Nil(t, extractedErr)

err = fmt.Errorf("%w: wrapped error", errors.New("error"))
extractedErr = extractHelmError(err)
assert.Nil(t, extractedErr)

err = fmt.Errorf("%w: wrapped error", containerderrors.ErrUnexpectedStatus{})
extractedErr = extractHelmError(err)
assert.Nil(t, extractedErr)

err = fmt.Errorf("%w: wrapped error", containerderrors.ErrUnexpectedStatus{StatusCode: http.StatusForbidden, RequestURL: "ghcr.io/myregistry"})
extractedErr = extractHelmError(err)
assert.Equal(t, errors.New("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"), extractedErr)

err = containerderrors.ErrUnexpectedStatus{StatusCode: http.StatusForbidden, RequestURL: "ghcr.io/myregistry"}
extractedErr = extractHelmError(err)
assert.Equal(t, errors.New("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"), extractedErr)
}
2 changes: 1 addition & 1 deletion pkg/cli/helm/radiusclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down