From a8753953e4f364cbd6f2da8ac71f7d3e95745764 Mon Sep 17 00:00:00 2001 From: Edmund Ochieng Date: Thu, 20 Mar 2025 12:02:57 -0500 Subject: [PATCH 1/2] check if helm chart is served by registry/v1 bundle --- internal/shared/util/helm/chart.go | 95 ++++++++++++++++++++++++ internal/shared/util/helm/chart_test.go | 96 +++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 internal/shared/util/helm/chart.go create mode 100644 internal/shared/util/helm/chart_test.go diff --git a/internal/shared/util/helm/chart.go b/internal/shared/util/helm/chart.go new file mode 100644 index 000000000..41c1e441a --- /dev/null +++ b/internal/shared/util/helm/chart.go @@ -0,0 +1,95 @@ +package helm + +import ( + "context" + "fmt" + "net/http" + "net/url" + "regexp" + "slices" + "strings" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/registry" +) + +func IsChart(ctx context.Context, chartUri string) (chart, oci bool, err error) { + addr, err := url.Parse(chartUri) + if err != nil { + return chart, oci, err + } + + if addr.Scheme != "" { + if !strings.HasPrefix(addr.Scheme, "http") { + err = fmt.Errorf("unexpected Url scheme; %s\n", addr.Scheme) + return + } + + oci = false + helmchart, err := validateHelmChart(addr.String()) + if err != nil { + chart = false + return chart, oci, err + } + + if helmchart != nil && + helmchart.Metadata != nil && + helmchart.Metadata.Name != "" { + chart = true + } + + return chart, oci, err + } + + ociRe := regexp.MustCompile("^(?P[a-zA-Z0-9-_.:]+)([/]?)(?P[a-zA-Z0-9-_/]+)?([/](?P[a-zA-Z0-9-_.:@]+))$") + if ociRe.MatchString(chartUri) { + oci = true + + chart, err = helmOciCheck(ctx, chartUri) + if err != nil { + return chart, oci, err + } + } + + return +} + +// helmOciCheck() pull a helm chart using the provided chartUri from an +// OCI registiry and inspects its media type to determine if a Helm chart +func helmOciCheck(ctx context.Context, chartUri string) (bool, error) { + helmclient, err := registry.NewClient() + if err != nil { + return false, err + } + + summary, err := helmclient.Pull(chartUri, + registry.PullOptWithProv(false), + registry.PullOptWithChart(true), + registry.PullOptIgnoreMissingProv(true), + ) + if err != nil { + return false, err + } + + return summary != nil && summary.Ref != "", nil +} + +func validateHelmChart(chartUri string) (*chart.Chart, error) { + // Download helm chart from HTTP + resp, err := http.Get(chartUri) + if err != nil { + return nil, fmt.Errorf("loading URL failed; %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected response; %w", err) + } + + if !slices.Contains(resp.Header["Content-Type"], "application/octet-stream") { + return nil, fmt.Errorf("unknown contype-type") + } + + return loader.LoadArchive(resp.Body) +} diff --git a/internal/shared/util/helm/chart_test.go b/internal/shared/util/helm/chart_test.go new file mode 100644 index 000000000..ad2e84c2c --- /dev/null +++ b/internal/shared/util/helm/chart_test.go @@ -0,0 +1,96 @@ +package helm_test + +import ( + "context" + "testing" + + helmutils "github.com/operator-framework/operator-controller/internal/shared/util/helm" + "github.com/stretchr/testify/assert" +) + +func TestIsChart(t *testing.T) { + type response struct { + Oci bool + Chart bool + } + + tt := []struct { + name string + url string + want response + wantErr bool + }{ + { + name: "pull helm chart using image tag", + url: "quay.io/eochieng/metrics-server:3.12.0", + want: response{ + Oci: true, + Chart: true, + }, + wantErr: false, + }, + { + name: "pull helm chart using image digest", + url: "quay.io/eochieng/metrics-server@sha256:dd56f2ccc6e29ba7a2c5492e12c8210fb7367771eca93380a8dd64a6c9c985cb", + want: response{ + Oci: true, + Chart: true, + }, + wantErr: false, + }, + { + name: "pull helm chart from HTTP repository", + url: "https://github.com/kubernetes-sigs/metrics-server/releases/download/metrics-server-helm-chart-3.12.0/metrics-server-3.12.0.tgz", + want: response{ + Oci: false, + Chart: true, + }, + wantErr: false, + }, + { + name: "pull helm chart with oci scheme", + url: "oci://quay.io/eochieng/metrics-server@sha256:dd56f2ccc6e29ba7a2c5492e12c8210fb7367771eca93380a8dd64a6c9c985cb", + want: response{ + Oci: false, + Chart: false, + }, + wantErr: true, + }, + { + name: "pull kubernetes web page", + url: "https://kubernetes.io", + want: response{ + Oci: false, + Chart: false, + }, + wantErr: true, + }, + { + name: "pull busybox image from OCI registry", + url: "quay.io/opdev/busybox:latest", + want: response{ + Oci: true, + Chart: false, + }, + wantErr: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + chart, oci, err := helmutils.IsChart(context.Background(), tc.url) + assert.Equal(t, oci, tc.want.Oci) + assert.Equal(t, chart, tc.want.Chart) + + if testing.Verbose() { + t.Logf("IsChart() is verifying if %s is a helm chart.\n The result should be %t but, got %t\n", tc.url, chart, tc.want.Chart) + } + + if !tc.wantErr { + assert.NoError(t, err) + } else { + assert.Error(t, err, "helm chart not found") + } + }) + } +} From 97c7e46afb94d440fd82b28791bcfb31667de0df Mon Sep 17 00:00:00 2001 From: Edmund Ochieng Date: Mon, 24 Mar 2025 12:36:08 -0500 Subject: [PATCH 2/2] Fix golang ci linting issues Signed-off-by: Edmund Ochieng --- internal/shared/util/helm/chart.go | 66 ++++++++++++++++--------- internal/shared/util/helm/chart_test.go | 11 +++-- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/internal/shared/util/helm/chart.go b/internal/shared/util/helm/chart.go index 41c1e441a..1bb5be48a 100644 --- a/internal/shared/util/helm/chart.go +++ b/internal/shared/util/helm/chart.go @@ -14,56 +14,71 @@ import ( "helm.sh/helm/v3/pkg/registry" ) -func IsChart(ctx context.Context, chartUri string) (chart, oci bool, err error) { - addr, err := url.Parse(chartUri) +type HelmCheckResponse struct { + // Chart returns true if helm chart + Chart bool + // Oci returns true if resource is stored + // in an OCI registry + Oci bool +} + +func IsChart(ctx context.Context, chartURI string) (HelmCheckResponse, error) { + addr, err := url.Parse(chartURI) if err != nil { - return chart, oci, err + return HelmCheckResponse{}, err } if addr.Scheme != "" { if !strings.HasPrefix(addr.Scheme, "http") { - err = fmt.Errorf("unexpected Url scheme; %s\n", addr.Scheme) - return + return HelmCheckResponse{}, fmt.Errorf("unexpected scheme; %s", addr.Scheme) } - oci = false helmchart, err := validateHelmChart(addr.String()) if err != nil { - chart = false - return chart, oci, err + return HelmCheckResponse{}, err } if helmchart != nil && helmchart.Metadata != nil && helmchart.Metadata.Name != "" { - chart = true + return HelmCheckResponse{ + Chart: true, + Oci: false, + }, err } - - return chart, oci, err } ociRe := regexp.MustCompile("^(?P[a-zA-Z0-9-_.:]+)([/]?)(?P[a-zA-Z0-9-_/]+)?([/](?P[a-zA-Z0-9-_.:@]+))$") - if ociRe.MatchString(chartUri) { - oci = true + if !ociRe.MatchString(chartURI) { + return HelmCheckResponse{ + Chart: false, + Oci: false, + }, fmt.Errorf("does not conform to OCI url format") + } - chart, err = helmOciCheck(ctx, chartUri) - if err != nil { - return chart, oci, err - } + ociCheck, err := helmOciCheck(ctx, chartURI) + if err != nil { + return HelmCheckResponse{ + Chart: false, + Oci: true, + }, err } - return + return HelmCheckResponse{ + Chart: ociCheck, + Oci: true, + }, nil } -// helmOciCheck() pull a helm chart using the provided chartUri from an +// helmOciCheck() pull a helm chart using the provided chartURI from an // OCI registiry and inspects its media type to determine if a Helm chart -func helmOciCheck(ctx context.Context, chartUri string) (bool, error) { +func helmOciCheck(_ context.Context, chartURI string) (bool, error) { helmclient, err := registry.NewClient() if err != nil { return false, err } - summary, err := helmclient.Pull(chartUri, + summary, err := helmclient.Pull(chartURI, registry.PullOptWithProv(false), registry.PullOptWithChart(true), registry.PullOptIgnoreMissingProv(true), @@ -75,9 +90,14 @@ func helmOciCheck(ctx context.Context, chartUri string) (bool, error) { return summary != nil && summary.Ref != "", nil } -func validateHelmChart(chartUri string) (*chart.Chart, error) { +func validateHelmChart(chartURI string) (*chart.Chart, error) { // Download helm chart from HTTP - resp, err := http.Get(chartUri) + req, err := http.NewRequest(http.MethodGet, chartURI, nil) + if err != nil { + return nil, fmt.Errorf("creating request failed; %w", err) + } + + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("loading URL failed; %w", err) } diff --git a/internal/shared/util/helm/chart_test.go b/internal/shared/util/helm/chart_test.go index ad2e84c2c..2e6501557 100644 --- a/internal/shared/util/helm/chart_test.go +++ b/internal/shared/util/helm/chart_test.go @@ -4,8 +4,9 @@ import ( "context" "testing" - helmutils "github.com/operator-framework/operator-controller/internal/shared/util/helm" "github.com/stretchr/testify/assert" + + helmutils "github.com/operator-framework/operator-controller/internal/shared/util/helm" ) func TestIsChart(t *testing.T) { @@ -78,12 +79,12 @@ func TestIsChart(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - chart, oci, err := helmutils.IsChart(context.Background(), tc.url) - assert.Equal(t, oci, tc.want.Oci) - assert.Equal(t, chart, tc.want.Chart) + response, err := helmutils.IsChart(context.Background(), tc.url) + assert.Equal(t, response.Oci, tc.want.Oci) + assert.Equal(t, response.Chart, tc.want.Chart) if testing.Verbose() { - t.Logf("IsChart() is verifying if %s is a helm chart.\n The result should be %t but, got %t\n", tc.url, chart, tc.want.Chart) + t.Logf("IsChart() is checking if %s is a helm chart.\n", tc.url) } if !tc.wantErr {