From e46d1c2645a61c5a3c9b68b1afaf284ec37250f2 Mon Sep 17 00:00:00 2001 From: xh4n3 Date: Tue, 28 May 2019 17:43:46 +0800 Subject: [PATCH] add readme, support auth-token Signed-off-by: xh4n3 --- README.md | 202 ++++++++---------------------------- cmd/helmpush/main.go | 4 + pkg/chartmuseum/download.go | 95 ++++++++++++++++- pkg/chartmuseum/option.go | 9 ++ pkg/chartmuseum/upload.go | 28 ++++- plugin.yaml | 14 +-- 6 files changed, 180 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 1fcacbe..198fe69 100644 --- a/README.md +++ b/README.md @@ -1,179 +1,59 @@ -# helm push plugin - +# helm acr -[![Codefresh build status]( https://g.codefresh.io/api/badges/pipeline/chartmuseum/chartmuseum%2Fhelm-push%2Fmaster?type=cf-1)]( https://g.codefresh.io/public/accounts/chartmuseum/pipelines/chartmuseum/helm-push/master) +[![CircleCI](https://circleci.com/gh/AliyunContainerService/helm-acr.svg?style=svg)](https://circleci.com/gh/AliyunContainerService/helm-acr) +[![Go Report Card](https://goreportcard.com/badge/github.com/AliyunContainerService/helm-acr)](https://goreportcard.com/report/github.com/AliyunContainerService/helm-acr) -Helm plugin to push chart package to [ChartMuseum](https://github.com/helm/chartmuseum) +Helm plugin to push chart package to [ChartMuseum](https://github.com/helm/chartmuseum). -## Install -Based on the version in `plugin.yaml`, release binary will be downloaded from GitHub: +This project is forked from [chartmuseum/helm-push](https://github.com/chartmuseum/helm-push). -``` -$ helm plugin install https://github.com/chartmuseum/helm-push -Downloading and installing helm-push v0.8.1 ... -https://github.com/chartmuseum/helm-push/releases/download/v0.8.1/helm-push_0.8.1_darwin_amd64.tar.gz -Installed plugin: push -``` - -## Usage -Start by adding a ChartMuseum-backed repo via Helm CLI (if not already added) -``` -$ helm repo add chartmuseum http://localhost:8080 -``` -For all available plugin options, please run -``` -$ helm push --help -``` - -### Pushing a directory -Point to a directory containing a valid `Chart.yaml` and the chart will be packaged and uploaded: -``` -$ cat mychart/Chart.yaml -name: mychart -version: 0.3.2 -``` -``` -$ helm push mychart/ chartmuseum -Pushing mychart-0.3.2.tgz to chartmuseum... -Done. -``` - -### Pushing with a custom version -The `--version` flag can be provided, which will push the package with a custom version. - -Here is an example using the last git commit id as the version: -``` -$ helm push mychart/ --version="$(git log -1 --pretty=format:%h)" chartmuseum -Pushing mychart-5abbbf28.tgz to chartmuseum... -Done. -``` -If you want to enable something like `--version="latest"`, which you intend to push regularly, you will need to run your ChartMuseum server with `ALLOW_OVERWRITE=true`. - -### Push .tgz package -This workflow does not require the use of `helm package`, but pushing .tgzs is still suppported: -``` -$ helm push mychart-0.3.2.tgz chartmuseum -Pushing mychart-0.3.2.tgz to chartmuseum... -Done. -``` - -### Force push -If your ChartMuseum install is configured with `ALLOW_OVERWRITE=true`, chart versions will be automatically overwritten upon re-upload. - -Otherwise, unless your install is configured with `DISABLE_FORCE_OVERWRITE=true` (ChartMuseum > v0.7.1), you can use the `--force`/`-f` option to to force an upload: -``` -$ helm push --force mychart-0.3.2.tgz chartmuseum -Pushing mychart-0.3.2.tgz to chartmuseum... -Done. -``` - -### Pushing directly to URL -If the second argument provided resembles a URL, you are not required to add the repo prior to push: -``` -$ helm push mychart-0.3.2.tgz http://localhost:8080 -Pushing mychart-0.3.2.tgz to http://localhost:8080... -Done. -``` - -## Context Path - -If you are running ChartMuseum behind a proxy that adds a route prefix, for example: -``` -https://my.chart.repo.com/helm/v1/index.yaml -> http://chartmuseum-svc/index.yaml -``` - -You can use the `--context-path=` option or `HELM_REPO_CONTEXT_PATH` env var in order for the plugin to construct the upload URL correctly: -``` -helm repo add chartmuseum https://my.chart.repo.com/helm/v1 -helm push --context-path=/helm/v1 mychart-0.3.2.tgz chartmuseum -``` - -Alternatively, you can add `serverInfo.contextPath` to your index.yaml: -``` -apiVersion: v1 -entries:{} -generated: "2018-08-09T11:08:21-05:00" -serverInfo: - contextPath: /helm/v1 -``` - -In ChartMuseum server (>0.7.1) this will automatically be added to index.yaml if the `--context-path` option is provided. +Some modifications has been made to meet the security requirements on Alibaba Cloud: +* the plugin is able to talk to auth server to gain a Bearer Token. +* the plugin is able to use the Bearer Token to download/upload charts to Chartmuseum. +* the plugin registers `acr`(short for Alibaba Cloud Container Registry) as protocol name in `plugin.yaml`. -## Authentication -### Basic Auth -If you have added your repo with the `--username`/`--password` flags (Helm 2.9+), or have added your repo with the basic auth username/password in the URL (e.g. `https://myuser:mypass@my.chart.repo.com`), no further setup is required. +### Installation -The plugin will use the auth info located in `~/.helm/repository/repositories.yaml` in order to authenticate. +```bash +# make sure you have git installed +yum install -y git -If you are running ChartMuseum with `AUTH_ANONYMOUS_GET=true`, and have added your repo without authentication, the plugin recognizes the following environment variables for basic auth on push operations: +# install plugin +helm plugin install https://github.com/AliyunContainerService/helm-acr ``` -$ export HELM_REPO_USERNAME="myuser" -$ export HELM_REPO_PASSWORD="mypass" -``` - -With this setup, you can enable people to use your repo for installing charts etc. without allowing them to upload to it. -### Token +### Usage -*ChartMuseum token-auth is currently in progress. Pleasee see [auth-server-example](https://github.com/chartmuseum/auth-server-example) for more info.* +Before you use Alibaba Cloud Container Registry's hosted Helm charts service, you should: +* purchase an ACR Enterprise Edition instance and activate its Helm charts service +* have a Kubernetes cluster and have `helm init` done +* make sure you have Internet access to GitHub to download plugin +* create a Helm chart namespace in your ACR Enterprise Edition -Although ChartMuseum server does not define or accept a token format (yet), if you are running it behind a proxy that accepts access tokens, you can provide the following env var: -``` -$ export HELM_REPO_ACCESS_TOKEN="" -``` +```bash +# add namespace/repo to your local repository +# please change username/password/namespace/repo/url below +export HELM_REPO_USERNAME=username; export HELM_REPO_PASSWORD=password; +helm repo add demo acr://hello-acr-helm.cn-hangzhou.cr.aliyuncs.com/foo/bar --username ${HELM_REPO_USERNAME} --password ${HELM_REPO_PASSWORD} -This will result in all basic auth options above being ignored, and the plugin will send the token in the header: -``` -Authorization: Bearer -``` +# create an empty chart locally +helm create hello-acr -If you require a custom header to be used for passing the token, you can the following env var: -``` -$ export HELM_REPO_AUTH_HEADER="" -``` +# push the chart +helm push hello-acr demo -This will then be used in place of `Authorization: Bearer`: -``` -: -``` +# delete local chart +rm -r hello-acr -#### Token config file (~/.cfconfig) -For users of [Managed Helm Repositories](https://codefresh.io/codefresh-news/introducing-managed-helm-repositories/) (Codefresh), the plugin is able to auto-detect your API key from `~/.cfconfig`. This file is managed by [Codefresh CLI](https://codefresh-io.github.io/cli/). +# update charts index from remote +helm repo update -If detected, this API key will be used for token-based auth, overriding basic auth options described above. +# show all remote charts +helm search -The format of this file is the following: +# fetch the chart we uploaded +helm fetch demo/hello-acr -``` -contexts: - default: - name: default - token: -current-context: default -``` - -### TLS Client Cert Auth - -ChartMuseum server does not yet have options to setup TLS client cert authentication (please see [chartmuseum#79](https://github.com/helm/chartmuseum/issues/79)). - -If you are running ChartMuseum behind a frontend that does, the following options are available: - -``` ---ca-file string Verify certificates of HTTPS-enabled servers using this CA bundle [$HELM_REPO_CA_FILE] ---cert-file string Identify HTTPS client using this SSL certificate file [$HELM_REPO_CERT_FILE] ---key-file string Identify HTTPS client using this SSL key file [$HELM_REPO_KEY_FILE] ---insecure Connect to server with an insecure way by skipping certificate verification [$HELM_REPO_INSECURE] -``` - -## Custom Downloader -This plugin also defines the `cm://` protocol that you may specify when adding a repo: -``` -$ helm repo add chartmuseum cm://my.chart.repo.com -``` - -The only real difference with this vs. simply using http/https, is that the environment variables above are recognized by the plugin and used to set the `Authorization` header appropriately. As in, if you do not add your repo in this way, you are unable to use token-based auth for GET requests (downloading index.yaml, chart .tgzs, etc). - -By default, `cm://` translates to `https://`. If you must use `http://`, you can set the following env var: -``` -$ export HELM_REPO_USE_HTTP="true" -``` +# delete local repository +helm repo remove demo +``` \ No newline at end of file diff --git a/cmd/helmpush/main.go b/cmd/helmpush/main.go index b585290..c63e4f7 100644 --- a/cmd/helmpush/main.go +++ b/cmd/helmpush/main.go @@ -1,3 +1,5 @@ +// Modifications copyright (C) 2019 Alibaba Group Holding Limited / Yuning Xie (xyn1016@gmail.com) + package main import ( @@ -291,6 +293,7 @@ func (p *pushCmd) push() error { cm.CertFile(p.certFile), cm.KeyFile(p.keyFile), cm.InsecureSkipVerify(p.insecureSkipVerify), + cm.AutoTokenAuth(true), ) if err != nil { @@ -365,6 +368,7 @@ func (p *pushCmd) download(fileURL string) error { cm.CertFile(p.certFile), cm.KeyFile(p.keyFile), cm.InsecureSkipVerify(p.insecureSkipVerify), + cm.AutoTokenAuth(true), ) if err != nil { diff --git a/pkg/chartmuseum/download.go b/pkg/chartmuseum/download.go index 912625e..2ade62b 100644 --- a/pkg/chartmuseum/download.go +++ b/pkg/chartmuseum/download.go @@ -1,7 +1,11 @@ +// Modifications copyright (C) 2019 Alibaba Group Holding Limited / Yuning Xie (xyn1016@gmail.com) + package chartmuseum import ( + "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "path" @@ -21,11 +25,28 @@ func (client *Client) DownloadFile(filePath string) (*http.Response, error) { return nil, err } - if client.opts.accessToken != "" { + accessToken := client.opts.accessToken + + if client.opts.autoTokenAuth { + resp, err := client.Do(req) + if err != nil { + return resp, err + } else if resp.StatusCode == http.StatusUnauthorized { + token, err := client.GetAuthTokenFromResponse(resp) + if err != nil { + return nil, err + } + accessToken = token + } else { + return resp, err + } + } + + if accessToken != "" { if client.opts.authHeader != "" { req.Header.Set(client.opts.authHeader, client.opts.accessToken) } else { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.opts.accessToken)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) } } else if client.opts.username != "" && client.opts.password != "" { req.SetBasicAuth(client.opts.username, client.opts.password) @@ -33,3 +54,73 @@ func (client *Client) DownloadFile(filePath string) (*http.Response, error) { return client.Do(req) } + +func (client *Client) GetAuthTokenFromResponse(resp *http.Response) (string, error) { + authHeader := resp.Header.Get("Www-Authenticate") + authHeader = strings.Split(authHeader, " ")[1] + tokens := strings.Split(authHeader, ",") + var realm, service, scope string + for _, token := range tokens { + if strings.HasPrefix(token, "realm") { + realm = strings.Trim(token[len("realm="):], "\"") + } + if strings.HasPrefix(token, "service") { + service = strings.Trim(token[len("service="):], "\"") + } + if strings.HasPrefix(token, "scope") { + scope = strings.Trim(token[len("scope="):], "\"") + } + } + if realm == "" { + return "", fmt.Errorf("missing realm in bearer auth challenge") + } + if service == "" { + return "", fmt.Errorf("missing service in bearer auth challenge") + } + if scope == "" { + return "", fmt.Errorf("missing scope in bearer auth challenge") + } + return client.getBearerToken(realm, service, scope) +} + +func (client *Client) getBearerToken(realm, service, scope string) (string, error) { + authReq, err := http.NewRequest("POST", realm, nil) + if err != nil { + return "", err + } + getParams := authReq.URL.Query() + getParams.Add("service", service) + if scope != "" { + getParams.Add("scope", scope) + } + authReq.URL.RawQuery = getParams.Encode() + if client.opts.username != "" && client.opts.password != "" { + authReq.SetBasicAuth(client.opts.username, client.opts.password) + } + resp, err := client.Do(authReq) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return "", err + } + switch resp.StatusCode { + case http.StatusUnauthorized: + return "", fmt.Errorf("unable to retrieve auth token: 401 unauthorized") + case http.StatusOK: + break + default: + return "", fmt.Errorf("unexpected http code: %d, URL: %s", resp.StatusCode, authReq.URL) + } + tokenBlob, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + token := struct { + Token string `json:"access_token"` + }{} + if err := json.Unmarshal(tokenBlob, &token); err != nil { + return "", err + } + return token.Token, nil +} diff --git a/pkg/chartmuseum/option.go b/pkg/chartmuseum/option.go index 950e57a..344dd58 100644 --- a/pkg/chartmuseum/option.go +++ b/pkg/chartmuseum/option.go @@ -1,3 +1,5 @@ +// Modifications copyright (C) 2019 Alibaba Group Holding Limited / Yuning Xie (xyn1016@gmail.com) + package chartmuseum import ( @@ -21,6 +23,7 @@ type ( certFile string keyFile string insecureSkipVerify bool + autoTokenAuth bool } ) @@ -100,3 +103,9 @@ func InsecureSkipVerify(insecureSkipVerify bool) Option { opts.insecureSkipVerify = insecureSkipVerify } } + +func AutoTokenAuth(autoTokenAuth bool) Option { + return func(opts *options) { + opts.autoTokenAuth = autoTokenAuth + } +} diff --git a/pkg/chartmuseum/upload.go b/pkg/chartmuseum/upload.go index dd381ae..862d344 100644 --- a/pkg/chartmuseum/upload.go +++ b/pkg/chartmuseum/upload.go @@ -1,3 +1,5 @@ +// Modifications copyright (C) 2019 Alibaba Group Holding Limited / Yuning Xie (xyn1016@gmail.com) + package chartmuseum import ( @@ -36,11 +38,33 @@ func (client *Client) UploadChartPackage(chartPackagePath string, force bool) (* return nil, err } - if client.opts.accessToken != "" { + accessToken := client.opts.accessToken + + if client.opts.autoTokenAuth { + resp, err := client.Do(req) + if err != nil { + return resp, err + } else if resp.StatusCode == http.StatusUnauthorized { + token, err := client.GetAuthTokenFromResponse(resp) + if err != nil { + return nil, err + } + accessToken = token + + err = setUploadChartPackageRequestBody(req, chartPackagePath) + if err != nil { + return nil, err + } + } else { + return resp, err + } + } + + if accessToken != "" { if client.opts.authHeader != "" { req.Header.Set(client.opts.authHeader, client.opts.accessToken) } else { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.opts.accessToken)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) } } else if client.opts.username != "" && client.opts.password != "" { req.SetBasicAuth(client.opts.username, client.opts.password) diff --git a/plugin.yaml b/plugin.yaml index d1035d8..b9b0d1d 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,13 +1,13 @@ name: "push" -version: "0.8.1" -usage: "Please see https://github.com/chartmuseum/helm-push for usage" -description: "Push chart package to ChartMuseum" +version: "0.7.1" +usage: "Please see https://github.com/AliyunContainerService/helm-acr for usage" +description: "Push chart package to Alibaba Container Registry" command: "$HELM_PLUGIN_DIR/bin/helmpush" downloaders: -- command: "bin/helmpush" - protocols: - - "cm" + - command: "bin/helmpush" + protocols: + - "acr" useTunnel: false hooks: install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh" - update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh" + update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh" \ No newline at end of file