diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1fcef041 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +--- +version: 2 +updates: + + - package-ecosystem: gomod + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + gomod: + update-types: + - "patch" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + actions: + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..af5bb457 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,79 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: ./go.mod + check-latest: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 + with: + install-only: true + + - name: Install cosign + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + + - name: Install syft + uses: anchore/sbom-action/download-syft@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7 + + - uses: chainguard-dev/actions/goimports@main + + - name: Run Mage + uses: magefile/mage-action@6a5dcb5fe61f43d7c08a98bc3cf9bc63c308c08e # v3.0.0 + with: + args: -v ci + + check-docs: + name: check-docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: ./go.mod + check-latest: true + + - name: generate docs + run: | + go build -o cr-bin ./cr/main.go + ./cr-bin doc-gen + git_status="$(git status --porcelain)" + if [[ ${git_status} ]]; then + echo -e 'Documentation is outdated. Please update the docs\n' + echo "${git_status}" + exit 1 + fi + + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: ./go.mod + check-latest: true + - name: golangci-lint + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + with: + version: v1.59 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..3f462d30 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,69 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: write + packages: write + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: ./go.mod + check-latest: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 + with: + install-only: true + + - name: Install cosign + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + + - name: Install syft + uses: anchore/sbom-action/download-syft@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7 + + - name: Cache + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - uses: chainguard-dev/actions/goimports@main + + - name: Login to registry + if: github.repository == 'helm/chart-releaser' + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: quay.io + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Run Mage + uses: magefile/mage-action@6a5dcb5fe61f43d7c08a98bc3cf9bc63c308c08e # v3.0.0 + with: + args: -v release + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1837e566 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +chart-releaser +.idea +/dist +/vendor +.vscode +/cr-bin diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..fe1bdf82 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,30 @@ +linters: + enable: + - asciicheck + - errcheck + - errorlint + - gofmt + - goimports + - gosec + - gocritic + - importas + - prealloc + - revive + - misspell + - stylecheck + - unconvert + - unused + - whitespace +output: + uniq-by-line: false +issues: + exclude-rules: + - path: _test\.go + linters: + - errcheck + - gosec + max-issues-per-linter: 0 + max-same-issues: 0 +run: + issues-exit-code: 1 + timeout: 10m diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..209c6d15 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,212 @@ +--- +version: 2 +project_name: chart-releaser + +env: + - COSIGN_YES=true + +before: + hooks: + - go mod download + +sboms: + - artifacts: archive + +builds: + - main: cr/main.go + binary: cr + env: + - CGO_ENABLED=0 + goarch: + - amd64 + - arm + - arm64 + - s390x + - ppc64le + goarm: + - 7 + goos: + - linux + - darwin + - windows + ignore: + - goos: windows + goarch: arm + - goos: windows + goarch: s390x + - goos: windows + goarch: ppc64le + ldflags: + - >- + -X github.com/helm/chart-releaser/cr/cmd.Version={{ .Tag }} + -X github.com/helm/chart-releaser/cr/cmd.GitCommit={{ .Commit }} + -X github.com/helm/chart-releaser/cr/cmd.BuildDate={{ .Date }} + +archives: + - format_overrides: + - goos: windows + format: zip + files: + - LICENSE + - README.md + +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" + +dockers: + - goos: linux + goarch: amd64 + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" + dockerfile: Dockerfile + use: buildx + image_templates: + - quay.io/helmpack/chart-releaser:{{ .Tag }}-amd64 + - quay.io/helmpack/chart-releaser:latest-amd64 + - ghcr.io/helm/chart-releaser:{{ .Tag }}-amd64 + - ghcr.io/helm/chart-releaser:latest-amd64 + build_flag_templates: + - "--platform=linux/amd64" + - --label=org.label-schema.schema-version=1.0 + - --label=org.label-schema.version={{ .Version }} + - --label=org.label-schema.name={{ .ProjectName }} + - --label=org.label-schema.build-date={{ .Date }} + - --label=org.label-schema.description='cr - The chart release tool' + - --label=org.label-schema.vendor=Helm + + - goos: linux + goarch: arm64 + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" + dockerfile: Dockerfile + use: buildx + image_templates: + - quay.io/helmpack/chart-releaser:{{ .Tag }}-arm64 + - quay.io/helmpack/chart-releaser:latest-arm64 + - ghcr.io/helm/chart-releaser:{{ .Tag }}-arm64 + - ghcr.io/helm/chart-releaser:latest-arm64 + build_flag_templates: + - "--platform=linux/arm64" + - --label=org.label-schema.schema-version=1.0 + - --label=org.label-schema.version={{ .Version }} + - --label=org.label-schema.name={{ .ProjectName }} + - --label=org.label-schema.build-date={{ .Date }} + - --label=org.label-schema.description='cr - The chart release tool' + - --label=org.label-schema.vendor=Helm + + - goos: linux + goarch: arm + goarm: 7 + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" + dockerfile: Dockerfile + use: buildx + image_templates: + - quay.io/helmpack/chart-releaser:{{ .Tag }}-armv7 + - quay.io/helmpack/chart-releaser:latest-armv7 + - ghcr.io/helm/chart-releaser:{{ .Tag }}-armv7 + - ghcr.io/helm/chart-releaser:latest-armv7 + build_flag_templates: + - "--platform=linux/arm/v7" + - --label=org.label-schema.schema-version=1.0 + - --label=org.label-schema.version={{ .Version }} + - --label=org.label-schema.name={{ .ProjectName }} + - --label=org.label-schema.build-date={{ .Date }} + - --label=org.label-schema.description='cr - The chart release tool' + - --label=org.label-schema.vendor=Helm + + - goos: linux + goarch: s390x + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" + dockerfile: Dockerfile + use: buildx + image_templates: + - quay.io/helmpack/chart-releaser:{{ .Tag }}-s390x + - quay.io/helmpack/chart-releaser:latest-s390x + - ghcr.io/helm/chart-releaser:{{ .Tag }}-s390x + - ghcr.io/helm/chart-releaser:latest-s390x + build_flag_templates: + - "--platform=linux/s390x" + - --label=org.label-schema.schema-version=1.0 + - --label=org.label-schema.version={{ .Version }} + - --label=org.label-schema.name={{ .ProjectName }} + - --label=org.label-schema.build-date={{ .Date }} + - --label=org.label-schema.description='cr - The chart release tool' + - --label=org.label-schema.vendor=Helm + + - goos: linux + goarch: ppc64le + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" + dockerfile: Dockerfile + use: buildx + image_templates: + - quay.io/helmpack/chart-releaser:{{ .Tag }}-ppc64le + - quay.io/helmpack/chart-releaser:latest-ppc64le + - ghcr.io/helm/chart-releaser:{{ .Tag }}-ppc64le + - ghcr.io/helm/chart-releaser:latest-ppc64le + build_flag_templates: + - "--platform=linux/ppc64le" + - --label=org.label-schema.schema-version=1.0 + - --label=org.label-schema.version={{ .Version }} + - --label=org.label-schema.name={{ .ProjectName }} + - --label=org.label-schema.build-date={{ .Date }} + - --label=org.label-schema.description='cr - The chart release tool' + - --label=org.label-schema.vendor=Helm + +docker_manifests: + - name_template: quay.io/helmpack/chart-releaser:{{ .Tag }} + image_templates: + - quay.io/helmpack/chart-releaser:{{ .Tag }}-amd64 + - quay.io/helmpack/chart-releaser:{{ .Tag }}-arm64 + - quay.io/helmpack/chart-releaser:{{ .Tag }}-armv7 + - quay.io/helmpack/chart-releaser:{{ .Tag }}-s390x + - quay.io/helmpack/chart-releaser:{{ .Tag }}-ppc64le + - name_template: ghcr.io/helm/chart-releaser:{{ .Tag }} + image_templates: + - ghcr.io/helm/chart-releaser:{{ .Tag }}-amd64 + - ghcr.io/helm/chart-releaser:{{ .Tag }}-arm64 + - ghcr.io/helm/chart-releaser:{{ .Tag }}-armv7 + - ghcr.io/helm/chart-releaser:{{ .Tag }}-s390x + - ghcr.io/helm/chart-releaser:{{ .Tag }}-ppc64le + - name_template: quay.io/helmpack/chart-releaser:latest + image_templates: + - quay.io/helmpack/chart-releaser:latest-amd64 + - quay.io/helmpack/chart-releaser:latest-arm64 + - quay.io/helmpack/chart-releaser:latest-armv7 + - quay.io/helmpack/chart-releaser:latest-s390x + - quay.io/helmpack/chart-releaser:latest-ppc64le + - name_template: ghcr.io/helm/chart-releaser:latest + image_templates: + - ghcr.io/helm/chart-releaser:latest-amd64 + - ghcr.io/helm/chart-releaser:latest-arm64 + - ghcr.io/helm/chart-releaser:latest-armv7 + - ghcr.io/helm/chart-releaser:latest-s390x + - ghcr.io/helm/chart-releaser:latest-ppc64le + +signs: + - id: all + signature: "${artifact}.sig" + certificate: "${artifact}.pem" + cmd: cosign + args: ["sign-blob", "--output-signature", "${artifact}.sig", "--output-certificate", "${artifact}.pem", "${artifact}"] + artifacts: all + +docker_signs: + - id: images + cmd: cosign + args: ["sign", "${artifact}"] + artifacts: manifests + +brews: + - repository: + owner: helm + name: homebrew-tap + commit_author: + name: helm-bot + email: helm-bot@users.noreply.github.com + directory: Formula + homepage: "{{ .GitURL }}" + description: Hosting Helm Charts via GitHub Pages and Releases + install: | + bin.install "cr" + test: | + system "#{bin}/cr version" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7fd22db4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine:3.18 + +RUN apk --no-cache add ca-certificates git + +COPY cr /usr/local/bin/cr + +# Ensure that the binary is available on path and is executable +RUN cr --help + +ENTRYPOINT [ "/usr/local/bin/cr" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..4b34a8b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The Helm Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..779fb752 --- /dev/null +++ b/README.md @@ -0,0 +1,297 @@ +# Chart Releaser + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +![CI](https://github.com/helm/chart-releaser/workflows/CI/badge.svg?branch=main&event=push) + +**Helps Turn GitHub Repositories into Helm Chart Repositories** + +`cr` is a tool designed to help GitHub repos self-host their own chart repos by adding Helm chart artifacts to GitHub Releases named for the chart version and then creating an `index.yaml` file for those releases that can be hosted on GitHub Pages (or elsewhere!). + +## Installation + +### Binaries (recommended) + +Download your preferred asset from the [releases page](https://github.com/helm/chart-releaser/releases) and install manually. + +### Homebrew + +```console +$ brew tap helm/tap +$ brew install chart-releaser +``` + +### Go get (for contributing) + +```console +// clone repo to some directory outside GOPATH + +$ git clone https://github.com/helm/chart-releaser +$ cd chart-releaser +$ go mod download +$ go install ./... +``` + +### Docker (for Continuous Integration) + +Docker images are pushed to the [helmpack/chart-releaser](https://quay.io/repository/helmpack/chart-releaser?tab=tags) Quay container registry. The Docker image is built on top of Alpine and its default entry-point is `cr`. See the [Dockerfile](./Dockerfile) for more details. + +## Common Usage + +Currently, `cr` can create GitHub Releases from a set of charts packaged up into a directory and create an `index.yaml` file for the chart repository from GitHub Releases. + +```console +$ cr --help +Create Helm chart repositories on GitHub Pages by uploading Chart packages +and Chart metadata to GitHub Releases and creating a suitable index file + +Usage: + cr [command] + +Available Commands: + completion generate the autocompletion script for the specified shell + help Help about any command + index Update Helm repo index.yaml for the given GitHub repo + package Package Helm charts + upload Upload Helm chart packages to GitHub Releases + version Print version information + +Flags: + --config string Config file (default is $HOME/.cr.yaml) + -h, --help help for cr + +Use "cr [command] --help" for more information about a command. +``` + +### Dealing with charts that have dependencies + +Unfortuntely the releaser-tool won't automatically add repositories for dependencies, and this needs to be added to your pipeline ([example](https://github.com/davidkarlsen/flyway-operator/blob/main/.github/workflows/chart-release.yaml#L31)), prior to running the releaser, like this: + +```yaml + - name: add repos + run: | + helm repo add bitnami https://charts.bitnami.com/bitnami + helm repo add bitnami-pre2022 https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + with: + charts_dir: config/helm-chart + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" +``` + + +### Create GitHub Releases from Helm Chart Packages + +Scans a path for Helm chart packages and creates releases in the specified GitHub repo uploading the packages. + +```console +$ cr upload --help +Upload Helm chart packages to GitHub Releases + +Usage: + cr upload [flags] + +Flags: + -c, --commit string Target commit for release + --generate-release-notes Whether to automatically generate the name and body for this release. See https://docs.github.com/en/rest/releases/releases + -b, --git-base-url string GitHub Base URL (only needed for private GitHub) (default "https://api.github.com/") + -r, --git-repo string GitHub repository + -u, --git-upload-url string GitHub Upload URL (only needed for private GitHub) (default "https://uploads.github.com/") + -h, --help help for upload + -o, --owner string GitHub username or organization + -p, --package-path string Path to directory with chart packages (default ".cr-release-packages") + --release-name-template string Go template for computing release names, using chart metadata (default "{{ .Name }}-{{ .Version }}") + --release-notes-file string Markdown file with chart release notes. If it is set to empty string, or the file is not found, the chart description will be used instead. The file is read from the chart package + --skip-existing Skip upload if release exists + -t, --token string GitHub Auth Token + --make-release-latest bool Mark the created GitHub release as 'latest' (default "true") + --packages-with-index Host the package files in the GitHub Pages branch + --prerelease Mark this release as 'Pre-release' (default: false) + +Global Flags: + --config string Config file (default is $HOME/.cr.yaml) +``` + +### Create the Repository Index from GitHub Releases + +Once uploaded you can create an `index.yaml` file that can be hosted on GitHub Pages (or elsewhere). + +```console +$ cr index --help +Update a Helm chart repository index.yaml file based on a the +given GitHub repository's releases. + +Usage: + cr index [flags] + +Flags: + -b, --git-base-url string GitHub Base URL (only needed for private GitHub) (default "https://api.github.com/") + -r, --git-repo string GitHub repository + -u, --git-upload-url string GitHub Upload URL (only needed for private GitHub) (default "https://uploads.github.com/") + -h, --help help for index + -i, --index-path string Path to index file (default ".cr-index/index.yaml") + -o, --owner string GitHub username or organization + -p, --package-path string Path to directory with chart packages (default ".cr-release-packages") + --pages-branch string The GitHub pages branch (default "gh-pages") + --pages-index-path string The GitHub pages index path (default "index.yaml") + --pr Create a pull request for index.yaml against the GitHub Pages branch (must not be set if --push is set) + --push Push index.yaml to the GitHub Pages branch (must not be set if --pr is set) + --release-name-template string Go template for computing release names, using chart metadata (default "{{ .Name }}-{{ .Version }}") + --remote string The Git remote used when creating a local worktree for the GitHub Pages branch (default "origin") + -t, --token string GitHub Auth Token (only needed for private repos) + --packages-with-index Host the package files in the GitHub Pages branch + +Global Flags: + --config string Config file (default is $HOME/.cr.yaml) +``` + +## Usage with a private repository + +When using this tool on a private repository, helm is unable to download the chart package files. When you give Helm your username and password it uses it to authenticate to the repository (the index file). The index file then tells Helm where to get the tarball. If the tarball is hosted in some other location (Github Releases in this case) then it would require a second authentication (which Helm does not support). The solution is to host the files in the same place as your index file and make the links relative paths so there is no need for the second authentication. + +[#123](https://github.com/helm/chart-releaser/pull/123) solve this by adding a `--packages-with-index` flag to the upload and index commands. + +### Prerequisites + +Have a Github token with the right permissions (SSO enabled for entreprise) and Github Pages configured. + +### Usage + +Here are the three commands you must run for a chart to end-up hosted in the root directory of your Github page and be accessible : + +```bash +cr package +``` + +```bash +cr upload --owner --git-repo --packages-with-index --token --push --skip-existing +``` + +Don't forget the `--skip-existing` flag in the upload command to avoid getting a `422 Validation Failed` error. + +```bash +cr index --owner --git-repo --packages-with-index --index-path . --token --push +``` + +### Example + +With a **testChart** helm chart in the root of your repository : + +```bash +cr package testChart/ +``` + +You will obtain the .tgz in the **./cr-release-pacakges** + +![repository](https://user-images.githubusercontent.com/116822264/227932604-50bf08d9-44bc-4170-b830-a2dccf1afdde.PNG) + +Do the two followng commands : + +```bash +cr upload --owner --git-repo --packages-with-index --token --push --skip-existing + +cr index --owner --git-repo --packages-with-index --index-path . --token --push +``` + +You should obtain a release of your **chart** as well as the .tgz in the root of your **github-pages** branch. + +![github pages](https://user-images.githubusercontent.com/116822264/230021850-8d713d5d-426b-41ac-9ff4-c7e19de40e32.PNG) + +With the `index.yaml` that references each chart and every different versions of those charts : + +![index](https://user-images.githubusercontent.com/116822264/230022176-ec57a8e7-ddb7-4318-a03d-032548421e21.PNG) + +## Configuration + +`cr` is a command-line application. +All command-line flags can also be set via environment variables or config file. +Environment variables must be prefixed with `CR_`. +Underscores must be used instead of hyphens. + +CLI flags, environment variables, and a config file can be mixed. +The following order of precedence applies: + +1. CLI flags +1. Environment variables +1. Config file + +### Examples + +The following example show various ways of configuring the same thing: + +#### CLI + +```bash +cr upload --owner myaccount --git-repo helm-charts --package-path .deploy --token 123456789 +``` + +#### Environment Variables + +```bash +export CR_OWNER=myaccount +export CR_GIT_REPO=helm-charts +export CR_PACKAGE_PATH=.deploy +export CR_TOKEN="123456789" +export CR_GIT_BASE_URL="https://api.github.com/" +export CR_GIT_UPLOAD_URL="https://uploads.github.com/" +export CR_SKIP_EXISTING=true + +cr upload +``` + +#### Config File + +`config.yaml`: + +```yaml +owner: myaccount +git-repo: helm-charts +package-path: .deploy +token: 123456789 +git-base-url: https://api.github.com/ +git-upload-url: https://uploads.github.com/ +``` + +#### Config Usage + +```bash +cr upload --config config.yaml +``` + +`cr` supports any format [Viper](https://github.com/spf13/viper) can read, i.e. JSON, TOML, YAML, HCL, and Java properties files. + +Notice that if no config file is specified, `cr.yaml` (or any of the supported formats) is loaded from the current directory, `$HOME/.cr`, or `/etc/cr`, in that order, if found. + +#### Notes for Github Enterprise Users + +For Github Enterprise, `chart-releaser` users need to set `git-base-url` and `git-upload-url` correctly, but the correct values are not always obvious to endusers. + +By default they are often along these lines: + +```console +https://ghe.example.com/api/v3/ +https://ghe.example.com/api/uploads/ +``` + +If you are trying to figure out what your `upload_url` is try to use a curl command like this: +`curl -u username:token https://example.com/api/v3/repos/org/repo/releases` +and then look for `upload_url`. You need the part of the URL that appears before `repos/` in the path. + +## Common Error Messages + +During the upload, you can get the following error : + +```bash +422 Validation Failed [{Resource:Release Field:tag_name Code:already_exists Message:}] +``` + +You can solve it by adding the `--skip-existing` flag to your command. More details can be found in [#101](https://github.com/helm/chart-releaser/issues/101#issuecomment-766410614) and in [#111](https://github.com/helm/chart-releaser/pull/111) that solved this. + +## Known Bug + +Currently, if you set the upload URL incorrectly, let's say to something like `https://example.com/uploads/`, then `cr upload` will appear to work, but the release will not be complete. When everything is working there should be three assets in each release, but instead, there will only be two source code assets. The third asset is missing and is needed by Helm. This issue will become apparent when you run `cr index` and it always claims that nothing has changed, because it can't find the asset it expects for the release. + +It appears like the [go-github Do call](https://github.com/google/go-github/blob/master/github/github.go#L520) does not catch the fact that the upload URL is incorrect and passes back the expected error. If the asset upload fails, it would be better if the release was rolled back (deleted) and an appropriate log message is displayed to the user. + +The `cr index` command should also generate a warning when a release has no assets attached to it, to help people detect and troubleshoot this type of problem. diff --git a/code-of-conduct.md b/code-of-conduct.md new file mode 100644 index 00000000..91ccaf03 --- /dev/null +++ b/code-of-conduct.md @@ -0,0 +1,3 @@ +# Community Code of Conduct + +Helm follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/cr/cmd/docGen.go b/cr/cmd/docGen.go new file mode 100644 index 00000000..e9f4c15b --- /dev/null +++ b/cr/cmd/docGen.go @@ -0,0 +1,51 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +var generateDocsCmd = &cobra.Command{ + Use: "doc-gen", + Short: "Generate documentation", + Long: heredoc.Doc(` + Generate documentation for all commands + to the 'docs' directory.`), + Hidden: true, + RunE: generateDocs, +} + +func generateDocs(_ *cobra.Command, _ []string) error { + fmt.Println("Generating docs...") + + err := doc.GenMarkdownTree(rootCmd.Root(), "doc") + if err != nil { + return err + } + + fmt.Println("Done.") + return nil +} + +func init() { + rootCmd.AddCommand(generateDocsCmd) + + rootCmd.DisableAutoGenTag = true +} diff --git a/cr/cmd/index.go b/cr/cmd/index.go new file mode 100644 index 00000000..e62f05f7 --- /dev/null +++ b/cr/cmd/index.go @@ -0,0 +1,80 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + + "github.com/helm/chart-releaser/pkg/config" + "github.com/helm/chart-releaser/pkg/git" + "github.com/helm/chart-releaser/pkg/github" + "github.com/helm/chart-releaser/pkg/releaser" + "github.com/spf13/cobra" +) + +// indexCmd represents the index command +var indexCmd = &cobra.Command{ + Use: "index", + Short: "Update Helm repo index.yaml for the given GitHub repo", + Long: ` +Update a Helm chart repository index.yaml file based on a the +given GitHub repository's releases. + `, + RunE: func(cmd *cobra.Command, _ []string) error { + config, err := config.LoadConfiguration(cfgFile, cmd, getRequiredIndexArgs()) + if err != nil { + return err + } + + if len(config.ChartsRepo) > 0 { + fmt.Fprintf(os.Stderr, "ATTENTION: Flag --charts-repo is deprecated. It does not have any effect.\n"+ + "The index.yaml is read from the '%s' branch instead.\n"+ + "Loading index.yaml directly from the charts repository lead to problems as there is a delay between\n"+ + "pushing to the GitHub pages branch until things appear online.\n"+ + "The flag will be removed with the next major release.", config.PagesBranch) + } + + ghc := github.NewClient(config.Owner, config.GitRepo, config.Token, config.GitBaseURL, config.GitUploadURL) + releaser := releaser.NewReleaser(config, ghc, &git.Git{}) + _, err = releaser.UpdateIndexFile() + return err + }, +} + +func getRequiredIndexArgs() []string { + return []string{"owner", "git-repo"} +} + +func init() { + rootCmd.AddCommand(indexCmd) + flags := indexCmd.Flags() + flags.StringP("owner", "o", "", "GitHub username or organization") + flags.StringP("git-repo", "r", "", "GitHub repository") + flags.StringP("charts-repo", "c", "", "The URL to the charts repository") + _ = flags.MarkHidden("charts-repo") + flags.StringP("index-path", "i", ".cr-index/index.yaml", "Path to index file") + flags.StringP("package-path", "p", ".cr-release-packages", "Path to directory with chart packages") + flags.StringP("token", "t", "", "GitHub Auth Token (only needed for private repos)") + flags.StringP("git-base-url", "b", "https://api.github.com/", "GitHub Base URL (only needed for private GitHub)") + flags.StringP("git-upload-url", "u", "https://uploads.github.com/", "GitHub Upload URL (only needed for private GitHub)") + flags.String("pages-branch", "gh-pages", "The GitHub pages branch") + flags.String("pages-index-path", "index.yaml", "The GitHub pages index path") + flags.String("remote", "origin", "The Git remote used when creating a local worktree for the GitHub Pages branch") + flags.Bool("push", false, "Push index.yaml to the GitHub Pages branch (must not be set if --pr is set)") + flags.Bool("pr", false, "Create a pull request for index.yaml against the GitHub Pages branch (must not be set if --push is set)") + flags.String("release-name-template", "{{ .Name }}-{{ .Version }}", "Go template for computing release names, using chart metadata") + flags.Bool("packages-with-index", false, "Host the package files in the GitHub Pages branch") +} diff --git a/cr/cmd/package.go b/cr/cmd/package.go new file mode 100644 index 00000000..45373e0e --- /dev/null +++ b/cr/cmd/package.go @@ -0,0 +1,61 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/helm/chart-releaser/pkg/config" + "github.com/helm/chart-releaser/pkg/packager" + "github.com/spf13/cobra" +) + +// packageCmd represents the package command +var packageCmd = &cobra.Command{ + Use: "package [CHART_PATH] [...]", + Short: "Package Helm charts", + Long: `This command packages a chart into a versioned chart archive file. If a path +is given, this will look at that path for a chart (which must contain a +Chart.yaml file) and then package that directory. + + +If you wish to use advanced packaging options such as creating signed +packages or updating chart dependencies please use "helm package" instead.`, + RunE: func(cmd *cobra.Command, args []string) error { + var err error + if len(args) == 0 { + args = append(args, ".") + } + config, err := config.LoadConfiguration(cfgFile, cmd, getRequiredPackageArgs()) + if err != nil { + return err + } + + p := packager.NewPackager(config, args) + return p.CreatePackages() + + }, +} + +func getRequiredPackageArgs() []string { + return []string{"package-path"} +} + +func init() { + rootCmd.AddCommand(packageCmd) + packageCmd.Flags().StringP("package-path", "p", ".cr-release-packages", "Path to directory with chart packages") + packageCmd.Flags().Bool("sign", false, "Use a PGP private key to sign this package") + packageCmd.Flags().String("key", "", "Name of the key to use when signing") + packageCmd.Flags().String("keyring", "~/.gnupg/pubring.gpg", "Location of a public keyring") + packageCmd.Flags().String("passphrase-file", "", "Location of a file which contains the passphrase for the signing key. Use '-' in order to read from stdin") +} diff --git a/cr/cmd/root.go b/cr/cmd/root.go new file mode 100644 index 00000000..904e16c0 --- /dev/null +++ b/cr/cmd/root.go @@ -0,0 +1,42 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "cr", + Short: "Helm Chart Repos on Github Pages", + Long: ` +Create Helm chart repositories on GitHub Pages by uploading Chart packages +and Chart metadata to GitHub Releases and creating a suitable index file +`} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Config file (default is $HOME/.cr.yaml)") +} diff --git a/cr/cmd/upload.go b/cr/cmd/upload.go new file mode 100644 index 00000000..cd436657 --- /dev/null +++ b/cr/cmd/upload.go @@ -0,0 +1,66 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/helm/chart-releaser/pkg/config" + "github.com/helm/chart-releaser/pkg/git" + "github.com/helm/chart-releaser/pkg/github" + "github.com/helm/chart-releaser/pkg/releaser" + "github.com/spf13/cobra" +) + +// uploadCmd represents the upload command +var uploadCmd = &cobra.Command{ + Use: "upload", + Short: "Upload Helm chart packages to GitHub Releases", + Long: `Upload Helm chart packages to GitHub Releases`, + RunE: func(cmd *cobra.Command, _ []string) error { + config, err := config.LoadConfiguration(cfgFile, cmd, getRequiredUploadArgs()) + if err != nil { + return err + } + ghc := github.NewClient(config.Owner, config.GitRepo, config.Token, config.GitBaseURL, config.GitUploadURL) + releaser := releaser.NewReleaser(config, ghc, &git.Git{}) + return releaser.CreateReleases() + }, +} + +func getRequiredUploadArgs() []string { + return []string{"owner", "git-repo", "token"} +} + +func init() { + rootCmd.AddCommand(uploadCmd) + uploadCmd.Flags().StringP("owner", "o", "", "GitHub username or organization") + uploadCmd.Flags().StringP("git-repo", "r", "", "GitHub repository") + uploadCmd.Flags().StringP("package-path", "p", ".cr-release-packages", "Path to directory with chart packages") + uploadCmd.Flags().StringP("token", "t", "", "GitHub Auth Token") + uploadCmd.Flags().StringP("git-base-url", "b", "https://api.github.com/", "GitHub Base URL (only needed for private GitHub)") + uploadCmd.Flags().StringP("git-upload-url", "u", "https://uploads.github.com/", "GitHub Upload URL (only needed for private GitHub)") + uploadCmd.Flags().StringP("commit", "c", "", "Target commit for release") + uploadCmd.Flags().Bool("skip-existing", false, "Skip upload if release exists") + uploadCmd.Flags().String("release-name-template", "{{ .Name }}-{{ .Version }}", "Go template for computing release names, using chart metadata") + uploadCmd.Flags().String("release-notes-file", "", "Markdown file with chart release notes. "+ + "If it is set to empty string, or the file is not found, the chart description will be used instead. The file is read from the chart package") + uploadCmd.Flags().Bool("generate-release-notes", false, "Whether to automatically generate the name and body for this release. See https://docs.github.com/en/rest/releases/releases") + uploadCmd.Flags().Bool("make-release-latest", true, "Mark the created GitHub release as 'latest'") + uploadCmd.Flags().String("pages-branch", "gh-pages", "The GitHub pages branch") + uploadCmd.Flags().String("remote", "origin", "The Git remote used when creating a local worktree for the GitHub Pages branch") + uploadCmd.Flags().Bool("push", false, "Push the chart package to the GitHub Pages branch (must not be set if --pr is set)") + uploadCmd.Flags().Bool("pr", false, "Create a pull request for the chart package against the GitHub Pages branch (must not be set if --push is set)") + uploadCmd.Flags().Bool("packages-with-index", false, "Host the package files in the GitHub Pages branch") + uploadCmd.Flags().Bool("prerelease", false, "Mark this as 'Pre-release' (default: false)") +} diff --git a/cr/cmd/version.go b/cr/cmd/version.go new file mode 100644 index 00000000..b9851be8 --- /dev/null +++ b/cr/cmd/version.go @@ -0,0 +1,112 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "fmt" + "runtime" + "strings" + "text/tabwriter" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + // GitCommit is updated with the Git tag by the Goreleaser build + GitCommit = "unknown" + // BuildDate is updated with the current ISO timestamp by the Goreleaser build + BuildDate = "unknown" + // Version is updated with the latest tag by the Goreleaser build + Version = "unreleased" + + outputJSON bool +) + +type Info struct { + Version string + GitCommit string + BuildDate string + GoVersion string + Compiler string + Platform string + License string +} + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version information", + RunE: func(_ *cobra.Command, _ []string) error { + v := GetVersionInfo() + res := v.String() + if outputJSON { + j, err := v.JSONString() + if err != nil { + return errors.Wrap(err, "unable to generate JSON from version info") + } + res = j + } + + fmt.Println(res) + return nil + }, +} + +func init() { + versionCmd.Flags().BoolVar(&outputJSON, "json", false, + "print JSON instead of text") + + rootCmd.AddCommand(versionCmd) +} + +func GetVersionInfo() Info { + return Info{ + Version: Version, + GitCommit: GitCommit, + BuildDate: BuildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + License: "Apache 2.0", + } +} + +// String returns the string representation of the version info +func (i *Info) String() string { + b := strings.Builder{} + w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0) + + fmt.Fprintf(w, "GitVersion:\t%s\n", i.Version) + fmt.Fprintf(w, "GitCommit:\t%s\n", i.GitCommit) + fmt.Fprintf(w, "BuildDate:\t%s\n", i.BuildDate) + fmt.Fprintf(w, "GoVersion:\t%s\n", i.GoVersion) + fmt.Fprintf(w, "Compiler:\t%s\n", i.Compiler) + fmt.Fprintf(w, "Platform:\t%s\n", i.Platform) + fmt.Fprintf(w, "License:\t%s\n", i.License) + + w.Flush() + return b.String() +} + +// JSONString returns the JSON representation of the version info +func (i *Info) JSONString() (string, error) { + b, err := json.MarshalIndent(i, "", " ") + if err != nil { + return "", err + } + + return string(b), nil +} diff --git a/cr/main.go b/cr/main.go new file mode 100644 index 00000000..b98b09b5 --- /dev/null +++ b/cr/main.go @@ -0,0 +1,21 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import "github.com/helm/chart-releaser/cr/cmd" + +func main() { + cmd.Execute() +} diff --git a/doc/cr.md b/doc/cr.md new file mode 100644 index 00000000..84f316ee --- /dev/null +++ b/doc/cr.md @@ -0,0 +1,26 @@ +## cr + +Helm Chart Repos on Github Pages + +### Synopsis + + +Create Helm chart repositories on GitHub Pages by uploading Chart packages +and Chart metadata to GitHub Releases and creating a suitable index file + + +### Options + +``` + --config string Config file (default is $HOME/.cr.yaml) + -h, --help help for cr +``` + +### SEE ALSO + +* [cr completion](cr_completion.md) - Generate the autocompletion script for the specified shell +* [cr index](cr_index.md) - Update Helm repo index.yaml for the given GitHub repo +* [cr package](cr_package.md) - Package Helm charts +* [cr upload](cr_upload.md) - Upload Helm chart packages to GitHub Releases +* [cr version](cr_version.md) - Print version information + diff --git a/doc/cr_completion.md b/doc/cr_completion.md new file mode 100644 index 00000000..2a33e06c --- /dev/null +++ b/doc/cr_completion.md @@ -0,0 +1,30 @@ +## cr completion + +Generate the autocompletion script for the specified shell + +### Synopsis + +Generate the autocompletion script for cr for the specified shell. +See each sub-command's help for details on how to use the generated script. + + +### Options + +``` + -h, --help help for completion +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr](cr.md) - Helm Chart Repos on Github Pages +* [cr completion bash](cr_completion_bash.md) - Generate the autocompletion script for bash +* [cr completion fish](cr_completion_fish.md) - Generate the autocompletion script for fish +* [cr completion powershell](cr_completion_powershell.md) - Generate the autocompletion script for powershell +* [cr completion zsh](cr_completion_zsh.md) - Generate the autocompletion script for zsh + diff --git a/doc/cr_completion_bash.md b/doc/cr_completion_bash.md new file mode 100644 index 00000000..13744a01 --- /dev/null +++ b/doc/cr_completion_bash.md @@ -0,0 +1,49 @@ +## cr completion bash + +Generate the autocompletion script for bash + +### Synopsis + +Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(cr completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + cr completion bash > /etc/bash_completion.d/cr + +#### macOS: + + cr completion bash > $(brew --prefix)/etc/bash_completion.d/cr + +You will need to start a new shell for this setup to take effect. + + +``` +cr completion bash +``` + +### Options + +``` + -h, --help help for bash + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr completion](cr_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/doc/cr_completion_fish.md b/doc/cr_completion_fish.md new file mode 100644 index 00000000..2828bf2b --- /dev/null +++ b/doc/cr_completion_fish.md @@ -0,0 +1,40 @@ +## cr completion fish + +Generate the autocompletion script for fish + +### Synopsis + +Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + cr completion fish | source + +To load completions for every new session, execute once: + + cr completion fish > ~/.config/fish/completions/cr.fish + +You will need to start a new shell for this setup to take effect. + + +``` +cr completion fish [flags] +``` + +### Options + +``` + -h, --help help for fish + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr completion](cr_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/doc/cr_completion_powershell.md b/doc/cr_completion_powershell.md new file mode 100644 index 00000000..4dda53e5 --- /dev/null +++ b/doc/cr_completion_powershell.md @@ -0,0 +1,37 @@ +## cr completion powershell + +Generate the autocompletion script for powershell + +### Synopsis + +Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + cr completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. + + +``` +cr completion powershell [flags] +``` + +### Options + +``` + -h, --help help for powershell + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr completion](cr_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/doc/cr_completion_zsh.md b/doc/cr_completion_zsh.md new file mode 100644 index 00000000..88347052 --- /dev/null +++ b/doc/cr_completion_zsh.md @@ -0,0 +1,51 @@ +## cr completion zsh + +Generate the autocompletion script for zsh + +### Synopsis + +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(cr completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + cr completion zsh > "${fpath[1]}/_cr" + +#### macOS: + + cr completion zsh > $(brew --prefix)/share/zsh/site-functions/_cr + +You will need to start a new shell for this setup to take effect. + + +``` +cr completion zsh [flags] +``` + +### Options + +``` + -h, --help help for zsh + --no-descriptions disable completion descriptions +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr completion](cr_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/doc/cr_index.md b/doc/cr_index.md new file mode 100644 index 00000000..68d00d9e --- /dev/null +++ b/doc/cr_index.md @@ -0,0 +1,45 @@ +## cr index + +Update Helm repo index.yaml for the given GitHub repo + +### Synopsis + + +Update a Helm chart repository index.yaml file based on a the +given GitHub repository's releases. + + +``` +cr index [flags] +``` + +### Options + +``` + -b, --git-base-url string GitHub Base URL (only needed for private GitHub) (default "https://api.github.com/") + -r, --git-repo string GitHub repository + -u, --git-upload-url string GitHub Upload URL (only needed for private GitHub) (default "https://uploads.github.com/") + -h, --help help for index + -i, --index-path string Path to index file (default ".cr-index/index.yaml") + -o, --owner string GitHub username or organization + -p, --package-path string Path to directory with chart packages (default ".cr-release-packages") + --packages-with-index Host the package files in the GitHub Pages branch + --pages-branch string The GitHub pages branch (default "gh-pages") + --pages-index-path string The GitHub pages index path (default "index.yaml") + --pr Create a pull request for index.yaml against the GitHub Pages branch (must not be set if --push is set) + --push Push index.yaml to the GitHub Pages branch (must not be set if --pr is set) + --release-name-template string Go template for computing release names, using chart metadata (default "{{ .Name }}-{{ .Version }}") + --remote string The Git remote used when creating a local worktree for the GitHub Pages branch (default "origin") + -t, --token string GitHub Auth Token (only needed for private repos) +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr](cr.md) - Helm Chart Repos on Github Pages + diff --git a/doc/cr_package.md b/doc/cr_package.md new file mode 100644 index 00000000..16726050 --- /dev/null +++ b/doc/cr_package.md @@ -0,0 +1,39 @@ +## cr package + +Package Helm charts + +### Synopsis + +This command packages a chart into a versioned chart archive file. If a path +is given, this will look at that path for a chart (which must contain a +Chart.yaml file) and then package that directory. + + +If you wish to use advanced packaging options such as creating signed +packages or updating chart dependencies please use "helm package" instead. + +``` +cr package [CHART_PATH] [...] [flags] +``` + +### Options + +``` + -h, --help help for package + --key string Name of the key to use when signing + --keyring string Location of a public keyring (default "~/.gnupg/pubring.gpg") + -p, --package-path string Path to directory with chart packages (default ".cr-release-packages") + --passphrase-file string Location of a file which contains the passphrase for the signing key. Use '-' in order to read from stdin + --sign Use a PGP private key to sign this package +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr](cr.md) - Helm Chart Repos on Github Pages + diff --git a/doc/cr_upload.md b/doc/cr_upload.md new file mode 100644 index 00000000..59b709a9 --- /dev/null +++ b/doc/cr_upload.md @@ -0,0 +1,46 @@ +## cr upload + +Upload Helm chart packages to GitHub Releases + +### Synopsis + +Upload Helm chart packages to GitHub Releases + +``` +cr upload [flags] +``` + +### Options + +``` + -c, --commit string Target commit for release + --generate-release-notes Whether to automatically generate the name and body for this release. See https://docs.github.com/en/rest/releases/releases + -b, --git-base-url string GitHub Base URL (only needed for private GitHub) (default "https://api.github.com/") + -r, --git-repo string GitHub repository + -u, --git-upload-url string GitHub Upload URL (only needed for private GitHub) (default "https://uploads.github.com/") + -h, --help help for upload + --make-release-latest Mark the created GitHub release as 'latest' (default true) + -o, --owner string GitHub username or organization + -p, --package-path string Path to directory with chart packages (default ".cr-release-packages") + --packages-with-index Host the package files in the GitHub Pages branch + --pages-branch string The GitHub pages branch (default "gh-pages") + --pr Create a pull request for the chart package against the GitHub Pages branch (must not be set if --push is set) + --prerelease Mark this as 'Pre-release' (default: false) + --push Push the chart package to the GitHub Pages branch (must not be set if --pr is set) + --release-name-template string Go template for computing release names, using chart metadata (default "{{ .Name }}-{{ .Version }}") + --release-notes-file string Markdown file with chart release notes. If it is set to empty string, or the file is not found, the chart description will be used instead. The file is read from the chart package + --remote string The Git remote used when creating a local worktree for the GitHub Pages branch (default "origin") + --skip-existing Skip upload if release exists + -t, --token string GitHub Auth Token +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr](cr.md) - Helm Chart Repos on Github Pages + diff --git a/doc/cr_version.md b/doc/cr_version.md new file mode 100644 index 00000000..e06261bc --- /dev/null +++ b/doc/cr_version.md @@ -0,0 +1,25 @@ +## cr version + +Print version information + +``` +cr version [flags] +``` + +### Options + +``` + -h, --help help for version + --json print JSON instead of text +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cr.yaml) +``` + +### SEE ALSO + +* [cr](cr.md) - Helm Chart Repos on Github Pages + diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..04c2915e --- /dev/null +++ b/go.mod @@ -0,0 +1,179 @@ +module github.com/helm/chart-releaser + +go 1.22.0 + +toolchain go1.22.4 + +require ( + github.com/MakeNowJust/heredoc v1.0.0 + github.com/Songmu/retry v0.1.0 + github.com/google/go-github/v56 v56.0.0 + github.com/magefile/mage v1.15.0 + github.com/mitchellh/go-homedir v1.1.0 + github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/oauth2 v0.24.0 + helm.sh/helm/v3 v3.16.2 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/Microsoft/hcsshim v0.12.4 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/errdefs v0.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cyphar/filepath-securejoin v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/cli v26.1.4+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v27.0.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch v5.9.0+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/spdystream v0.4.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rubenv/sql-migrate v1.7.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.starlark.net v0.0.0-20240520160348-046347dcd104 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.31.1 // indirect + k8s.io/apiextensions-apiserver v0.31.1 // indirect + k8s.io/apimachinery v0.31.1 // indirect + k8s.io/apiserver v0.31.1 // indirect + k8s.io/cli-runtime v0.31.1 // indirect + k8s.io/client-go v0.31.1 // indirect + k8s.io/component-base v0.31.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect + k8s.io/kubectl v0.31.1 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + oras.land/oras-go v1.2.5 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/kustomize/api v0.17.2 // indirect + sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace github.com/docker/docker => github.com/moby/moby v24.0.7+incompatible diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..1a65c2da --- /dev/null +++ b/go.sum @@ -0,0 +1,523 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.12.4 h1:Ev7YUMHAHoWNm+aDSPzc5W9s6E2jyL1szpVDJeZ/Rr4= +github.com/Microsoft/hcsshim v0.12.4/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/Songmu/retry v0.1.0 h1:hPA5xybQsksLR/ry/+t/7cFajPW+dqjmjhzZhioBILA= +github.com/Songmu/retry v0.1.0/go.mod h1:7sXIW7eseB9fq0FUvigRcQMVLR9tuHI0Scok+rkpAuA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +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-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= +github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= +github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= +github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= +github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= +github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= +github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +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.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +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.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/moby v24.0.7+incompatible h1:RrVT5IXBn85mRtFKP+gFwVLCcnNPZIgN3NVRJG9Le+4= +github.com/moby/moby v24.0.7+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= +github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rubenv/sql-migrate v1.7.0 h1:HtQq1xyTN2ISmQDggnh0c9U3JlP8apWh8YO2jzlXpTI= +github.com/rubenv/sql-migrate v1.7.0/go.mod h1:S4wtDEG1CKn+0ShpTtzWhFpHHI5PvCUtiGI+C+Z2THE= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.starlark.net v0.0.0-20240520160348-046347dcd104 h1:3qhteRISupnJvaWshOmeqEUs2y9oc/+/ePPvDh3Eygg= +go.starlark.net v0.0.0-20240520160348-046347dcd104/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +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.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +helm.sh/helm/v3 v3.16.2 h1:Y9v7ry+ubQmi+cb5zw1Llx8OKHU9Hk9NQ/+P+LGBe2o= +helm.sh/helm/v3 v3.16.2/go.mod h1:SyTXgKBjNqi2NPsHCW5dDAsHqvGIu0kdNYNH9gQaw70= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= +k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= +k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= +k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk= +k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= +k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= +k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24= +k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= +oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g= +sigs.k8s.io/kustomize/api v0.17.2/go.mod h1:UWTz9Ct+MvoeQsHcJ5e+vziRRkwimm3HytpZgIYqye0= +sigs.k8s.io/kustomize/kyaml v0.17.1 h1:TnxYQxFXzbmNG6gOINgGWQt09GghzgTP6mIurOgrLCQ= +sigs.k8s.io/kustomize/kyaml v0.17.1/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/magefile.go b/magefile.go new file mode 100644 index 00000000..64bcb1c1 --- /dev/null +++ b/magefile.go @@ -0,0 +1,148 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build mage +// +build mage + +package main + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +func Lint() error { + // if running on CI we have a gh action to run it + if os.Getenv("CI") == "" { + if err := sh.RunV("golangci-lint", "run", "--timeout", "5m"); err != nil { + return err + } + } + + if err := sh.RunV("go", "vet", "-v", "./..."); err != nil { + return err + } + if err := sh.RunV("goimports", "-w", "-l", "."); err != nil { + return err + } + if err := sh.RunV("go", "mod", "tidy"); err != nil { + return err + } + + return sh.RunV("git", "diff", "--exit-code") +} + +func CheckLicenseHeaders() error { + var checkFailed bool + + if err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + ext := filepath.Ext(path) + if ext == ".sh" || ext == ".go" { + fmt.Print("Checking ", path, " ") + + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + var hasCopyright bool + var hasLicense bool + + scanner := bufio.NewScanner(f) + // only check first 20 lines + for i := 0; i < 20 && scanner.Scan(); i++ { + line := scanner.Text() + if !hasCopyright && strings.Contains(line, "Copyright The Helm Authors") { + hasCopyright = true + } + if !hasLicense && strings.Contains(line, "https://www.apache.org/licenses/LICENSE-2.0") { + hasLicense = true + } + } + + if !(hasCopyright && hasLicense) { + fmt.Println("❌") + checkFailed = true + } else { + fmt.Println("☑️") + } + + return nil + } + return nil + }); err != nil { + return err + } + + if checkFailed { + return errors.New("file(s) without license header found") + } + return nil +} + +func Test() error { + return sh.RunV("go", "test", "./...", "-race") +} + +func Build() error { + return sh.RunV("goreleaser", "release", "--clean", "--snapshot", "--skip=sign") +} + +func CI() error { + if err := CheckLicenseHeaders(); err != nil { + return err + } + if err := Lint(); err != nil { + return err + } + if err := Test(); err != nil { + return err + } + if err := Build(); err != nil { + return err + } + + return nil +} + +func Release() error { + mg.Deps(Test) + + if err := CheckLicenseHeaders(); err != nil { + return err + } + + var args []string + args = append(args, "release", "--clean") + + if os.Getenv("GITHUB_REPOSITORY") != "helm/chart-releaser" { + args = append(args, "--skip=docker,homebrew") + } + + return sh.RunV("goreleaser", args...) +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..310dc11c --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,140 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "path/filepath" + "reflect" + "strings" + + "github.com/mitchellh/go-homedir" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var ( + homeDir, _ = homedir.Dir() + configSearchLocations = []string{ + ".", + filepath.Join(homeDir, ".cr"), + "/etc/cr", + } +) + +type Options struct { + Owner string `mapstructure:"owner"` + GitRepo string `mapstructure:"git-repo"` + ChartsRepo string `mapstructure:"charts-repo"` + IndexPath string `mapstructure:"index-path"` + PackagePath string `mapstructure:"package-path"` + Sign bool `mapstructure:"sign"` + Key string `mapstructure:"key"` + KeyRing string `mapstructure:"keyring"` + PassphraseFile string `mapstructure:"passphrase-file"` + Token string `mapstructure:"token"` + GitBaseURL string `mapstructure:"git-base-url"` + GitUploadURL string `mapstructure:"git-upload-url"` + Commit string `mapstructure:"commit"` + PagesBranch string `mapstructure:"pages-branch"` + PagesIndexPath string `mapstructure:"pages-index-path"` + Push bool `mapstructure:"push"` + PR bool `mapstructure:"pr"` + Remote string `mapstructure:"remote"` + ReleaseNameTemplate string `mapstructure:"release-name-template"` + SkipExisting bool `mapstructure:"skip-existing"` + ReleaseNotesFile string `mapstructure:"release-notes-file"` + GenerateReleaseNotes bool `mapstructure:"generate-release-notes"` + MakeReleaseLatest bool `mapstructure:"make-release-latest"` + PackagesWithIndex bool `mapstructure:"packages-with-index"` + Prerelease bool `mapstructure:"prerelease"` +} + +func LoadConfiguration(cfgFile string, cmd *cobra.Command, requiredFlags []string) (*Options, error) { + v := viper.New() + + cmd.Flags().VisitAll(func(flag *flag.Flag) { + flagName := flag.Name + if flagName != "config" && flagName != "help" { + if err := v.BindPFlag(flagName, flag); err != nil { + // can't really happen + panic(fmt.Sprintln(errors.Wrapf(err, "Error binding flag '%s'", flagName))) + } + } + }) + + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + v.SetEnvPrefix("CR") + + if cfgFile != "" { + v.SetConfigFile(cfgFile) + } else { + v.SetConfigName("cr") + for _, searchLocation := range configSearchLocations { + v.AddConfigPath(searchLocation) + } + } + + if err := v.ReadInConfig(); err != nil { + if cfgFile != "" { + // Only error out for specified config file. Ignore for default locations. + return nil, errors.Wrap(err, "Error loading config file") + } + } else { + fmt.Println("Using config file: ", v.ConfigFileUsed()) + } + + opts := &Options{} + if err := v.Unmarshal(opts); err != nil { + return nil, errors.Wrap(err, "Error unmarshaling configuration") + } + + if opts.Push && opts.PR { + return nil, errors.New("specify either --push or --pr, but not both") + } + + elem := reflect.ValueOf(opts).Elem() + for _, requiredFlag := range requiredFlags { + fieldName := kebabCaseToTitleCamelCase(requiredFlag) + f := elem.FieldByName(fieldName) + value := fmt.Sprintf("%v", f.Interface()) + if value == "" { + return nil, errors.Errorf("'--%s' is required", requiredFlag) + } + } + + return opts, nil +} + +func kebabCaseToTitleCamelCase(input string) (result string) { + nextToUpper := true + for _, runeValue := range input { + if nextToUpper { + result += strings.ToUpper(string(runeValue)) + nextToUpper = false + } else { + if runeValue == '-' { + nextToUpper = true + } else { + result += string(runeValue) + } + } + } + return +} diff --git a/pkg/git/git.go b/pkg/git/git.go new file mode 100644 index 00000000..39212189 --- /dev/null +++ b/pkg/git/git.go @@ -0,0 +1,96 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package git + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +type Git struct{} + +// AddWorktree creates a new Git worktree with a detached HEAD for the given commit-ish and returns its path. +func (g *Git) AddWorktree(workingDir string, commitIsh string) (string, error) { + dir, err := os.MkdirTemp("", "chart-releaser-") + if err != nil { + return "", err + } + command := exec.Command("git", "worktree", "add", "--detach", dir, commitIsh) + + if err := runCommand(workingDir, command); err != nil { + return "", err + } + return dir, nil +} + +// RemoveWorktree removes the Git worktree with the given path. +func (g *Git) RemoveWorktree(workingDir string, path string) error { + command := exec.Command("git", "worktree", "remove", path, "--force") + return runCommand(workingDir, command) +} + +// Add runs 'git add' with the given args. +func (g *Git) Add(workingDir string, args ...string) error { + if len(args) == 0 { + return fmt.Errorf("no args specified") + } + addArgs := []string{"add"} + addArgs = append(addArgs, args...) + command := exec.Command("git", addArgs...) + return runCommand(workingDir, command) +} + +// Commit runs 'git commit' with the given message. the commit is signed off. +func (g *Git) Commit(workingDir string, message string) error { + command := exec.Command("git", "commit", "--message", message, "--signoff") + return runCommand(workingDir, command) +} + +// UpdateBranch runs 'git pull' with the given args. +func (g *Git) Pull(workingDir string, args ...string) error { + pullArgs := []string{"pull"} + pullArgs = append(pullArgs, args...) + command := exec.Command("git", pullArgs...) + return runCommand(workingDir, command) +} + +// Push runs 'git push' with the given args. +func (g *Git) Push(workingDir string, args ...string) error { + pushArgs := []string{"push"} + pushArgs = append(pushArgs, args...) + command := exec.Command("git", pushArgs...) + return runCommand(workingDir, command) +} + +// GetPushURL returns the push url with a token inserted +func (g *Git) GetPushURL(remote string, token string) (string, error) { + pushURL, err := exec.Command("git", "remote", "get-url", "--push", remote).Output() + if err != nil { + return "", err + } + + pushURLArray := strings.SplitAfter(strings.TrimSpace(string(pushURL)), "https://") + pushURLWithToken := fmt.Sprintf("https://x-access-token:%s@%s", token, pushURLArray[1]) + return pushURLWithToken, nil +} + +func runCommand(workingDir string, command *exec.Cmd) error { + command.Dir = workingDir + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} diff --git a/pkg/git/git_test.go b/pkg/git/git_test.go new file mode 100644 index 00000000..e84b4850 --- /dev/null +++ b/pkg/git/git_test.go @@ -0,0 +1,85 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package git + +import ( + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGit_GetPushURL(t *testing.T) { + curDir, _ := os.Getwd() + repoPath := t.TempDir() + repoDirErr := os.Chdir(repoPath) + if repoDirErr != nil { + t.Error(repoDirErr.Error()) + } + + t.Cleanup(func() { + chdirErr := os.Chdir(curDir) + if chdirErr != nil { + t.Error(chdirErr.Error()) + } + }) + + _, initErr := exec.Command("git", "init").Output() + if initErr != nil { + t.Error(initErr.Error()) + } + + tests := []struct { + name string + repo string + remote string + url string + token string + pushURL string + }{ + { + name: "Public GitHub", + repo: "publicrepo", + remote: "public", + url: "https://github.com/org/publicrepo", + token: "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", + pushURL: "https://x-access-token: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@github.com/org/publicrepo", + }, + { + name: "GitHub Enterprise", + repo: "privaterepo", + remote: "enterprise", + url: "https://github.example.com/org/privaterepo", + token: "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", + pushURL: "https://x-access-token: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@github.example.com/org/privaterepo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, addErr := exec.Command("git", "remote", "add", tt.remote, tt.url).Output() + if addErr != nil { + t.Error(addErr.Error()) + } + + g := Git{} + pushURL, pushErr := g.GetPushURL(tt.remote, tt.token) + + require.Empty(t, pushErr) + require.EqualValues(t, pushURL, tt.pushURL) + }) + } +} diff --git a/pkg/github/github.go b/pkg/github/github.go new file mode 100644 index 00000000..80217f63 --- /dev/null +++ b/pkg/github/github.go @@ -0,0 +1,181 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package github + +import ( + "context" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/Songmu/retry" + "github.com/pkg/errors" + + "github.com/google/go-github/v56/github" + "golang.org/x/oauth2" +) + +type Release struct { + Name string + Description string + Assets []*Asset + Commit string + GenerateReleaseNotes bool + MakeLatest string + Prerelease bool +} + +type Asset struct { + Path string + URL string +} + +// Client is the client for interacting with the GitHub API +type Client struct { + owner string + repo string + *github.Client +} + +// NewClient creates and initializes a new GitHubClient +func NewClient(owner, repo, token, baseURL, uploadURL string) *Client { + var client *github.Client + if token != "" { + ts := oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: token, + }) + tc := oauth2.NewClient(context.TODO(), ts) + client = github.NewClient(tc) + } else { + client = github.NewClient(nil) + } + + if baseEndpoint, err := url.Parse(baseURL); err == nil { + if !strings.HasSuffix(baseEndpoint.Path, "/") { + baseEndpoint.Path += "/" + } + client.BaseURL = baseEndpoint + } + + if uploadEndpoint, err := url.Parse(uploadURL); err == nil { + if !strings.HasSuffix(uploadEndpoint.Path, "/") { + uploadEndpoint.Path += "/" + } + client.UploadURL = uploadEndpoint + } + + return &Client{ + owner: owner, + repo: repo, + Client: client, + } +} + +// GetRelease queries the GitHub API for a specified release object +func (c *Client) GetRelease(_ context.Context, tag string) (*Release, error) { + // Check Release whether already exists or not + release, _, err := c.Repositories.GetReleaseByTag(context.TODO(), c.owner, c.repo, tag) + if err != nil { + return nil, err + } + + result := &Release{ + Assets: []*Asset{}, + } + for _, ass := range release.Assets { + asset := &Asset{*ass.Name, *ass.BrowserDownloadURL} + result.Assets = append(result.Assets, asset) + } + return result, nil +} + +// CreateRelease creates a new release object in the GitHub API +func (c *Client) CreateRelease(_ context.Context, input *Release) error { + req := &github.RepositoryRelease{ + Name: &input.Name, + Body: &input.Description, + TagName: &input.Name, + TargetCommitish: &input.Commit, + GenerateReleaseNotes: &input.GenerateReleaseNotes, + MakeLatest: &input.MakeLatest, + Prerelease: &input.Prerelease, + } + + release, _, err := c.Repositories.CreateRelease(context.TODO(), c.owner, c.repo, req) + if err != nil { + return err + } + + for _, asset := range input.Assets { + if err := c.uploadReleaseAsset(context.TODO(), *release.ID, asset.Path); err != nil { + return err + } + } + return nil +} + +// CreatePullRequest creates a pull request in the repository specified by repoURL. +// The return value is the pull request URL. +func (c *Client) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) { + split := strings.SplitN(message, "\n", 2) + title := split[0] + + pr := &github.NewPullRequest{ + Title: &title, + Head: &head, + Base: &base, + } + if len(split) == 2 { + body := strings.TrimSpace(split[1]) + pr.Body = &body + } + + pullRequest, _, err := c.PullRequests.Create(context.Background(), owner, repo, pr) + if err != nil { + return "", err + } + return *pullRequest.HTMLURL, nil +} + +// UploadAsset uploads specified assets to a given release object +func (c *Client) uploadReleaseAsset(_ context.Context, releaseID int64, filename string) error { + filename, err := filepath.Abs(filename) + if err != nil { + return errors.Wrap(err, "failed to get abs path") + } + + opts := &github.UploadOptions{ + // Use base name by default + Name: filepath.Base(filename), + } + + if err := retry.Retry(3, 3*time.Second, func() error { //nolint: revive + f, err := os.Open(filename) + if err != nil { + return errors.Wrap(err, "failed to open file") + } + defer f.Close() + if _, _, err = c.Repositories.UploadReleaseAsset(context.TODO(), c.owner, c.repo, releaseID, opts, f); err != nil { + return errors.Wrapf(err, "failed to upload release asset: %s", filename) + } + return nil + }); err != nil { + return err + } + + return nil +} diff --git a/pkg/packager/packager.go b/pkg/packager/packager.go new file mode 100644 index 00000000..6f17ce9d --- /dev/null +++ b/pkg/packager/packager.go @@ -0,0 +1,108 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package packager + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" + + "github.com/helm/chart-releaser/pkg/config" + "github.com/mitchellh/go-homedir" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/registry" +) + +// Packager exposes the packager object +type Packager struct { + config *config.Options + paths []string +} + +// NewPackager returns a configured Packager +func NewPackager(config *config.Options, paths []string) *Packager { + return &Packager{ + config: config, + paths: paths, + } +} + +// CreatePackages creates Helm chart packages +func (p *Packager) CreatePackages() error { + helmClient := action.NewPackage() + helmClient.DependencyUpdate = true + helmClient.Destination = p.config.PackagePath + if p.config.Sign { + // expand the ~ to the full home dir + if strings.HasPrefix(p.config.KeyRing, "~") { + dir, err := homedir.Dir() + if err != nil { + panic(err) + } + + p.config.KeyRing = strings.ReplaceAll(p.config.KeyRing, "~", dir) + } + + helmClient.Sign = true + helmClient.Key = p.config.Key + helmClient.Keyring = p.config.KeyRing + helmClient.PassphraseFile = p.config.PassphraseFile + } + + settings := cli.New() + getters := getter.All(settings) + registryClient, err := registry.NewClient() + if err != nil { + return err + } + + for i := 0; i < len(p.paths); i++ { + path, err := filepath.Abs(p.paths[i]) + if err != nil { + return err + } + if _, err := os.Stat(p.paths[i]); err != nil { + return err + } + + downloadManager := &downloader.Manager{ + Out: io.Discard, + ChartPath: path, + Keyring: helmClient.Keyring, + Getters: getters, + Debug: settings.Debug, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + RegistryClient: registryClient, + } + if err := downloadManager.Build(); err != nil { + return err + } + packageRun, err := helmClient.Run(path, nil) + if err != nil { + fmt.Printf("Failed to package chart in %s (%s)\n", path, err.Error()) + return err + } + + fmt.Printf("Successfully packaged chart in %s and saved it to: %s\n", path, packageRun) + } + return nil +} diff --git a/pkg/packager/packager_test.go b/pkg/packager/packager_test.go new file mode 100644 index 00000000..603ea19e --- /dev/null +++ b/pkg/packager/packager_test.go @@ -0,0 +1,97 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package packager + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/helm/chart-releaser/pkg/config" +) + +func TestPackager_CreatePackages(t *testing.T) { + packagePath, _ := os.MkdirTemp(".", "packages") + invalidPackagePath := filepath.Join(packagePath, "bad") + file, _ := os.Create(invalidPackagePath) + t.Cleanup(func() { + file.Close() + os.RemoveAll(packagePath) + }) + + tests := []struct { + name string + chartPath string + options *config.Options + error bool + }{ + { + name: "valid-chart-path", + chartPath: "testdata/test-chart", + options: &config.Options{PackagePath: packagePath}, + error: false, + }, + { + name: "invalid-package-path", + chartPath: "testdata/test-chart", + options: &config.Options{PackagePath: invalidPackagePath}, + error: true, + }, + { + name: "invalid-chart-path", + chartPath: "testdata/invalid-chart", + options: &config.Options{PackagePath: packagePath}, + error: true, + }, + { + name: "valid-chart-path-with-provenance", + chartPath: "testdata/test-chart", + options: &config.Options{ + PackagePath: packagePath, + Sign: true, + Key: "Chart Releaser Test Key ", + KeyRing: "testdata/testkeyring.gpg", + PassphraseFile: "testdata/passphrase-file.txt", + }, + error: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + os.Remove(filepath.Join(packagePath, "test-chart-0.1.0.tgz")) + os.Remove(filepath.Join(packagePath, "test-chart-0.1.0.tgz.prov")) + }) + + p := &Packager{ + paths: []string{tt.chartPath}, + config: tt.options, + } + err := p.CreatePackages() + if tt.error { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.FileExists(t, filepath.Join(tt.options.PackagePath, "test-chart-0.1.0.tgz")) + if tt.options.Sign { + assert.FileExists(t, filepath.Join(tt.options.PackagePath, "test-chart-0.1.0.tgz.prov")) + } + } + }) + } +} diff --git a/pkg/packager/testdata/passphrase-file.txt b/pkg/packager/testdata/passphrase-file.txt new file mode 100644 index 00000000..d97c5ead --- /dev/null +++ b/pkg/packager/testdata/passphrase-file.txt @@ -0,0 +1 @@ +secret diff --git a/pkg/packager/testdata/test-chart/.helmignore b/pkg/packager/testdata/test-chart/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/pkg/packager/testdata/test-chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/packager/testdata/test-chart/Chart.yaml b/pkg/packager/testdata/test-chart/Chart.yaml new file mode 100644 index 00000000..0d1dbd10 --- /dev/null +++ b/pkg/packager/testdata/test-chart/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: test-chart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 1.16.0 diff --git a/pkg/packager/testdata/test-chart/templates/NOTES.txt b/pkg/packager/testdata/test-chart/templates/NOTES.txt new file mode 100644 index 00000000..0b1a16d5 --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "test-chart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "test-chart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "test-chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "test-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/pkg/packager/testdata/test-chart/templates/_helpers.tpl b/pkg/packager/testdata/test-chart/templates/_helpers.tpl new file mode 100644 index 00000000..129835c9 --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "test-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "test-chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "test-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "test-chart.labels" -}} +helm.sh/chart: {{ include "test-chart.chart" . }} +{{ include "test-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "test-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "test-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "test-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "test-chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/pkg/packager/testdata/test-chart/templates/deployment.yaml b/pkg/packager/testdata/test-chart/templates/deployment.yaml new file mode 100644 index 00000000..91e453b6 --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "test-chart.fullname" . }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} +spec: +{{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} +{{- end }} + selector: + matchLabels: + {{- include "test-chart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "test-chart.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "test-chart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/pkg/packager/testdata/test-chart/templates/hpa.yaml b/pkg/packager/testdata/test-chart/templates/hpa.yaml new file mode 100644 index 00000000..4642327b --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "test-chart.fullname" . }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "test-chart.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/pkg/packager/testdata/test-chart/templates/ingress.yaml b/pkg/packager/testdata/test-chart/templates/ingress.yaml new file mode 100644 index 00000000..95c5936f --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "test-chart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} diff --git a/pkg/packager/testdata/test-chart/templates/service.yaml b/pkg/packager/testdata/test-chart/templates/service.yaml new file mode 100644 index 00000000..b8d5d516 --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "test-chart.fullname" . }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "test-chart.selectorLabels" . | nindent 4 }} diff --git a/pkg/packager/testdata/test-chart/templates/serviceaccount.yaml b/pkg/packager/testdata/test-chart/templates/serviceaccount.yaml new file mode 100644 index 00000000..85ea1e19 --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "test-chart.serviceAccountName" . }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/pkg/packager/testdata/test-chart/templates/tests/test-connection.yaml b/pkg/packager/testdata/test-chart/templates/tests/test-connection.yaml new file mode 100644 index 00000000..772f913e --- /dev/null +++ b/pkg/packager/testdata/test-chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "test-chart.fullname" . }}-test-connection" + labels: + {{- include "test-chart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "test-chart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/pkg/packager/testdata/test-chart/values.yaml b/pkg/packager/testdata/test-chart/values.yaml new file mode 100644 index 00000000..d009b4a4 --- /dev/null +++ b/pkg/packager/testdata/test-chart/values.yaml @@ -0,0 +1,79 @@ +# Default values for test-chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/packager/testdata/testkeyring.gpg b/pkg/packager/testdata/testkeyring.gpg new file mode 100644 index 00000000..69be75c6 Binary files /dev/null and b/pkg/packager/testdata/testkeyring.gpg differ diff --git a/pkg/releaser/releaser.go b/pkg/releaser/releaser.go new file mode 100644 index 00000000..8f89cf50 --- /dev/null +++ b/pkg/releaser/releaser.go @@ -0,0 +1,422 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package releaser + +import ( + "bytes" + "context" + "fmt" + "io" + "math/rand" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/Songmu/retry" + + "text/template" + + "helm.sh/helm/v3/pkg/chart" + + "github.com/pkg/errors" + "helm.sh/helm/v3/pkg/chart/loader" + + "github.com/helm/chart-releaser/pkg/config" + + "helm.sh/helm/v3/pkg/provenance" + "helm.sh/helm/v3/pkg/repo" + + "github.com/helm/chart-releaser/pkg/github" +) + +// GitHub contains the functions necessary for interacting with GitHub release +// objects +type GitHub interface { + CreateRelease(ctx context.Context, input *github.Release) error + GetRelease(ctx context.Context, tag string) (*github.Release, error) + CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) +} + +type Git interface { + AddWorktree(workingDir string, commitIsh string) (string, error) + RemoveWorktree(workingDir string, path string) error + Add(workingDir string, args ...string) error + Commit(workingDir string, message string) error + Push(workingDir string, args ...string) error + Pull(workingDir string, args ...string) error + GetPushURL(remote string, token string) (string, error) +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + +const chartAssetFileExtension = ".tgz" + +func init() { + rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec +} + +type Releaser struct { + config *config.Options + github GitHub + git Git +} + +func NewReleaser(config *config.Options, github GitHub, git Git) *Releaser { + return &Releaser{ + config: config, + github: github, + git: git, + } +} + +// UpdateIndexFile updates the index.yaml file for a given Git repo +func (r *Releaser) UpdateIndexFile() (bool, error) { + // if index-path doesn't end with index.yaml we can try and fix it + if filepath.Base(r.config.IndexPath) != "index.yaml" { + // if path is a directory then add index.yaml + if stat, err := os.Stat(r.config.IndexPath); err == nil && stat.IsDir() { + r.config.IndexPath = filepath.Join(r.config.IndexPath, "index.yaml") + // otherwise error out + } else { + fmt.Printf("index-path (%s) should be a directory or a file called index.yaml\n", r.config.IndexPath) + os.Exit(1) + } + } + + fmt.Printf("Loading index file from git repository %s\n", r.config.IndexPath) + worktree, err := r.git.AddWorktree("", r.config.Remote+"/"+r.config.PagesBranch) + if err != nil { + return false, err + } + defer r.git.RemoveWorktree("", worktree) // nolint: errcheck + + // if pages-index-path doesn't end with index.yaml we can try and fix it + if filepath.Base(r.config.PagesIndexPath) != "index.yaml" { + // if path is a directory then add index.yaml + if err := os.MkdirAll(filepath.Join(worktree, r.config.PagesIndexPath), 0755); err == nil { + r.config.PagesIndexPath = filepath.Join(r.config.PagesIndexPath, "index.yaml") + } else { + fmt.Printf("pages-index-path (%s) should be a directory or a file called index.yaml\n", r.config.PagesIndexPath) + os.Exit(1) // nolint: gocritic + } + } + indexYamlPath := filepath.Join(worktree, r.config.PagesIndexPath) + + var indexFile *repo.IndexFile + _, err = os.Stat(indexYamlPath) + if err == nil { // nolint: gocritic + indexFile, err = repo.LoadIndexFile(indexYamlPath) + if err != nil { + return false, err + } + } else if errors.Is(err, os.ErrNotExist) { + indexFile = repo.NewIndexFile() + } else { + return false, err + } + + // We have to explicitly glob for *.tgz files only. If GPG signing is enabled, + // this would also return *.tgz.prov files otherwise, which we don't want here. + chartPackages, err := filepath.Glob(r.config.PackagePath + "/*.tgz") + if err != nil { + return false, err + } + + var update bool + for _, chartPackage := range chartPackages { + ch, err := loader.LoadFile(chartPackage) + if err != nil { + return false, err + } + releaseName, err := r.computeReleaseName(ch) + if err != nil { + return false, err + } + + var release *github.Release + if err := retry.Retry(3, 3*time.Second, func() error { + rel, err := r.github.GetRelease(context.TODO(), releaseName) + if err != nil { + return err + } + release = rel + return nil + }); err != nil { + return false, err + } + + for _, asset := range release.Assets { + downloadURL, _ := url.Parse(asset.URL) + name := filepath.Base(downloadURL.Path) + // Ignore any other files added in the release by the users. + if filepath.Ext(name) != chartAssetFileExtension { + continue + } + baseName := strings.TrimSuffix(name, filepath.Ext(name)) + tagParts := r.splitPackageNameAndVersion(baseName) + packageName, packageVersion := tagParts[0], tagParts[1] + fmt.Printf("Found %s-%s.tgz\n", packageName, packageVersion) + if _, err := indexFile.Get(packageName, packageVersion); err != nil { + if err := r.addToIndexFile(indexFile, downloadURL.String()); err != nil { + return false, err + } + update = true + break + } + } + } + + if !update { + fmt.Printf("Index %s did not change\n", r.config.IndexPath) + return false, nil + } + + fmt.Printf("Updating index %s\n", r.config.IndexPath) + indexFile.SortEntries() + + indexFile.Generated = time.Now() + + if err := indexFile.WriteFile(r.config.IndexPath, 0644); err != nil { + return false, err + } + + if !r.config.Push && !r.config.PR { + return true, nil + } + + if err := copyFile(r.config.IndexPath, indexYamlPath); err != nil { + return false, err + } + + if err := r.git.Pull(worktree, r.config.Remote, r.config.PagesBranch); err != nil { + return false, err + } + + if err := r.git.Add(worktree, indexYamlPath); err != nil { + return false, err + } + + if err := r.git.Commit(worktree, fmt.Sprintf("Update %s", r.config.PagesIndexPath)); err != nil { + return false, err + } + + if err := r.pushToPagesBranch(worktree); err != nil { + return false, err + } + + return true, nil +} + +func (r *Releaser) computeReleaseName(chart *chart.Chart) (string, error) { + tmpl, err := template.New("gotpl").Parse(r.config.ReleaseNameTemplate) + if err != nil { + return "", err + } + + var buffer bytes.Buffer + if err := tmpl.Execute(&buffer, chart.Metadata); err != nil { + return "", err + } + + releaseName := buffer.String() + return releaseName, nil +} + +func (r *Releaser) getReleaseNotes(chart *chart.Chart) string { + if r.config.ReleaseNotesFile != "" { + for _, f := range chart.Files { + if f.Name == r.config.ReleaseNotesFile { + return string(f.Data) + } + } + fmt.Printf("The release note file %q, is not present in the chart package\n", r.config.ReleaseNotesFile) + } + return chart.Metadata.Description +} + +func (r *Releaser) splitPackageNameAndVersion(pkg string) []string { + delimIndex := strings.LastIndex(pkg, "-") + return []string{pkg[0:delimIndex], pkg[delimIndex+1:]} +} + +func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error { + arch := filepath.Join(r.config.PackagePath, filepath.Base(url)) + + // extract chart metadata + fmt.Printf("Extracting chart metadata from %s\n", arch) + c, err := loader.LoadFile(arch) + if err != nil { + return errors.Wrapf(err, "%s is not a helm chart package", arch) + } + // calculate hash + fmt.Printf("Calculating Hash for %s\n", arch) + hash, err := provenance.DigestFile(arch) + if err != nil { + return err + } + + // remove url name from url as helm's index library + // adds it in during .Add + // there should be a better way to handle this :( + s := strings.Split(url, "/") + s = s[:len(s)-1] + + if r.config.PackagesWithIndex { + // the chart will be stored in the same repo as + // the index file so let's make the path relative + s = s[:0] + } + + // Add to index + return indexFile.MustAdd(c.Metadata, filepath.Base(arch), strings.Join(s, "/"), hash) +} + +// CreateReleases finds and uploads Helm chart packages to GitHub +func (r *Releaser) CreateReleases() error { + worktree, err := r.git.AddWorktree("", r.config.Remote+"/"+r.config.PagesBranch) + if err != nil { + return err + } + defer r.git.RemoveWorktree("", worktree) // nolint: errcheck + + packages, err := r.getListOfPackages(r.config.PackagePath) + if err != nil { + return err + } + + if len(packages) == 0 { + return errors.Errorf("no charts found at %s", r.config.PackagePath) + } + + for _, p := range packages { + ch, err := loader.LoadFile(p) + if err != nil { + return err + } + releaseName, err := r.computeReleaseName(ch) + if err != nil { + return err + } + + release := &github.Release{ + Name: releaseName, + Description: r.getReleaseNotes(ch), + Assets: []*github.Asset{ + {Path: p}, + }, + Commit: r.config.Commit, + GenerateReleaseNotes: r.config.GenerateReleaseNotes, + MakeLatest: strconv.FormatBool(r.config.MakeReleaseLatest), + Prerelease: r.config.Prerelease, + } + provFile := fmt.Sprintf("%s.prov", p) + if _, err := os.Stat(provFile); err == nil { + asset := &github.Asset{Path: provFile} + release.Assets = append(release.Assets, asset) + } + if r.config.SkipExisting { + existingRelease, _ := r.github.GetRelease(context.TODO(), releaseName) + if existingRelease != nil { + continue + } + } + if err := r.github.CreateRelease(context.TODO(), release); err != nil { + return errors.Wrapf(err, "error creating GitHub release %s", releaseName) + } + + if r.config.PackagesWithIndex { + pkgTargetPath := filepath.Join(worktree, filepath.Base(p)) + if err := copyFile(p, pkgTargetPath); err != nil { + return err + } + + if err := r.git.Add(worktree, pkgTargetPath); err != nil { + return err + } + + if err := r.git.Commit(worktree, fmt.Sprintf("Publishing chart package for %s", releaseName)); err != nil { + return err + } + } + } + if r.config.Push { + if err := r.pushToPagesBranch(worktree); err != nil { + return err + } + } + + return nil +} + +func (r *Releaser) getListOfPackages(dir string) ([]string, error) { + return filepath.Glob(filepath.Join(dir, "*.tgz")) +} + +func (r *Releaser) pushToPagesBranch(worktree string) error { + pushURL, err := r.git.GetPushURL(r.config.Remote, r.config.Token) + if err != nil { + return err + } + + if r.config.Push { + fmt.Printf("Pushing to branch %q\n", r.config.PagesBranch) + if err := r.git.Push(worktree, pushURL, "HEAD:refs/heads/"+r.config.PagesBranch); err != nil { + return err + } + } else if r.config.PR { + branch := fmt.Sprintf("chart-releaser-%s", randomString(16)) + + fmt.Printf("Pushing to branch %q\n", branch) + if err := r.git.Push(worktree, pushURL, "HEAD:refs/heads/"+branch); err != nil { + return err + } + fmt.Printf("Creating pull request against branch %q\n", r.config.PagesBranch) + prURL, err := r.github.CreatePullRequest(r.config.Owner, r.config.GitRepo, "Update index.yaml", branch, r.config.PagesBranch) + if err != nil { + return err + } + fmt.Println("Pull request created:", prURL) + } + + return nil +} + +func copyFile(srcFile string, dstFile string) error { + source, err := os.Open(srcFile) + if err != nil { + return err + } + defer source.Close() + + destination, err := os.Create(dstFile) + if err != nil { + return err + } + defer destination.Close() + + _, err = io.Copy(destination, source) + return err +} + +func randomString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] // nolint: gosec + } + return string(b) +} diff --git a/pkg/releaser/releaser_test.go b/pkg/releaser/releaser_test.go new file mode 100644 index 00000000..52e747cc --- /dev/null +++ b/pkg/releaser/releaser_test.go @@ -0,0 +1,537 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package releaser + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/helm/chart-releaser/pkg/github" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "helm.sh/helm/v3/pkg/provenance" + "helm.sh/helm/v3/pkg/repo" + + "github.com/helm/chart-releaser/pkg/config" +) + +type FakeGitHub struct { + mock.Mock + release *github.Release +} + +type FakeGit struct { + indexFile string + mock.Mock +} + +func (f *FakeGit) AddWorktree(workingDir string, committish string) (string, error) { //nolint: revive + dir, err := os.MkdirTemp("", "chart-releaser-") + if err != nil { + return "", err + } + if len(f.indexFile) == 0 { + return dir, nil + } + + return dir, copyFile(f.indexFile, filepath.Join(dir, "index.yaml")) +} + +func (f *FakeGit) RemoveWorktree(workingDir string, path string) error { + f.Called(workingDir, path) + return os.RemoveAll(workingDir) +} + +func (f *FakeGit) Add(workingDir string, args ...string) error { + f.Called(workingDir, args) + if len(args) == 0 { + return fmt.Errorf("no args specified") + } + return nil +} + +func (f *FakeGit) Commit(workingDir string, message string) error { + f.Called(workingDir, message) + return nil +} + +func (f *FakeGit) Pull(workingDir string, args ...string) error { + f.Called(workingDir, args) + return nil +} + +func (f *FakeGit) Push(workingDir string, args ...string) error { + f.Called(workingDir, args) + return nil +} + +func (f *FakeGit) GetPushURL(remote string, token string) (string, error) { + f.Called(remote, token) + pushURLWithToken := fmt.Sprintf("https://x-access-token:%s@github.com/owner/repo", token) + return pushURLWithToken, nil +} + +func (f *FakeGitHub) CreateRelease(ctx context.Context, input *github.Release) error { + f.Called(ctx, input) + f.release = input + return nil +} + +func (f *FakeGitHub) GetRelease(ctx context.Context, tag string) (*github.Release, error) { //nolint: revive + release := &github.Release{ + Name: "testdata/release-packages/test-chart-0.1.0", + Description: "A Helm chart for Kubernetes", + Assets: []*github.Asset{ + { + Path: "testdata/release-packages/test-chart-0.1.0.tgz", + URL: "https://myrepo/charts/test-chart-0.1.0.tgz", + }, + { + Path: "testdata/release-packages/third-party-file-0.1.0.txt", + URL: "https://myrepo/charts/third-party-file-0.1.0.txt", + }, + }, + Prerelease: false, + } + return release, nil +} + +func (f *FakeGitHub) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) { + f.Called(owner, repo, message, head, base) + return "https://github.com/owner/repo/pull/42", nil +} + +func TestReleaser_UpdateIndexFile(t *testing.T) { + indexDir, _ := os.MkdirTemp(".", "index") + defer os.RemoveAll(indexDir) + + fakeGitHub := new(FakeGitHub) + + tests := []struct { + name string + exists bool + releaser *Releaser + indexFile string + }{ + { + name: "index-file-exists", + exists: true, + releaser: &Releaser{ + config: &config.Options{ + IndexPath: "testdata/index/index.yaml", + PackagePath: "testdata/release-packages", + }, + github: fakeGitHub, + }, + indexFile: "testdata/repo/index.yaml", + }, + { + name: "index-file-exists-pages-index-path", + exists: true, + releaser: &Releaser{ + config: &config.Options{ + IndexPath: "testdata/index/index.yaml", + PackagePath: "testdata/release-packages", + PagesIndexPath: "./", + }, + github: fakeGitHub, + }, + indexFile: "testdata/repo/index.yaml", + }, + { + name: "index-file-does-not-exist", + exists: false, + releaser: &Releaser{ + config: &config.Options{ + IndexPath: filepath.Join(indexDir, "index.yaml"), + PackagePath: "testdata/release-packages", + }, + github: fakeGitHub, + }, + indexFile: "", + }, + { + name: "index-file-does-not-exist-pages-index-path", + exists: false, + releaser: &Releaser{ + config: &config.Options{ + IndexPath: filepath.Join(indexDir, "index.yaml"), + PackagePath: "testdata/release-packages", + PagesIndexPath: "./", + }, + github: fakeGitHub, + }, + indexFile: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var sha256 string + if tt.exists { + sha256, _ = provenance.DigestFile(tt.releaser.config.IndexPath) + } + + fakeGit := new(FakeGit) + fakeGit.indexFile = tt.indexFile + fakeGit.On("RemoveWorktree", mock.Anything, mock.Anything).Return(nil) + tt.releaser.git = fakeGit + update, err := tt.releaser.UpdateIndexFile() + assert.NoError(t, err) + assert.Equal(t, update, !tt.exists) + if tt.exists { + newSha256, _ := provenance.DigestFile(tt.releaser.config.IndexPath) + assert.Equal(t, sha256, newSha256) + } else { + _, err := os.Stat(tt.releaser.config.IndexPath) + assert.NoError(t, err) + } + }) + } +} + +func TestReleaser_UpdateIndexFileGenerated(t *testing.T) { + indexDir, _ := os.MkdirTemp(".", "index") + defer os.RemoveAll(indexDir) + + fakeGitHub := new(FakeGitHub) + + tests := []struct { + name string + releaser *Releaser + indexFile string + }{ + { + name: "index-file-exists", + releaser: &Releaser{ + config: &config.Options{ + IndexPath: filepath.Join(indexDir, "index.yaml"), + PackagePath: "testdata/release-packages", + }, + github: fakeGitHub, + }, + indexFile: "testdata/empty-repo/index.yaml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeGit := new(FakeGit) + fakeGit.indexFile = tt.indexFile + fakeGit.On("RemoveWorktree", mock.Anything, mock.Anything).Return(nil) + tt.releaser.git = fakeGit + + indexFile, _ := repo.LoadIndexFile("testdata/empty-repo/index.yaml") + generated := indexFile.Generated + update, err := tt.releaser.UpdateIndexFile() + assert.NoError(t, err) + assert.True(t, update) + newIndexFile, _ := repo.LoadIndexFile(tt.releaser.config.IndexPath) + newGenerated := newIndexFile.Generated + assert.True(t, newGenerated.After(generated)) + assert.Equal(t, 2, len(newIndexFile.Entries)) + }) + } +} + +func TestReleaser_splitPackageNameAndVersion(t *testing.T) { + tests := []struct { + name string + pkg string + expected []string + }{ + { + "no-hyphen", + "foo", + nil, + }, + { + "one-hyphen", + "foo-1.2.3", + []string{"foo", "1.2.3"}, + }, + { + "two-hyphens", + "foo-bar-1.2.3", + []string{"foo-bar", "1.2.3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Releaser{} + if tt.expected == nil { + assert.Panics(t, func() { + r.splitPackageNameAndVersion(tt.pkg) + }, "slice bounds out of range") + } else { + actual := r.splitPackageNameAndVersion(tt.pkg) + assert.Equal(t, tt.expected, actual) + } + }) + } +} + +func TestReleaser_addToIndexFile(t *testing.T) { + tests := []struct { + name string + chart string + version string + releaser *Releaser + packagesWithIndex bool + error bool + }{ + { + "invalid-package", + "does-not-exist", + "0.1.0", + &Releaser{ + config: &config.Options{ + PackagePath: "testdata/release-packages", + PackagesWithIndex: false, + }, + }, + false, + true, + }, + { + "valid-package", + "test-chart", + "0.1.0", + &Releaser{ + config: &config.Options{ + PackagePath: "testdata/release-packages", + PackagesWithIndex: false, + }, + }, + false, + false, + }, + { + "valid-package-with-index", + "test-chart", + "0.1.0", + &Releaser{ + config: &config.Options{ + PackagePath: "testdata/release-packages", + PackagesWithIndex: true, + }, + }, + true, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + indexFile := repo.NewIndexFile() + url := fmt.Sprintf("https://myrepo/charts/%s-%s.tgz", tt.chart, tt.version) + err := tt.releaser.addToIndexFile(indexFile, url) + if tt.error { + assert.Error(t, err) + assert.False(t, indexFile.Has(tt.chart, tt.version)) + } else { + assert.True(t, indexFile.Has(tt.chart, tt.version)) + + indexEntry, _ := indexFile.Get(tt.chart, tt.version) + if tt.packagesWithIndex { + assert.Equal(t, filepath.Base(url), indexEntry.URLs[0]) + } else { + assert.Equal(t, url, indexEntry.URLs[0]) + } + } + }) + } +} + +func TestReleaser_CreateReleases(t *testing.T) { + tests := []struct { + name string + packagePath string + chart string + version string + commit string + latest string + prerelease bool + Releaser *Releaser + error bool + }{ + { + name: "invalid-package-path", + packagePath: "testdata/does-not-exist", + chart: "test-chart", + version: "0.1.0", + commit: "", + latest: "true", + Releaser: &Releaser{ + config: &config.Options{ + PackagePath: "testdata/does-not-exist", + Commit: "", + PackagesWithIndex: false, + MakeReleaseLatest: true, + Prerelease: false, + }, + }, + error: true, + }, + { + name: "valid-package-path", + packagePath: "testdata/release-packages", + chart: "test-chart", + version: "0.1.0", + commit: "", + latest: "true", + Releaser: &Releaser{ + config: &config.Options{ + PackagePath: "testdata/release-packages", + Commit: "", + PackagesWithIndex: false, + MakeReleaseLatest: true, + Prerelease: false, + }, + }, + error: false, + }, + { + name: "valid-package-path-with-commit", + packagePath: "testdata/release-packages", + chart: "test-chart", + version: "0.1.0", + commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c", + latest: "true", + Releaser: &Releaser{ + config: &config.Options{ + PackagePath: "testdata/release-packages", + Commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c", + PackagesWithIndex: false, + MakeReleaseLatest: true, + Prerelease: false, + }, + }, + error: false, + }, + { + name: "valid-package-with-index", + packagePath: "testdata/release-packages", + chart: "test-chart", + version: "0.1.0", + commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c", + latest: "true", + Releaser: &Releaser{ + config: &config.Options{ + PackagePath: "testdata/release-packages", + Commit: "5e239bd19fbefb9eb0181ecf0c7ef73b8fe2753c", + PackagesWithIndex: true, + Push: true, + MakeReleaseLatest: true, + Prerelease: false, + }, + }, + error: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeGitHub := new(FakeGitHub) + fakeGitHub.On("CreateRelease", mock.Anything, mock.Anything).Return(nil) + tt.Releaser.github = fakeGitHub + fakeGit := new(FakeGit) + fakeGit.On("AddWorktree", mock.Anything, mock.Anything).Return("/tmp/chart-releaser-012345678", nil) + fakeGit.On("RemoveWorktree", mock.Anything, mock.Anything).Return(nil) + fakeGit.On("Add", mock.Anything, mock.Anything).Return(nil) + fakeGit.On("Commit", mock.Anything, mock.Anything).Return(nil) + fakeGit.On("Push", mock.Anything, mock.Anything).Return(nil) + pushURL := fmt.Sprintf("https://x-access-token:%s@github.com/owner/repo", tt.Releaser.config.Token) + fakeGit.On("GetPushURL", mock.Anything, mock.Anything).Return(pushURL, nil) + tt.Releaser.git = fakeGit + tt.Releaser.config.ReleaseNameTemplate = "{{ .Name }}-{{ .Version }}" + err := tt.Releaser.CreateReleases() + if tt.error { + assert.Error(t, err) + assert.Nil(t, fakeGitHub.release) + fakeGitHub.AssertNumberOfCalls(t, "CreateRelease", 0) + } else { + assert.NoError(t, err) + releaseName := fmt.Sprintf("%s-%s", tt.chart, tt.version) + assetPath := fmt.Sprintf("%s/%s-%s.tgz", tt.Releaser.config.PackagePath, tt.chart, tt.version) + releaseDescription := "A Helm chart for Kubernetes" + assert.Equal(t, releaseName, fakeGitHub.release.Name) + assert.Equal(t, releaseDescription, fakeGitHub.release.Description) + assert.Len(t, fakeGitHub.release.Assets, 1) + assert.Equal(t, assetPath, fakeGitHub.release.Assets[0].Path) + assert.Equal(t, tt.commit, fakeGitHub.release.Commit) + assert.Equal(t, tt.latest, fakeGitHub.release.MakeLatest) + assert.Equal(t, tt.prerelease, fakeGitHub.release.Prerelease) + assert.Equal(t, tt.Releaser.config.Commit, fakeGitHub.release.Commit) + fakeGitHub.AssertNumberOfCalls(t, "CreateRelease", 1) + } + }) + } +} + +func TestReleaser_ReleaseNotes(t *testing.T) { + tests := []struct { + name string + packagePath string + chart string + version string + releaseNotesFile string + expectedReleaseNotes string + }{ + { + "chart-package-with-release-notes-file", + "testdata/release-packages", + "test-chart", + "0.1.0", + "release-notes.md", + "The release notes file content is used as release notes", + }, + { + "chart-package-with-non-exists-release-notes-file", + "testdata/release-packages", + "test-chart", + "0.1.0", + "non-exists-release-notes.md", + "A Helm chart for Kubernetes", + }, + { + "chart-package-with-empty-release-notes-file-config-value", + "testdata/release-packages", + "test-chart", + "0.1.0", + "", + "A Helm chart for Kubernetes", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeGitHub := new(FakeGitHub) + fakeGit := new(FakeGit) + r := &Releaser{ + config: &config.Options{ + PackagePath: "testdata/release-packages", + ReleaseNotesFile: tt.releaseNotesFile, + }, + github: fakeGitHub, + git: fakeGit, + } + fakeGit.On("AddWorktree", mock.Anything, mock.Anything).Return("/tmp/chart-releaser-012345678", nil) + fakeGit.On("RemoveWorktree", mock.Anything, mock.Anything).Return(nil) + fakeGitHub.On("CreateRelease", mock.Anything, mock.Anything).Return(nil) + err := r.CreateReleases() + assert.NoError(t, err) + assert.Equal(t, tt.expectedReleaseNotes, fakeGitHub.release.Description) + }) + } +} diff --git a/pkg/releaser/testdata/empty-repo/index.yaml b/pkg/releaser/testdata/empty-repo/index.yaml new file mode 100644 index 00000000..7e544c2d --- /dev/null +++ b/pkg/releaser/testdata/empty-repo/index.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +entries: + some-other-chart: + - apiVersion: v1 + appVersion: "1.0" + created: "2019-03-29T22:50:44.754424+01:00" + description: A Helm chart for Kubernetes + digest: b61c67a17ac0215b45db5d4a60677d06993c772b1412c2dc32885ef7f49e4264 + name: other-chart + urls: + - other-chart-0.0.1.tgz + version: 0.0.1 +generated: "2019-03-29T22:50:44.751503+01:00" diff --git a/pkg/releaser/testdata/index/index.yaml b/pkg/releaser/testdata/index/index.yaml new file mode 100644 index 00000000..106d43b0 --- /dev/null +++ b/pkg/releaser/testdata/index/index.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +entries: + test-chart: + - apiVersion: v1 + appVersion: "1.0" + created: "2019-03-29T22:50:44.754424+01:00" + description: A Helm chart for Kubernetes + digest: b61c67a17ac0215b45db5d4a60677d06993c772b1412c2dc32885ef7f49e4264 + name: test-chart + urls: + - test-chart-0.1.0.tgz + version: 0.1.0 +generated: "2019-03-29T22:50:44.751503+01:00" diff --git a/pkg/releaser/testdata/release-packages/test-chart-0.1.0.tgz b/pkg/releaser/testdata/release-packages/test-chart-0.1.0.tgz new file mode 100644 index 00000000..9ba6d6c3 Binary files /dev/null and b/pkg/releaser/testdata/release-packages/test-chart-0.1.0.tgz differ diff --git a/pkg/releaser/testdata/repo/index.yaml b/pkg/releaser/testdata/repo/index.yaml new file mode 100644 index 00000000..106d43b0 --- /dev/null +++ b/pkg/releaser/testdata/repo/index.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +entries: + test-chart: + - apiVersion: v1 + appVersion: "1.0" + created: "2019-03-29T22:50:44.754424+01:00" + description: A Helm chart for Kubernetes + digest: b61c67a17ac0215b45db5d4a60677d06993c772b1412c2dc32885ef7f49e4264 + name: test-chart + urls: + - test-chart-0.1.0.tgz + version: 0.1.0 +generated: "2019-03-29T22:50:44.751503+01:00"