From ec820a1e8b294d3bf3fd44b549a6613e4349d3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20A=CC=8Ahsberg?= Date: Tue, 1 Mar 2022 20:34:50 +0100 Subject: [PATCH] Add support for Helm3 OCI registries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mathias Åhsberg --- apis/release/v1beta1/types.go | 2 + examples/sample/release-oci.yaml | 62 ++++++++++ examples/sample/release.yaml | 1 + package/crds/helm.crossplane.io_releases.yaml | 4 + pkg/clients/helm/args.go | 2 + pkg/clients/helm/client.go | 114 +++++++++++++++--- pkg/controller/release/release.go | 1 + 7 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 examples/sample/release-oci.yaml diff --git a/apis/release/v1beta1/types.go b/apis/release/v1beta1/types.go index 8dc4021..9631229 100644 --- a/apis/release/v1beta1/types.go +++ b/apis/release/v1beta1/types.go @@ -91,6 +91,8 @@ type ReleaseParameters struct { ValuesSpec `json:",inline"` // SkipCRDs skips installation of CRDs for the release. SkipCRDs bool `json:"skipCRDs,omitempty"` + // InsecureSkipTLSVerify skips tls certificate checks for the chart download + InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` } // ReleaseObservation are the observable fields of a Release. diff --git a/examples/sample/release-oci.yaml b/examples/sample/release-oci.yaml new file mode 100644 index 0000000..b24167e --- /dev/null +++ b/examples/sample/release-oci.yaml @@ -0,0 +1,62 @@ +apiVersion: helm.crossplane.io/v1beta1 +kind: Release +metadata: + name: wordpress-example +spec: +# rollbackLimit: 3 + forProvider: + chart: + name: wordpress + repository: "oci://localhost:5000/helm-charts" + version: 9.3.19 +# pullSecretRef: +# name: oci-creds +# namespace: default +# url: "oci://localhost:5000/helm-charts/wordpress:9.3.19" + namespace: wordpress +# insecureSkipTLSVerify: true +# skipCreateNamespace: true +# wait: true +# skipCRDs: true + values: + service: + type: ClusterIP + set: + - name: param1 + value: value2 +# valuesFrom: +# - configMapKeyRef: +# key: values.yaml +# name: default-vals +# namespace: wordpress +# optional: false +# - secretKeyRef: +# key: svalues.yaml +# name: svals +# namespace: wordpress +# optional: false +# connectionDetails: +# - apiVersion: v1 +# kind: Service +# name: wordpress-example +# namespace: wordpress +# fieldPath: spec.clusterIP +# #fieldPath: status.loadBalancer.ingress[0].ip +# toConnectionSecretKey: ip +# - apiVersion: v1 +# kind: Service +# name: wordpress-example +# namespace: wordpress +# fieldPath: spec.ports[0].port +# toConnectionSecretKey: port +# - apiVersion: v1 +# kind: Secret +# name: wordpress-example +# namespace: wordpress +# fieldPath: data.wordpress-password +# toConnectionSecretKey: password +# writeConnectionSecretToRef: +# name: wordpress-credentials +# namespace: crossplane-system + providerConfigRef: + name: helm-provider diff --git a/examples/sample/release.yaml b/examples/sample/release.yaml index 3ca76a7..d02c1fb 100644 --- a/examples/sample/release.yaml +++ b/examples/sample/release.yaml @@ -14,6 +14,7 @@ spec: # namespace: default # url: "https://charts.bitnami.com/bitnami/wordpress-9.3.19.tgz" namespace: wordpress +# insecureSkipTLSVerify: true # skipCreateNamespace: true # wait: true # skipCRDs: true diff --git a/package/crds/helm.crossplane.io_releases.yaml b/package/crds/helm.crossplane.io_releases.yaml index f04a066..06ced53 100644 --- a/package/crds/helm.crossplane.io_releases.yaml +++ b/package/crds/helm.crossplane.io_releases.yaml @@ -481,6 +481,10 @@ spec: latest version if not set type: string type: object + insecureSkipTLSVerify: + description: InsecureSkipTLSVerify skips tls certificate checks + for the chart download + type: boolean namespace: description: Namespace to install the release into. type: string diff --git a/pkg/clients/helm/args.go b/pkg/clients/helm/args.go index 72744d8..6fd83b9 100644 --- a/pkg/clients/helm/args.go +++ b/pkg/clients/helm/args.go @@ -12,4 +12,6 @@ type Args struct { Timeout time.Duration // SkipCRDs skips CRDs creation during Helm release install or upgrade. SkipCRDs bool + // InsecureSkipTLSVerify skips tls certificate checks for the chart download + InsecureSkipTLSVerify bool } diff --git a/pkg/clients/helm/client.go b/pkg/clients/helm/client.go index 1cd5ac0..70ceed3 100644 --- a/pkg/clients/helm/client.go +++ b/pkg/clients/helm/client.go @@ -51,6 +51,8 @@ const ( errFailedToLoadChart = "failed to load chart" errUnexpectedDirContentTmpl = "expected 1 .tgz chart file, got [%s]" errFailedToParseURL = "failed to parse URL" + errFailedToLogin = "failed to login to registry" + errUnexpectedOCIUrlTmpl = "url not prefixed with oci://, got [%s]" ) // Client is the interface to interact with Helm @@ -71,6 +73,7 @@ type client struct { upgradeClient *action.Upgrade rollbackClient *action.Rollback uninstallClient *action.Uninstall + loginClient *action.RegistryLogin } // ArgsApplier defines helm client arguments helper @@ -111,6 +114,7 @@ func NewClient(log logging.Logger, restConfig *rest.Config, argAppliers ...ArgsA pc.DestDir = chartCache pc.Settings = &cli.EnvSettings{} + pc.InsecureSkipTLSverify = args.InsecureSkipTLSVerify gc := action.NewGet(actionConfig) @@ -119,11 +123,13 @@ func NewClient(log logging.Logger, restConfig *rest.Config, argAppliers ...ArgsA ic.Wait = args.Wait ic.Timeout = args.Timeout ic.SkipCRDs = args.SkipCRDs + ic.InsecureSkipTLSverify = args.InsecureSkipTLSVerify uc := action.NewUpgrade(actionConfig) uc.Wait = args.Wait uc.Timeout = args.Timeout uc.SkipCRDs = args.SkipCRDs + uc.InsecureSkipTLSverify = args.InsecureSkipTLSVerify uic := action.NewUninstall(actionConfig) @@ -131,6 +137,8 @@ func NewClient(log logging.Logger, restConfig *rest.Config, argAppliers ...ArgsA rb.Wait = args.Wait rb.Timeout = args.Timeout + lc := action.NewRegistryLogin(actionConfig) + return &client{ log: log, pullClient: pc, @@ -139,6 +147,7 @@ func NewClient(log logging.Logger, restConfig *rest.Config, argAppliers ...ArgsA upgradeClient: uc, rollbackClient: rb, uninstallClient: uic, + loginClient: lc, }, nil } @@ -190,16 +199,31 @@ func (hc *client) pullChart(spec *v1beta1.ChartSpec, creds *RepoCreds, chartDir chartRef := spec.URL if spec.URL == "" { - chartRef = spec.Name - - pc.RepoURL = spec.Repository + if registry.IsOCI(spec.Repository) { + chartRef = resolveOCIChartRef(spec.Repository, spec.Name) + } else { + chartRef = spec.Name + pc.RepoURL = spec.Repository + } pc.Version = spec.Version + } else if registry.IsOCI(spec.URL) { + ociURL, version, err := resolveOCIChartVersion(spec.URL) + if err != nil { + return err + } + pc.Version = version + chartRef = ociURL.String() } pc.Username = creds.Username pc.Password = creds.Password pc.DestDir = chartDir + err := hc.login(spec, creds, pc.InsecureSkipTLSverify) + if err != nil { + return err + } + o, err := pc.Run(chartRef) hc.log.Debug(o) if err != nil { @@ -208,32 +232,63 @@ func (hc *client) pullChart(spec *v1beta1.ChartSpec, creds *RepoCreds, chartDir return nil } +func (hc *client) login(spec *v1beta1.ChartSpec, creds *RepoCreds, insecure bool) error { + ociURL := spec.URL + if spec.URL == "" { + ociURL = spec.Repository + } + if !registry.IsOCI(ociURL) { + return nil + } + parsedURL, err := url.Parse(ociURL) + if err != nil { + return errors.Wrap(err, errFailedToParseURL) + } + var out strings.Builder + err = hc.loginClient.Run(&out, parsedURL.Host, creds.Username, creds.Password, insecure) + hc.log.Debug(out.String()) + return errors.Wrap(err, errFailedToLogin) +} + func (hc *client) PullAndLoadChart(spec *v1beta1.ChartSpec, creds *RepoCreds) (*chart.Chart, error) { var chartFilePath string var err error - if spec.URL == "" && spec.Version == "" { + switch { + case spec.URL == "" && spec.Version == "": chartFilePath, err = hc.pullLatestChartVersion(spec, creds) if err != nil { return nil, err } - } else { - filename := fmt.Sprintf("%s-%s.tgz", spec.Name, spec.Version) - if spec.URL != "" { - u, err := url.Parse(spec.URL) - if err != nil { - return nil, errors.Wrap(err, errFailedToParseURL) - } - filename = path.Base(u.Path) + case registry.IsOCI(spec.URL): + u, v, err := resolveOCIChartVersion(spec.URL) + if err != nil { + return nil, err } - chartFilePath = filepath.Join(chartCache, filename) - if _, err := os.Stat(chartFilePath); os.IsNotExist(err) { - if err = hc.pullChart(spec, creds, chartCache); err != nil { + if v == "" { + chartFilePath, err = hc.pullLatestChartVersion(spec, creds) + if err != nil { return nil, err } - } else if err != nil { - return nil, errors.Wrap(err, errFailedToCheckIfLocalChartExists) + } else { + chartFilePath = resolveChartFilePath(path.Base(u.Path), v) } + case spec.URL != "": + u, err := url.Parse(spec.URL) + if err != nil { + return nil, errors.Wrap(err, errFailedToParseURL) + } + chartFilePath = filepath.Join(chartCache, path.Base(u.Path)) + default: + chartFilePath = resolveChartFilePath(spec.Name, spec.Version) + } + + if _, err := os.Stat(chartFilePath); os.IsNotExist(err) { + if err = hc.pullChart(spec, creds, chartCache); err != nil { + return nil, err + } + } else if err != nil { + return nil, errors.Wrap(err, errFailedToCheckIfLocalChartExists) } chart, err := loader.Load(chartFilePath) @@ -283,3 +338,28 @@ func (hc *client) Uninstall(release string) error { _, err := hc.uninstallClient.Run(release) return err } + +func resolveOCIChartVersion(chartURL string) (*url.URL, string, error) { + if !registry.IsOCI(chartURL) { + return nil, "", errors.Errorf(errUnexpectedOCIUrlTmpl, chartURL) + } + ociURL, err := url.Parse(chartURL) + if err != nil { + return nil, "", errors.Wrap(err, errFailedToParseURL) + } + parts := strings.Split(ociURL.Path, ":") + if len(parts) > 1 { + ociURL.Path = parts[0] + return ociURL, parts[1], nil + } + return ociURL, "", nil +} + +func resolveChartFilePath(name string, version string) string { + filename := fmt.Sprintf("%s-%s.tgz", name, version) + return filepath.Join(chartCache, filename) +} + +func resolveOCIChartRef(repository string, name string) string { + return strings.Join([]string{strings.TrimSuffix(repository, "/"), name}, "/") +} diff --git a/pkg/controller/release/release.go b/pkg/controller/release/release.go index 3cb9917..fb25838 100644 --- a/pkg/controller/release/release.go +++ b/pkg/controller/release/release.go @@ -136,6 +136,7 @@ func withRelease(cr *v1beta1.Release) helmClient.ArgsApplier { config.Wait = cr.Spec.ForProvider.Wait config.Timeout = waitTimeout(cr) config.SkipCRDs = cr.Spec.ForProvider.SkipCRDs + config.InsecureSkipTLSVerify = cr.Spec.ForProvider.InsecureSkipTLSVerify } }