Skip to content

Commit 2ad868d

Browse files
committed
check if helm chart is served by registry/v1 bundle
1 parent fc88b93 commit 2ad868d

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

internal/shared/util/helm/chart.go

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package helm
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"regexp"
9+
"slices"
10+
"strings"
11+
12+
"helm.sh/helm/v3/pkg/chart"
13+
"helm.sh/helm/v3/pkg/chart/loader"
14+
"helm.sh/helm/v3/pkg/registry"
15+
)
16+
17+
func IsChart(ctx context.Context, chartUri string) (chart, oci bool, err error) {
18+
addr, err := url.Parse(chartUri)
19+
if err != nil {
20+
return chart, oci, err
21+
}
22+
23+
if addr.Scheme != "" {
24+
if !strings.HasPrefix(addr.Scheme, "http") {
25+
err = fmt.Errorf("unexpected Url scheme; %s\n", addr.Scheme)
26+
return
27+
}
28+
29+
oci = false
30+
helmchart, err := validateHelmChart(addr.String())
31+
if err != nil {
32+
chart = false
33+
return chart, oci, err
34+
}
35+
36+
if helmchart != nil &&
37+
helmchart.Metadata != nil &&
38+
helmchart.Metadata.Name != "" {
39+
chart = true
40+
}
41+
42+
return chart, oci, err
43+
}
44+
45+
ociRe := regexp.MustCompile("^(?P<host>[a-zA-Z0-9-_.:]+)([/]?)(?P<org>[a-zA-Z0-9-_/]+)?([/](?P<chart>[a-zA-Z0-9-_.:@]+))$")
46+
if ociRe.MatchString(chartUri) {
47+
oci = true
48+
49+
chart, err = helmOciCheck(ctx, chartUri)
50+
if err != nil {
51+
return chart, oci, err
52+
}
53+
}
54+
55+
return
56+
}
57+
58+
// helmOciCheck() pull a helm chart using the provided chartUri from an
59+
// OCI registiry and inspects its media type to determine if a Helm chart
60+
func helmOciCheck(ctx context.Context, chartUri string) (bool, error) {
61+
helmclient, err := registry.NewClient()
62+
if err != nil {
63+
return false, err
64+
}
65+
66+
summary, err := helmclient.Pull(chartUri,
67+
registry.PullOptWithProv(false),
68+
registry.PullOptWithChart(true),
69+
registry.PullOptIgnoreMissingProv(true),
70+
)
71+
if err != nil {
72+
return false, err
73+
}
74+
75+
return summary != nil && summary.Ref != "", nil
76+
}
77+
78+
func validateHelmChart(chartUri string) (*chart.Chart, error) {
79+
// Download helm chart from HTTP
80+
resp, err := http.Get(chartUri)
81+
if err != nil {
82+
return nil, fmt.Errorf("loading URL failed; %w", err)
83+
}
84+
defer resp.Body.Close()
85+
86+
if resp.StatusCode != http.StatusOK {
87+
return nil, fmt.Errorf("unexpected response; %w", err)
88+
}
89+
90+
if !slices.Contains(resp.Header["Content-Type"], "application/octet-stream") {
91+
return nil, fmt.Errorf("unknown contype-type")
92+
}
93+
94+
return loader.LoadArchive(resp.Body)
95+
}
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package helm_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
helmutils "github.com/operator-framework/operator-controller/internal/shared/util/helm"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestIsChart(t *testing.T) {
12+
type response struct {
13+
Oci bool
14+
Chart bool
15+
}
16+
17+
tt := []struct {
18+
name string
19+
url string
20+
want response
21+
wantErr bool
22+
}{
23+
{
24+
name: "pull helm chart using image tag",
25+
url: "quay.io/eochieng/metrics-server:3.12.0",
26+
want: response{
27+
Oci: true,
28+
Chart: true,
29+
},
30+
wantErr: false,
31+
},
32+
{
33+
name: "pull helm chart using image digest",
34+
url: "quay.io/eochieng/metrics-server@sha256:dd56f2ccc6e29ba7a2c5492e12c8210fb7367771eca93380a8dd64a6c9c985cb",
35+
want: response{
36+
Oci: true,
37+
Chart: true,
38+
},
39+
wantErr: false,
40+
},
41+
{
42+
name: "pull helm chart from HTTP repository",
43+
url: "https://github.com/kubernetes-sigs/metrics-server/releases/download/metrics-server-helm-chart-3.12.0/metrics-server-3.12.0.tgz",
44+
want: response{
45+
Oci: false,
46+
Chart: true,
47+
},
48+
wantErr: false,
49+
},
50+
{
51+
name: "pull helm chart with oci scheme",
52+
url: "oci://quay.io/eochieng/metrics-server@sha256:dd56f2ccc6e29ba7a2c5492e12c8210fb7367771eca93380a8dd64a6c9c985cb",
53+
want: response{
54+
Oci: false,
55+
Chart: false,
56+
},
57+
wantErr: true,
58+
},
59+
{
60+
name: "pull kubernetes web page",
61+
url: "https://kubernetes.io",
62+
want: response{
63+
Oci: false,
64+
Chart: false,
65+
},
66+
wantErr: true,
67+
},
68+
{
69+
name: "pull busybox image from OCI registry",
70+
url: "quay.io/opdev/busybox:latest",
71+
want: response{
72+
Oci: true,
73+
Chart: false,
74+
},
75+
wantErr: true,
76+
},
77+
}
78+
79+
for _, tc := range tt {
80+
t.Run(tc.name, func(t *testing.T) {
81+
chart, oci, err := helmutils.IsChart(context.Background(), tc.url)
82+
assert.Equal(t, oci, tc.want.Oci)
83+
assert.Equal(t, chart, tc.want.Chart)
84+
85+
if testing.Verbose() {
86+
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)
87+
}
88+
89+
if !tc.wantErr {
90+
assert.NoError(t, err)
91+
} else {
92+
assert.Error(t, err, "helm chart not found")
93+
}
94+
})
95+
}
96+
}

0 commit comments

Comments
 (0)