diff --git a/.gitignore b/.gitignore index 13be40c0d1..8eeb7191f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ .idea *.DS_Store /fleet +/.vscode diff --git a/docs/gitrepo-structure.md b/docs/gitrepo-structure.md index 3175a0563f..1a44efbce9 100644 --- a/docs/gitrepo-structure.md +++ b/docs/gitrepo-structure.md @@ -59,7 +59,8 @@ kustomize: dir: ./kustomize helm: - # Use a custom location for the Helm chart. This can refer to any go-getter URL. + # Use a custom location for the Helm chart. This can refer to any go-getter URL or + # OCI registry based helm chart URL e.g. "oci://ghcr.io/fleetrepoci/guestbook". # This allows one to download charts from most any location. Also know that # go-getter URL supports adding a digest to validate the download. If repo # is set below this field is the name of the chart to lookup @@ -73,6 +74,7 @@ helm: releaseName: my-release # The version of the chart or semver constraint of the chart to find. If a constraint # is specified it is evaluated each time git changes. + # The version also determines which chart to download from OCI registries. version: 0.1.0 # Any values that should be placed in the `values.yaml` and passed to helm during # install. diff --git a/e2e/assets/single-cluster/helm-oci.yaml b/e2e/assets/single-cluster/helm-oci.yaml new file mode 100644 index 0000000000..b3ed7c7a4a --- /dev/null +++ b/e2e/assets/single-cluster/helm-oci.yaml @@ -0,0 +1,16 @@ +kind: GitRepo +apiVersion: fleet.cattle.io/v1alpha1 +metadata: + name: helm +spec: + repo: https://github.com/rancher/fleet-examples + branch: add-helm-oci-example + paths: + - single-cluster/helm-oci + targets: + - clusterSelector: + matchExpressions: + - key: provider.cattle.io + operator: NotIn + values: + - harvester \ No newline at end of file diff --git a/e2e/single-cluster/single_cluster_test.go b/e2e/single-cluster/single_cluster_test.go index 85ab9a4ca2..6928aad75b 100644 --- a/e2e/single-cluster/single_cluster_test.go +++ b/e2e/single-cluster/single_cluster_test.go @@ -32,6 +32,19 @@ var _ = Describe("Single Cluster Examples", func() { }) + Context("containing an oci based helm chart", func() { + BeforeEach(func() { + asset = "single-cluster/helm-oci.yaml" + }) + + It("deploys the helm chart", func() { + Eventually(func() string { + out, _ := k.Namespace("fleet-helm-oci-example").Get("pods") + return out + }, testenv.Timeout).Should(ContainSubstring("frontend-")) + }) + }) + When("creating a gitrepo resource", func() { Context("containing a helm chart", func() { BeforeEach(func() { diff --git a/pkg/bundle/read.go b/pkg/bundle/read.go index 81fc8e2d8a..92a71267db 100644 --- a/pkg/bundle/read.go +++ b/pkg/bundle/read.go @@ -39,7 +39,7 @@ func Open(ctx context.Context, name, baseDir, file string, opts *Options) (*Bund } if file == "-" { - return Read(ctx, name, baseDir, os.Stdin, opts) + return mayCompress(ctx, name, baseDir, os.Stdin, opts) } var ( @@ -65,7 +65,7 @@ func Open(ctx context.Context, name, baseDir, file string, opts *Options) (*Bund in = f } - return Read(ctx, name, baseDir, in, opts) + return mayCompress(ctx, name, baseDir, in, opts) } // Try accessing the documented, primary fleet.yaml extension first. If that returns an "IsNotExist" error, then we @@ -89,7 +89,7 @@ func setupIOReader(baseDir string) (*os.File, error) { return nil, nil } -func Read(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, opts *Options) (*Bundle, error) { +func mayCompress(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, opts *Options) (*Bundle, error) { if opts == nil { opts = &Options{} } diff --git a/pkg/bundle/resources.go b/pkg/bundle/resources.go index e7b1bc7e55..122c84e587 100644 --- a/pkg/bundle/resources.go +++ b/pkg/bundle/resources.go @@ -12,6 +12,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "sort" "strings" "sync" @@ -21,6 +22,9 @@ import ( "github.com/pkg/errors" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/downloader" + helmgetter "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/repo" "github.com/rancher/fleet/modules/cli/pkg/progress" @@ -83,7 +87,7 @@ func readResources(ctx context.Context, spec *fleet.BundleSpec, compress bool, b return result, nil } -func ChartPath(helm *fleet.HelmOptions) string { +func checksum(helm *fleet.HelmOptions) string { if helm == nil { return "none" } @@ -186,11 +190,12 @@ func addCharts(directories []directory, base string, charts []*fleet.HelmOptions } directories = append(directories, directory{ - prefix: ChartPath(chart), - base: base, - path: chartURL, - key: ChartPath(chart), - auth: auth, + prefix: checksum(chart), + base: base, + path: chartURL, + key: checksum(chart), + auth: auth, + version: chart.Version, }) } } @@ -216,11 +221,12 @@ func addDirectory(directories []directory, base, customDir, defaultDir string) ( } type directory struct { - prefix string - base string - path string - key string - auth Auth + prefix string + base string + path string + key string + version string + auth Auth } func readDirectories(ctx context.Context, compress bool, directories ...directory) (map[string][]fleet.BundleResource, error) { @@ -241,7 +247,7 @@ func readDirectories(ctx context.Context, compress bool, directories ...director dir := dir eg.Go(func() error { defer sem.Release(1) - resources, err := readDirectory(ctx, compress, dir.prefix, dir.base, dir.path, dir.auth) + resources, err := readDirectory(ctx, compress, dir.prefix, dir.base, dir.path, dir.version, dir.auth) if err != nil { return err } @@ -261,10 +267,10 @@ func readDirectories(ctx context.Context, compress bool, directories ...director return result, eg.Wait() } -func readDirectory(ctx context.Context, compress bool, prefix, base, name string, auth Auth) ([]fleet.BundleResource, error) { +func readDirectory(ctx context.Context, compress bool, prefix, base, name, version string, auth Auth) ([]fleet.BundleResource, error) { var resources []fleet.BundleResource - files, err := readContent(ctx, base, name, auth) + files, err := readContent(ctx, base, name, version, auth) if err != nil { return nil, err } @@ -295,13 +301,29 @@ func readDirectory(ctx context.Context, compress bool, prefix, base, name string return resources, nil } -func readContent(ctx context.Context, base, name string, auth Auth) (map[string][]byte, error) { +func readContent(ctx context.Context, base, name, version string, auth Auth) (map[string][]byte, error) { temp, err := os.MkdirTemp("", "fleet") if err != nil { return nil, err } defer os.RemoveAll(temp) + src := name + + // go-getter does not support downloading OCI registry based files yet + // until this is implemented we use Helm to download charts from OCI based registries + // and provide the downloaded file to go-getter locally + hasOCIURL, err := regexp.MatchString(`^oci:\/\/`, name) + if err != nil { + return nil, err + } + if hasOCIURL { + src, err = downloadOCIChart(name, version, temp) + if err != nil { + return nil, err + } + } + temp = filepath.Join(temp, "content") base, err = filepath.Abs(base) @@ -309,7 +331,6 @@ func readContent(ctx context.Context, base, name string, auth Auth) (map[string] return nil, err } - src := name if auth.SSHPrivateKey != nil { if !strings.ContainsAny(src, "?") { src += "?" @@ -387,6 +408,21 @@ func readContent(ctx context.Context, base, name string, auth Auth) (map[string] return files, nil } +// downloadOciChart uses Helm to download charts from OCI based registries +func downloadOCIChart(name, version, path string) (string, error) { + c := downloader.ChartDownloader{ + Verify: downloader.VerifyNever, + Getters: helmgetter.All(&cli.EnvSettings{}), + } + + saved, _, err := c.DownloadTo(name, version, path) + if err != nil { + return "", err + } + + return saved, nil +} + func newHttpGetter(auth Auth) *getter.HttpGetter { httpGetter := &getter.HttpGetter{ Client: &http.Client{}, diff --git a/pkg/bundle/style.go b/pkg/bundle/style.go index 25c0c1bd5c..ca9296dc0c 100644 --- a/pkg/bundle/style.go +++ b/pkg/bundle/style.go @@ -20,7 +20,7 @@ func chartPath(options fleet.BundleDeploymentOptions) (string, string) { if options.Helm == nil || options.Helm.Chart == "" { return chartYAML, "" } - return joinAndClean(options.Helm.Chart, chartYAML), ChartPath(options.Helm) + "/" + return joinAndClean(options.Helm.Chart, chartYAML), checksum(options.Helm) + "/" } func kustomizePath(options fleet.BundleDeploymentOptions) string {