diff --git a/Dockerfile b/Dockerfile index 80416f4..a2efafc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.15 AS khelm +FROM alpine:3.16 AS khelm RUN apk update --no-cache RUN mkdir /helm && chown root:nobody /helm && chmod 1777 /helm ENV HELM_REPOSITORY_CONFIG=/helm/repository/repositories.yaml diff --git a/README.md b/README.md index 942b6ef..630a9ad 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Since [kpt](https://github.com/GoogleContainerTools/kpt) is [published](https:// * Automatically fetches and updates required repository index files when needed * Allows to automatically reload dependencies when lock file is out of sync * Allows to use any repository without registering it in repositories.yaml +* Allows to use a Helm chart from a remote git repository * Allows to exclude certain resources from the Helm chart output * Allows to enforce namespace-scoped resources within the template output * Allows to enforce a namespace on all resources @@ -212,10 +213,17 @@ khelm template cert-manager --version=0.9.x --repo=https://charts.jetstack.io _For all available options see the [table](#configuration-options) below._ #### Docker usage example + +Generate a manifest from a chart: ```sh docker run mgoltzsche/khelm:latest template cert-manager --version=0.9.x --repo=https://charts.jetstack.io ``` +Generate a manifest from a chart within a git repository: +```sh +docker run mgoltzsche/khelm:latest template cert-manager --repo=git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2 +``` + ### Go API The khelm Go API `github.com/mgoltzsche/khelm/v2/pkg/helm` provides a simple templating interface on top of the Helm Go API. @@ -269,6 +277,20 @@ When running khelm as kpt function or within a container the `repositories.yaml` Unlike Helm khelm allows usage of any repository when `repositories.yaml` is not present or `--trust-any-repo` (env var `KHELM_TRUST_ANY_REPO`) is enabled. +#### Git URLs as Helm repositories + +Helm charts can be fetched from git repositories by letting the Helm repository URL point to the chart's parent directory using the URL scheme `git+https` or `git+ssh`. +The path within the git repository URL and the repository part of the URL must be separated by `@`. +The `ref` parameter can be used to specify the git tag. + +The following example points to an old version of cert-manager using a git URL: +``` +git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2 +``` + +This feature is meant to be compatible with Helm's [helm-git](https://github.com/aslafy-z/helm-git#usage) plugin (but is reimplemented in Go). +However currently khelm does not support `sparse` git checkouts (due to [lack of support in go-git](https://github.com/go-git/go-git/issues/90)). + ## Helm support * Helm 2 is supported by the `v1` module version. diff --git a/e2e/cli-tests.bats b/e2e/cli-tests.bats index e39af7f..95899f6 100755 --- a/e2e/cli-tests.bats +++ b/e2e/cli-tests.bats @@ -1,5 +1,7 @@ #!/usr/bin/env bats +bats_require_minimum_version 1.5.0 + IMAGE=${IMAGE:-mgoltzsche/khelm:latest} EXAMPLE_DIR="$(pwd)/example" OUT_DIR="$(mktemp -d)" @@ -20,6 +22,15 @@ teardown() { grep -q myrelease "$OUT_DIR/subdir/manifest.yaml" } +@test "CLI should reject repository when not in repositories.yaml and trust-any disabled" { + run -1 docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" -e KHELM_TRUST_ANY_REPO=false "$IMAGE" template cert-manager \ + --name=myrelease \ + --version 1.0.4 \ + --repo https://charts.jetstack.io \ + --output /out/subdir/manifest.yaml \ + --debug +} + @test "CLI should output kustomization" { docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" -v "$EXAMPLE_DIR/namespace:/chart" "$IMAGE" template /chart \ --output /out/kdir/ \ @@ -37,4 +48,37 @@ teardown() { --debug [ -f "$OUT_DIR/manifest.yaml" ] grep -q myreleasex "$OUT_DIR/manifest.yaml" -} \ No newline at end of file +} + +@test "CLI should accept git url as helm repository" { + docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" "$IMAGE" template cert-manager \ + --repo git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2 \ + --output /out/manifest.yaml \ + --debug + [ -f "$OUT_DIR/manifest.yaml" ] + grep -q ca-sync "$OUT_DIR/manifest.yaml" +} + +@test "CLI should cache git repository" { + mkdir $OUT_DIR/cache + docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" -v "$OUT_DIR/cache:/helm/cache" "$IMAGE" template cert-manager \ + --repo git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2 \ + --output /out/manifest.yaml \ + --debug + [ -f "$OUT_DIR/manifest.yaml" ] + grep -q ca-sync "$OUT_DIR/manifest.yaml" + rm -f "$OUT_DIR/manifest.yaml" + docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" -v "$OUT_DIR/cache:/helm/cache" --network=none "$IMAGE" template cert-manager \ + --repo git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2 \ + --output /out/manifest.yaml \ + --debug + [ -f "$OUT_DIR/manifest.yaml" ] + grep -q ca-sync "$OUT_DIR/manifest.yaml" +} + +@test "CLI should reject git repository when not in repositories.yaml and trust-any disabled" { + run -1 docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" -e KHELM_TRUST_ANY_REPO=false "$IMAGE" template cert-manager \ + --repo git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2 \ + --output /out/manifest.yaml \ + --debug +} diff --git a/example/git-getter/Chart.lock b/example/git-getter/Chart.lock new file mode 100644 index 0000000..b47d51d --- /dev/null +++ b/example/git-getter/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: cert-manager + repository: git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2 + version: v0.6.6 +digest: sha256:cd75b404696beff523a4297ba055389e9dd9e8214f47c668cfbf3f200ea41191 +generated: "2022-10-14T00:58:00.186187675+02:00" diff --git a/example/git-getter/Chart.yaml b/example/git-getter/Chart.yaml new file mode 100644 index 0000000..660fb6d --- /dev/null +++ b/example/git-getter/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +description: example chart using a git url as dependency +name: git-getter-example-chart +version: 0.1.0 +dependencies: +- name: cert-manager + version: "x.x.x" + repository: "git+https://github.com/cert-manager/cert-manager@deploy/charts?ref=v0.6.2" diff --git a/example/git-getter/generator.yaml b/example/git-getter/generator.yaml new file mode 100644 index 0000000..ac0125e --- /dev/null +++ b/example/git-getter/generator.yaml @@ -0,0 +1,6 @@ +apiVersion: khelm.mgoltzsche.github.com/v2 +kind: ChartRenderer +metadata: + name: cert-manager + namespace: cert-manager +chart: . diff --git a/example/git-getter/kustomization.yaml b/example/git-getter/kustomization.yaml new file mode 100644 index 0000000..318ffad --- /dev/null +++ b/example/git-getter/kustomization.yaml @@ -0,0 +1,2 @@ +generators: +- generator.yaml diff --git a/go.mod b/go.mod index 76f1ec0..8730a7a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/Masterminds/semver/v3 v3.1.1 + github.com/go-git/go-git/v5 v5.4.2 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.0 @@ -27,8 +28,11 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/sprig/v3 v3.2.2 // indirect github.com/Masterminds/squirrel v1.5.3 // indirect + github.com/Microsoft/go-winio v0.5.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/acomagu/bufpipe v1.0.3 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -44,10 +48,13 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/emirpasic/gods v1.12.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.13.0 // indirect github.com/go-errors/errors v1.0.1 // indirect + github.com/go-git/gcfg v1.5.0 // indirect + github.com/go-git/go-billy/v5 v5.3.1 // indirect github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -68,9 +75,11 @@ require ( github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -82,6 +91,7 @@ require ( github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect @@ -102,10 +112,12 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/rubenv/sql-migrate v1.1.2 // indirect github.com/russross/blackfriday v1.5.2 // indirect + github.com/sergi/go-diff v1.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect @@ -124,6 +136,7 @@ require ( google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/api v0.25.0 // indirect k8s.io/apiextensions-apiserver v0.25.0 // indirect diff --git a/go.sum b/go.sum index 393677a..a087e71 100644 --- a/go.sum +++ b/go.sum @@ -57,24 +57,34 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= +github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= +github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= +github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= @@ -144,6 +154,8 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7fo github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -161,10 +173,22 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= +github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= +github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= +github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= +github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -328,6 +352,9 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -346,6 +373,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= +github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= @@ -385,6 +414,8 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= @@ -409,6 +440,8 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= @@ -512,8 +545,10 @@ github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXY github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -548,6 +583,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= +github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -587,6 +624,7 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -596,6 +634,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -671,6 +711,7 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -719,6 +760,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -753,9 +795,11 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -965,6 +1009,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/getter/git/checkout.go b/pkg/getter/git/checkout.go new file mode 100644 index 0000000..59e78b0 --- /dev/null +++ b/pkg/getter/git/checkout.go @@ -0,0 +1,79 @@ +package git + +import ( + "context" + "fmt" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" +) + +func gitCheckoutImpl(ctx context.Context, repoURL, ref, destDir string) error { + /*err := runCmds(destDir, [][]string{ + {"git", "init", "--quiet"}, + {"git", "remote", "add", "origin", repoURL}, + + {"git", "config", "core.sparseCheckout", "true"}, + {"git", "sparse-checkout", "set", path}, + {"git", "pull", "--quiet", "--depth", "1", "origin", ref}, + + //{"git", "fetch", "--quiet", "--tags", "origin"}, + //{"git", "checkout", "--quiet", ref}, + })*/ + r, err := git.PlainCloneContext(ctx, destDir, false, &git.CloneOptions{ + URL: repoURL, + // TODO: Auth: ... + RemoteName: "origin", + SingleBranch: true, + Depth: 1, + NoCheckout: true, + }) + if err != nil { + return err + } + tree, err := r.Worktree() + if err != nil { + return fmt.Errorf("git worktree: %w", err) + } + // TODO: support sparse checkout, see https://github.com/go-git/go-git/issues/90 + opts := git.CheckoutOptions{} + if ref != "" { + opts.Branch = plumbing.ReferenceName("refs/tags/" + ref) + } + err = tree.Checkout(&opts) + if err != nil { + return err + } + return nil +} + +/* +func runCmds(dir string, cmds [][]string) error { + for _, c := range cmds { + err := runCmd(dir, c[0], c[1:]...) + if err != nil { + return err + } + } + return nil +} + +func runCmd(dir, cmd string, args ...string) error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + c := exec.CommandContext(ctx, cmd, args...) + var stderr bytes.Buffer + c.Stderr = &stderr + c.Dir = dir + err := c.Run() + if err != nil { + msg := strings.TrimSpace(stderr.String()) + if msg == "" { + msg = err.Error() + } + cmds := append([]string{cmd}, args...) + return fmt.Errorf("%s: %s", strings.Join(cmds, " "), msg) + } + return err +} +*/ diff --git a/pkg/getter/git/gitgetter.go b/pkg/getter/git/gitgetter.go new file mode 100644 index 0000000..83e9d4f --- /dev/null +++ b/pkg/getter/git/gitgetter.go @@ -0,0 +1,227 @@ +package git + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + "log" + "os" + "path" + "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli" + helmgetter "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" + helmyaml "sigs.k8s.io/yaml" +) + +var gitCheckout = gitCheckoutImpl + +type HelmPackageFunc func(ctx context.Context, path, repoDir string) (string, error) + +func New(settings *cli.EnvSettings, packageFn HelmPackageFunc) helmgetter.Constructor { + return func(o ...helmgetter.Option) (helmgetter.Getter, error) { + return &gitIndexGetter{ + settings: settings, + packageFn: packageFn, + }, nil + } +} + +type gitIndexGetter struct { + settings *cli.EnvSettings + Getters helmgetter.Providers + packageFn HelmPackageFunc +} + +func (g *gitIndexGetter) Get(location string, options ...helmgetter.Option) (*bytes.Buffer, error) { + ctx := context.Background() + ref, err := parseURL(location) + if err != nil { + return nil, err + } + if ref.Ref == "" { + log.Println("WARNING: Specifying a git URL without the ref parameter may return a cached, outdated version") + } + isRepoIndex := path.Base(ref.Path) == "index.yaml" + var b []byte + if isRepoIndex { + // Generate repo index from directory + ref = ref.Dir() + repoDir, err := download(ctx, ref, g.settings.RepositoryCache) + if err != nil { + return nil, err + } + dir := filepath.Join(repoDir, filepath.FromSlash(ref.Path)) + idx, err := generateRepoIndex(dir, g.settings.RepositoryCache, ref) + if err != nil { + return nil, fmt.Errorf("generate git repo index: %w", err) + } + b, err = helmyaml.Marshal(idx) + if err != nil { + return nil, fmt.Errorf("marshal generated repo index: %w", err) + } + } else { + // Build and package chart + chartPath := filepath.FromSlash(strings.TrimSuffix(ref.Path, ".tgz")) + repoDir, err := download(ctx, ref, g.settings.RepositoryCache) + if err != nil { + return nil, err + } + if _, e := os.Stat(filepath.Join(filepath.Join(repoDir, chartPath), "Chart.yaml")); e == nil { + tmpDir, err := os.MkdirTemp("", "khelm-git-") + if err != nil { + return nil, err + } + defer os.RemoveAll(tmpDir) + tmpRepoDir := filepath.Join(tmpDir, "repo") + tmpChartDir := filepath.Join(tmpRepoDir, chartPath) + err = copyDir(repoDir, tmpRepoDir) + if err != nil { + return nil, fmt.Errorf("make temp git repo copy: %w", err) + } + tgzFile, err := g.packageFn(ctx, tmpChartDir, tmpRepoDir) + if err != nil { + return nil, fmt.Errorf("package %s: %w", location, err) + } + b, err = os.ReadFile(tgzFile) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("unsupported git location: %s", location) + } + } + var buf bytes.Buffer + _, err = buf.Write(b) + return &buf, err +} + +func generateRepoIndex(dir, cacheDir string, u *gitURL) (*repo.IndexFile, error) { + idx := repo.NewIndexFile() + idx.APIVersion = "v2" + idx.Entries = map[string]repo.ChartVersions{} + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, file := range files { + if file.IsDir() { + chartDir := filepath.Join(dir, file.Name()) + chartYamlFile := filepath.Join(chartDir, "Chart.yaml") + b, err := os.ReadFile(chartYamlFile) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + chrt := chart.Metadata{} + err = helmyaml.Unmarshal(b, &chrt) + if err != nil { + return nil, fmt.Errorf("read %s: %w", chartYamlFile, err) + } + idx.Entries[file.Name()] = repo.ChartVersions{ + { + Metadata: &chrt, + URLs: []string{"git+" + u.JoinPath(file.Name()+".tgz").String()}, + }, + } + } + } + return idx, nil +} + +func download(ctx context.Context, ref *gitURL, cacheDir string) (string, error) { + repoRef := *ref + repoRef.Path = "" + cacheKey := fmt.Sprintf("sha256-%x", sha256.Sum256([]byte(repoRef.String()))) + cacheDir = filepath.Join(cacheDir, "git") + destDir := filepath.Join(cacheDir, cacheKey) + + if _, e := os.Stat(destDir); os.IsNotExist(e) { + err := os.MkdirAll(cacheDir, 0755) + if err != nil { + return "", err + } + tmpDir, err := os.MkdirTemp(cacheDir, ".tmp-") + if err != nil { + return "", err + } + defer os.RemoveAll(tmpDir) + + tmpRepoDir := tmpDir + err = gitCheckout(ctx, ref.Repo, ref.Ref, tmpRepoDir) + if err != nil { + return "", err + } + if err = os.Rename(tmpRepoDir, destDir); err != nil && !os.IsExist(err) { + return "", err + } + } + return destDir, nil +} + +func copyDir(srcDir, dstDir string) error { + files, err := os.ReadDir(srcDir) + if err != nil { + return err + } + err = os.Mkdir(dstDir, 0750) + if err != nil { + return err + } + for _, file := range files { + name := file.Name() + if name == ".git" { + continue + } + srcFile := filepath.Join(srcDir, name) + dstFile := filepath.Join(dstDir, name) + if file.IsDir() { + err = copyDir(srcFile, dstFile) + if err != nil { + return err + } + } else if file.Type() == os.ModeSymlink { + linkDest, err := os.Readlink(srcFile) + if err != nil { + return err + } + err = os.Symlink(srcFile, linkDest) + if err != nil { + return err + } + } else if file.Type() != os.ModeCharDevice && file.Type() != os.ModeDevice && file.Type() != os.ModeIrregular { + err = copyFile(srcFile, dstFile) + if err != nil { + return fmt.Errorf("copy %s to %s", srcFile, dstFile) + } + } else { + log.Printf("WARNING: ignoring file with unsupported type (%d) found at %s", file.Type(), srcFile) + } + } + return nil +} + +func copyFile(src, dst string) error { + fin, err := os.Open(src) + if err != nil { + return err + } + defer fin.Close() + fout, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer fout.Close() + _, err = io.Copy(fout, fin) + if err != nil { + return err + } + return nil +} diff --git a/pkg/getter/git/gitgetter_test.go b/pkg/getter/git/gitgetter_test.go new file mode 100644 index 0000000..0074b95 --- /dev/null +++ b/pkg/getter/git/gitgetter_test.go @@ -0,0 +1,72 @@ +package git + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/repo" + "sigs.k8s.io/yaml" +) + +func TestGitGetter(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "khelm-git-getter-test-") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + gitCheckout = func(_ context.Context, repoURL, ref, destDir string) error { + err := os.MkdirAll(filepath.Join(destDir, "mypath", "fakechart"), 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(destDir, "mypath", "fakechart", "Chart.yaml"), []byte(` +apiVersion: v2 +name: fake-chart +version: 0.1.0`), 0600) + require.NoError(t, err) + return nil + } + packageCalls := 0 + settings := cli.New() + settings.RepositoryCache = filepath.Join(tmpDir, "cache") + fakePackageFn := func(ctx context.Context, path, repoDir string) (string, error) { + packageCalls++ + file := filepath.Join(tmpDir, "fake-chart.tgz") + err := os.WriteFile(file, []byte("fake-chart-tgz-contents "+path), 0600) + require.NoError(t, err) + return file, nil + } + testee := New(settings, fakePackageFn) + getter, err := testee() + require.NoError(t, err) + + b, err := getter.Get("git+https://git.example.org/org/repo@mypath/index.yaml?ref=v0.6.2") + require.NoError(t, err) + idx := repo.NewIndexFile() + err = yaml.Unmarshal(b.Bytes(), idx) + require.NoErrorf(t, err, "unmarshal Get() result: %s", b.String()) + expect := repo.NewIndexFile() + expect.APIVersion = "v2" + fakeChartURL := "git+https://git.example.org/org/repo@mypath/fakechart.tgz?ref=v0.6.2" + expect.Entries = map[string]repo.ChartVersions{ + "fakechart": []*repo.ChartVersion{ + { + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "fake-chart", + Version: "0.1.0", + }, + URLs: []string{fakeChartURL}, + }, + }, + } + expect.Generated = idx.Generated + require.Equal(t, expect, idx, "should generate repository index from directory") + + b, err = getter.Get(fakeChartURL) + require.NoError(t, err) + require.Contains(t, b.String(), "fake-chart-tgz-contents /tmp/khelm-git-", "should return packaged chart") + require.Truef(t, strings.HasSuffix(b.String(), "/repo/mypath/fakechart"), "should end with path within repo but was: %s", b.String()) +} diff --git a/pkg/getter/git/url.go b/pkg/getter/git/url.go new file mode 100644 index 0000000..58ef975 --- /dev/null +++ b/pkg/getter/git/url.go @@ -0,0 +1,66 @@ +package git + +import ( + "fmt" + "net/url" + "path" + "strings" +) + +type gitURL struct { + Repo string + Ref string + Path string +} + +func (u *gitURL) Dir() *gitURL { + n := *u + n.Path = path.Dir(n.Path) + return &n +} + +func (u *gitURL) JoinPath(p ...string) *gitURL { + n := *u + n.Path = path.Join(append([]string{n.Path}, p...)...) + return &n +} + +func (u *gitURL) String() string { + ref := "" + if u.Ref != "" { + ref = fmt.Sprintf("?ref=%s", u.Ref) + } + path := u.Path + if path != "" { + path = fmt.Sprintf("@%s", path) + } + return fmt.Sprintf("%s%s%s", u.Repo, path, ref) +} + +func parseURL(s string) (*gitURL, error) { + u, err := url.Parse(s) + if err != nil { + return nil, err + } + if !strings.HasPrefix(u.Scheme, "git+") { + return nil, fmt.Errorf("unsupported git url scheme %q", u.Scheme) + } + s = s[4:] + queryStartPos := strings.LastIndex(s, "?") + if queryStartPos < 0 { + queryStartPos = len(s) + } + repoEndPosition := queryStartPos + pathStartPos := strings.LastIndex(s, "@") + if pathStartPos < 0 { + pathStartPos = queryStartPos + } else { + repoEndPosition = pathStartPos + pathStartPos++ + } + return &gitURL{ + Repo: s[:repoEndPosition], + Ref: u.Query().Get("ref"), + Path: s[pathStartPos:queryStartPos], + }, nil +} diff --git a/pkg/getter/git/url_test.go b/pkg/getter/git/url_test.go new file mode 100644 index 0000000..c4c857e --- /dev/null +++ b/pkg/getter/git/url_test.go @@ -0,0 +1,83 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGitURL(t *testing.T) { + for _, tc := range []struct { + name string + input string + expect gitURL + }{ + { + name: "repo", + input: "git+https://example.org/git/org/user", + expect: gitURL{ + Repo: "https://example.org/git/org/user", + }, + }, + { + name: "repo path", + input: "git+https://example.org/git/org/user@some/path", + expect: gitURL{ + Repo: "https://example.org/git/org/user", + Path: "some/path", + }, + }, + { + name: "repo ref", + input: "git+https://example.org/git/org/user?ref=v1.2.3", + expect: gitURL{ + Repo: "https://example.org/git/org/user", + Ref: "v1.2.3", + }, + }, + { + name: "repo path ref", + input: "git+https://example.org/git/org/user@some/path?ref=v1.2.3", + expect: gitURL{ + Repo: "https://example.org/git/org/user", + Ref: "v1.2.3", + Path: "some/path", + }, + }, + { + name: "abs path", + input: "git+https://example.org/git/org/user@/some/path?ref=v1.2.3", + expect: gitURL{ + Repo: "https://example.org/git/org/user", + Ref: "v1.2.3", + Path: "/some/path", + }, + }, + { + name: "ssh", + input: "git+ssh://git@example.org/git/org/user@some/path?ref=v1.2.3", + expect: gitURL{ + Repo: "ssh://git@example.org/git/org/user", + Ref: "v1.2.3", + Path: "some/path", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + actual, err := parseURL(tc.input) + require.NoError(t, err) + require.Equal(t, &tc.expect, actual, "parseURL()") + require.Equal(t, tc.input[4:], actual.String(), "url.String()") + }) + } +} +func TestGitURLJoinPath(t *testing.T) { + u := gitURL{ + Repo: "ssh://example.org/repo", + Ref: "v1.2.3", + Path: "some/path", + } + n := u.JoinPath("other", "path") + require.Equal(t, "some/path/other/path", n.Path) + require.Equal(t, "some/path", u.Path) +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index c92278e..56a4901 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -23,5 +23,7 @@ func NewHelm() *Helm { // Fallback for old helm env var settings.RepositoryConfig = filepath.Join(helmHome, "repository", "repositories.yaml") } - return &Helm{Settings: *settings, Getters: getter.All(settings)} + h := &Helm{Settings: *settings} + h.Getters = getters(settings, &h.TrustAnyRepository) + return h } diff --git a/pkg/helm/load.go b/pkg/helm/load.go index e9d7f6a..2cffe9e 100644 --- a/pkg/helm/load.go +++ b/pkg/helm/load.go @@ -28,7 +28,7 @@ func (h *Helm) loadChart(ctx context.Context, cfg *config.ChartConfig) (*chart.C fileExists := err == nil if cfg.Repository == "" { if fileExists { - return h.buildAndLoadLocalChart(ctx, cfg) + return buildAndLoadLocalChart(ctx, cfg, h.TrustAnyRepository, h.Settings, h.Getters) } else if l := strings.Split(cfg.Chart, "/"); len(l) == 2 && l[0] != "" && l[1] != "" && l[0] != ".." && l[0] != "." { cfg.Repository = "@" + l[0] cfg.Chart = l[1] @@ -61,7 +61,7 @@ func (h *Helm) loadRemoteChart(ctx context.Context, cfg *config.ChartConfig) (*c return loader.Load(chartPath) } -func (h *Helm) buildAndLoadLocalChart(ctx context.Context, cfg *config.ChartConfig) (*chart.Chart, error) { +func buildAndLoadLocalChart(ctx context.Context, cfg *config.ChartConfig, trustAnyRepo *bool, settings cli.EnvSettings, getters getter.Providers) (*chart.Chart, error) { chartPath := absPath(cfg.Chart, cfg.BaseDir) chartRequested, err := loader.Load(chartPath) if err != nil { @@ -76,7 +76,7 @@ func (h *Helm) buildAndLoadLocalChart(ctx context.Context, cfg *config.ChartConf } // Create (temporary) repository configuration that includes all dependencies - repos, err := reposForDependencies(dependencies, h.TrustAnyRepository, &h.Settings, h.Getters) + repos, err := reposForDependencies(dependencies, trustAnyRepo, &settings, getters) if err != nil { return nil, errors.Wrap(err, "init temp repositories.yaml") } @@ -86,8 +86,8 @@ func (h *Helm) buildAndLoadLocalChart(ctx context.Context, cfg *config.ChartConf return nil, err } defer repos.Close() - settings := h.Settings - settings.RepositoryConfig = repos.FilePath() + tmpSettings := settings + tmpSettings.RepositoryConfig = repos.FilePath() // Download/update repo indices if needsRepoIndexUpdate { @@ -100,7 +100,7 @@ func (h *Helm) buildAndLoadLocalChart(ctx context.Context, cfg *config.ChartConf } // Build local charts recursively - needsReload, err := buildLocalCharts(ctx, localCharts, &cfg.LoaderConfig, repos, &settings, h.Getters) + needsReload, err := buildLocalCharts(ctx, localCharts, &cfg.LoaderConfig, repos, &tmpSettings, getters) if err != nil { return nil, errors.Wrap(err, "build/fetch dependencies") } @@ -162,7 +162,7 @@ func collectCharts(chartRequested *chart.Chart, chartPath string, cfg *config.Ch if needsUpdate { needsRepoIndexUpdate = true } - } else if strings.HasPrefix(dep.Repository, "https://") || strings.HasPrefix(dep.Repository, "http://") { + } else { *deps = append(*deps, dep) if chartRequested.Lock == nil { // Update repo index when remote dependencies present but no lock file diff --git a/pkg/helm/locate.go b/pkg/helm/locate.go index 516b401..2beaad3 100644 --- a/pkg/helm/locate.go +++ b/pkg/helm/locate.go @@ -42,8 +42,7 @@ func locateChart(ctx context.Context, cfg *config.LoaderConfig, repos repository return "", errors.Wrap(err, "failed to make chart URL absolute") } - chartCacheDir := filepath.Join(settings.RepositoryCache, "khelm") - cacheFile, err := cacheFilePath(chartURL, cv, chartCacheDir) + cacheFile, err := cacheFilePath(chartURL, cv, settings.RepositoryCache) if err != nil { return "", errors.Wrap(err, "derive chart cache file") } @@ -127,24 +126,27 @@ func locateChart(ctx context.Context, cfg *config.LoaderConfig, repos repository } } -func cacheFilePath(chartURL string, cv *repo.ChartVersion, cacheDir string) (string, error) { +func cacheFilePath(chartURL string, cv *repo.ChartVersion, cacheRootDir string) (string, error) { + cacheDir := filepath.Join(cacheRootDir, "khelm") u, err := url.Parse(chartURL) if err != nil { - return "", errors.Wrapf(err, "parse chart URL %q", chartURL) + return "", errors.Wrapf(err, "parse chart url %q", chartURL) } - if u.Path == "" { - return "", errors.Errorf("parse chart URL %s: empty path in URL", chartURL) + p := u.Path + if p == "" { + return "", errors.Errorf("invalid chart url %s: empty path", chartURL) } - // Try reading file from cache - path := filepath.Clean(filepath.FromSlash(u.Path)) + path := filepath.Clean(filepath.FromSlash(p)) if strings.Contains(path, "..") { return "", errors.Errorf("get %s: path %q points outside the cache dir", chartURL, path) } digest := "none" if len(cv.Digest) < 16 { // not all the helm repository implementations populate the digest field (e.g. Nexus 3) - log.Printf("WARNING: repo index entry for chart %q does not specify a digest", cv.Name) + if !strings.HasPrefix(chartURL, "git+") { + log.Printf("WARNING: repo index entry for chart %q does not specify a digest", cv.Name) + } } else { digest = cv.Digest[:16] } diff --git a/pkg/helm/package.go b/pkg/helm/package.go new file mode 100644 index 0000000..32596e0 --- /dev/null +++ b/pkg/helm/package.go @@ -0,0 +1,27 @@ +package helm + +import ( + "context" + + "github.com/mgoltzsche/khelm/v2/pkg/config" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" +) + +func packageHelmChart(ctx context.Context, cfg *config.ChartConfig, destDir string, trustAnyRepo *bool, settings cli.EnvSettings, getters getter.Providers) (string, error) { + // TODO: add unit test (there is an e2e/cli test for this though) + _, err := buildAndLoadLocalChart(ctx, cfg, trustAnyRepo, settings, getters) + if err != nil { + return "", err + } + // See https://github.com/helm/helm/blob/v3.10.0/cmd/helm/package.go#L104 + client := action.NewPackage() + client.Destination = destDir + chartPath := absPath(cfg.Chart, cfg.BaseDir) + tgzFile, err := client.Run(chartPath, map[string]interface{}{}) + if err != nil { + return "", err + } + return tgzFile, nil +} diff --git a/pkg/helm/providers.go b/pkg/helm/providers.go new file mode 100644 index 0000000..64757b6 --- /dev/null +++ b/pkg/helm/providers.go @@ -0,0 +1,26 @@ +package helm + +import ( + "context" + + "github.com/mgoltzsche/khelm/v2/pkg/config" + "github.com/mgoltzsche/khelm/v2/pkg/getter/git" + "helm.sh/helm/v3/pkg/cli" + helmgetter "helm.sh/helm/v3/pkg/getter" +) + +func getters(settings *cli.EnvSettings, trustAnyRepo **bool) helmgetter.Providers { + g := helmgetter.All(settings) + g = append(g, helmgetter.Provider{ + Schemes: []string{"git+https", "git+ssh"}, + New: git.New(settings, func(ctx context.Context, chartDir, repoDir string) (string, error) { + return packageHelmChart(ctx, &config.ChartConfig{ + LoaderConfig: config.LoaderConfig{ + Chart: chartDir, + }, + BaseDir: repoDir, + }, chartDir, *trustAnyRepo, *settings, g) + }), + }) + return g +} diff --git a/pkg/helm/render_test.go b/pkg/helm/render_test.go index f363261..d28b658 100644 --- a/pkg/helm/render_test.go +++ b/pkg/helm/render_test.go @@ -69,6 +69,7 @@ func TestRender(t *testing.T) { "chart-hooks-test", }}, {"chart-hooks-disabled", "example/chart-hooks-disabled/generator.yaml", []string{"default"}, " key: myvalue", []string{"chart-hooks-disabled-myconfig"}}, + {"git-getter", "example/git-getter/generator.yaml", []string{"cert-manager", "kube-system"}, "ca-sync", nil}, } { t.Run(c.name, func(t *testing.T) { for _, cached := range []string{"", "cached "} {