Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
115 changes: 115 additions & 0 deletions internal/shared/util/helm/chart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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"
)

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 HelmCheckResponse{}, err
}

if addr.Scheme != "" {
if !strings.HasPrefix(addr.Scheme, "http") {
return HelmCheckResponse{}, fmt.Errorf("unexpected scheme; %s", addr.Scheme)
}

helmchart, err := validateHelmChart(addr.String())
if err != nil {
return HelmCheckResponse{}, err
}

if helmchart != nil &&
helmchart.Metadata != nil &&
helmchart.Metadata.Name != "" {
return HelmCheckResponse{
Chart: true,
Oci: false,
}, err
}
}

ociRe := regexp.MustCompile("^(?P<host>[a-zA-Z0-9-_.:]+)([/]?)(?P<org>[a-zA-Z0-9-_/]+)?([/](?P<chart>[a-zA-Z0-9-_.:@]+))$")
if !ociRe.MatchString(chartURI) {
return HelmCheckResponse{
Chart: false,
Oci: false,
}, fmt.Errorf("does not conform to OCI url format")
}

ociCheck, err := helmOciCheck(ctx, chartURI)
if err != nil {
return HelmCheckResponse{
Chart: false,
Oci: true,
}, err
}

return HelmCheckResponse{
Chart: ociCheck,
Oci: true,
}, nil
}

// 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(_ 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
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)
}
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)
}
97 changes: 97 additions & 0 deletions internal/shared/util/helm/chart_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package helm_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"

helmutils "github.com/operator-framework/operator-controller/internal/shared/util/helm"
)

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) {
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 checking if %s is a helm chart.\n", tc.url)
}

if !tc.wantErr {
assert.NoError(t, err)
} else {
assert.Error(t, err, "helm chart not found")
}
})
}
}
Loading