diff --git a/.circleci/config.yml b/.circleci/config.yml index 8e83d51017..99f28279e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,17 +6,19 @@ orbs: executors: go: docker: - - image: docker.mirror.hashicorp.services/circleci/golang:1.16 + - image: docker.mirror.hashicorp.services/cimg/go:1.17.5 environment: TEST_RESULTS: /tmp/test-results # path to where test results are saved - CONSUL_VERSION: 1.10.0 # Consul's OSS version to use in tests - CONSUL_ENT_VERSION: 1.10.0+ent # Consul's enterprise version to use in tests - -control-plane-path : &control-plane-path control-plane -acceptance-test-path: &acceptance-test-path charts/consul/test/acceptance -acceptance-framework-path: &acceptance-framework-path charts/consul/test/acceptance/framework + CONSUL_VERSION: 1.11.2 # Consul's OSS version to use in tests + CONSUL_ENT_VERSION: 1.11.2+ent # Consul's enterprise version to use in tests + +control-plane-path: &control-plane-path control-plane +cli-path: &cli-path cli +acceptance-mod-path: &acceptance-mod-path acceptance +acceptance-test-path: &acceptance-test-path acceptance/tests +acceptance-framework-path: &acceptance-framework-path acceptance/framework charts-consul-path: &charts-consul-path charts/consul -helm-gen-path: &helm-gen-path charts/consul/hack/helm-reference-gen +helm-gen-path: &helm-gen-path hack/helm-reference-gen gke-terraform-path: &gke-terraform-path charts/consul/test/terraform/gke eks-terraform-path: &eks-terraform-path charts/consul/test/terraform/eks aks-terraform-path: &aks-terraform-path charts/consul/test/terraform/aks @@ -28,9 +30,9 @@ commands: - run: name: Install gotestsum, kind, kubectl, and helm command: | - wget https://golang.org/dl/go1.16.5.linux-amd64.tar.gz - sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.16.5.linux-amd64.tar.gz - rm go1.16.5.linux-amd64.tar.gz + wget https://golang.org/dl/go1.17.5.linux-amd64.tar.gz + sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.17.5.linux-amd64.tar.gz + rm go1.17.5.linux-amd64.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' >> $BASH_ENV wget https://github.com/gotestyourself/gotestsum/releases/download/v1.6.4/gotestsum_1.6.4_linux_amd64.tar.gz @@ -45,11 +47,10 @@ commands: chmod +x ./kubectl sudo mv ./kubectl /usr/local/bin/kubectl - curl https://baltocdn.com/helm/signing.asc | sudo apt-key add - - sudo apt-get install apt-transport-https --yes - echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list - sudo apt-get update - sudo apt-get install helm + wget https://get.helm.sh/helm-v3.7.0-linux-amd64.tar.gz + tar -zxvf helm-v3.7.0-linux-amd64.tar.gz + sudo mv linux-amd64/helm /usr/local/bin/helm + create-kind-clusters: parameters: version: @@ -69,14 +70,17 @@ commands: type: string consul-k8s-image: type: string - default: "docker.mirror.hashicorp.services/hashicorpdev/consul-k8s-control-plane:latest" + default: "docker.mirror.hashicorp.services/hashicorpdev/consul-k8s-control-plane:$(git rev-parse --short HEAD)" + go-path: + type: string + default: "/home/circleci/.go_workspace" steps: - when: condition: << parameters.failfast >> steps: - run: name: Run acceptance tests - working_directory: charts/consul/test/acceptance/tests + working_directory: *acceptance-test-path no_output_timeout: 2h command: | # Enterprise tests can't run on fork PRs because they require @@ -114,7 +118,7 @@ commands: steps: - run: name: Run acceptance tests - working_directory: charts/consul/test/acceptance/tests + working_directory: *acceptance-test-path no_output_timeout: 2h command: | # Enterprise tests can't run on fork PRs because they require @@ -141,7 +145,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-k8s-modcache-v1-{{ checksum "control-plane/go.mod" }} + - consul-k8s-modcache-v2-{{ checksum "control-plane/go.mod" }} - run: name: go mod download @@ -150,9 +154,9 @@ jobs: # Save go module cache if the go.mod file has changed - save_cache: - key: consul-k8s-modcache-v1-{{ checksum "control-plane/go.mod" }} + key: consul-k8s-modcache-v2-{{ checksum "control-plane/go.mod" }} paths: - - "/go/pkg/mod" + - "/home/circleci/go/pkg/mod" # check go fmt output because it does not report non-zero when there are fmt changes - run: @@ -189,7 +193,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-k8s-modcache-v1-{{ checksum "control-plane/go.mod" }} + - consul-k8s-modcache-v2-{{ checksum "control-plane/go.mod" }} # run go tests with gotestsum - run: @@ -220,7 +224,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-k8s-modcache-v1-{{ checksum "control-plane/go.mod" }} + - consul-k8s-modcache-v2-{{ checksum "control-plane/go.mod" }} # run go tests with gotestsum - run: @@ -259,7 +263,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-k8s-modcache-v1-{{ checksum "control-plane/go.mod" }} + - consul-k8s-modcache-v2-{{ checksum "control-plane/go.mod" }} - run: name: build local working_directory: *control-plane-path @@ -287,6 +291,40 @@ jobs: working_directory: *control-plane-path command: make ci.dev-docker + unit-cli: + executor: go + steps: + - checkout + + # Restore go module cache if there is one + - restore_cache: + keys: + - consul-k8s-cli-modcache-v2-{{ checksum "cli/go.mod" }} + + - run: + name: go mod download + working_directory: *cli-path + command: go mod download + + # Save go module cache if the go.mod file has changed + - save_cache: + key: consul-k8s-cli-modcache-v2-{{ checksum "cli/go.mod" }} + paths: + - "/home/circleci/go/pkg/mod" + + - run: mkdir -p $TEST_RESULTS + + - run: + name: Run tests + working_directory: *cli-path + command: | + gotestsum --junitfile $TEST_RESULTS/gotestsum-report.xml ./... -- -p 4 + + - store_test_results: + path: /tmp/test-results + - store_artifacts: + path: /tmp/test-results + go-fmt-and-vet-acceptance: executor: go steps: @@ -295,23 +333,23 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-acceptance-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: name: go mod download - working_directory: *acceptance-test-path + working_directory: *acceptance-mod-path command: go mod download # Save go module cache if the go.mod file has changed - save_cache: - key: consul-helm-acceptance-modcache-v1-{{ checksum "charts/consul/test/acceptance/go.mod" }} + key: consul-helm-acceptance-modcache-v2-{{ checksum "acceptance/go.mod" }} paths: - - "/go/pkg/mod" + - "/home/circleci/go/pkg/mod" # check go fmt output because it does not report non-zero when there are fmt changes - run: name: check go fmt - working_directory: *acceptance-test-path + working_directory: *acceptance-mod-path command: | files=$(go fmt ./...) if [ -n "$files" ]; then @@ -322,7 +360,7 @@ jobs: - run: name: go vet - working_directory: *acceptance-test-path + working_directory: *acceptance-mod-path command: go vet ./... go-fmt-and-vet-helm-gen: @@ -333,7 +371,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-helm-gen-modcache-v1-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} + - consul-helm-helm-gen-modcache-v2-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} - run: name: go mod download @@ -342,9 +380,9 @@ jobs: # Save go module cache if the go.mod file has changed - save_cache: - key: consul-helm-helm-gen-modcache-v1-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} + key: consul-helm-helm-gen-modcache-v2-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} paths: - - "/go/pkg/mod" + - "/home/circleci/go/pkg/mod" # check go fmt output because it does not report non-zero when there are fmt changes - run: @@ -371,7 +409,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-acceptance-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -394,7 +432,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-helm-gen-modcache-v1-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} + - consul-helm-helm-gen-modcache-v2-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -417,7 +455,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-helm-gen-modcache-v1-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} + - consul-helm-helm-gen-modcache-v2-{{ checksum "charts/consul/hack/helm-reference-gen/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -450,16 +488,16 @@ jobs: - checkout - install-prereqs - create-kind-clusters: - version: "v1.20.7" + version: "v1.22.4" - restore_cache: keys: - - consul-helm-modcache-v2-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: name: go mod download - working_directory: *acceptance-test-path + working_directory: *acceptance-mod-path command: go mod download - save_cache: - key: consul-helm-modcache-v2-{{ checksum "charts/consul/test/acceptance/go.mod" }} + key: consul-helm-modcache-v2-{{ checksum "acceptance/go.mod" }} paths: - ~/.go_workspace/pkg/mod - run: mkdir -p $TEST_RESULTS @@ -482,16 +520,16 @@ jobs: - checkout - install-prereqs - create-kind-clusters: - version: "v1.20.7" + version: "v1.22.4" - restore_cache: keys: - - consul-helm-modcache-v2-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: name: go mod download - working_directory: *acceptance-test-path + working_directory: *acceptance-mod-path command: go mod download - save_cache: - key: consul-helm-modcache-v2-{{ checksum "charts/consul/test/acceptance/go.mod" }} + key: consul-helm-modcache-v2-{{ checksum "acceptance/go.mod" }} paths: - ~/.go_workspace/pkg/mod - run: mkdir -p $TEST_RESULTS @@ -542,12 +580,11 @@ jobs: cleanup-eks-resources: docker: - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.9.0 + - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 steps: - checkout - run: name: cleanup eks resources - working_directory: *charts-consul-path command: | # Assume the role and set environment variables. aws sts assume-role --role-arn "$AWS_ROLE_ARN" --role-session-name "consul-helm-$CIRCLE_BUILD_NUM" --duration-seconds 10800 > assume-role.json @@ -555,8 +592,7 @@ jobs: export AWS_SECRET_ACCESS_KEY="$(jq -r .Credentials.SecretAccessKey assume-role.json)" export AWS_SESSION_TOKEN="$(jq -r .Credentials.SessionToken assume-role.json)" - cd hack/aws-acceptance-test-cleanup - go run ./... -auto-approve + make ci.aws-acceptance-test-cleanup - slack/status: fail_only: true failure_message: "EKS cleanup failed" @@ -564,7 +600,7 @@ jobs: ######################## # ACCEPTANCE TESTS ######################## - acceptance-gke-1-17: + acceptance-gke-1-20: environment: - TEST_RESULTS: /tmp/test-results docker: @@ -607,7 +643,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-acceptance-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -630,7 +666,7 @@ jobs: fail_only: true failure_message: "GKE acceptance tests failed. Check the logs at: ${CIRCLE_BUILD_URL}" - acceptance-aks-1-19: + acceptance-aks-1-21: environment: - TEST_RESULTS: /tmp/test-results docker: @@ -662,7 +698,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-acceptance-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -685,7 +721,7 @@ jobs: fail_only: true failure_message: "AKS acceptance tests failed. Check the logs at: ${CIRCLE_BUILD_URL}" - acceptance-eks-1-18: + acceptance-eks-1-19: environment: - TEST_RESULTS: /tmp/test-results docker: @@ -720,15 +756,10 @@ jobs: echo "export primary_kubeconfig=$primary_kubeconfig" >> $BASH_ENV echo "export secondary_kubeconfig=$secondary_kubeconfig" >> $BASH_ENV - # Change file permissions of the kubecofig files to avoid warnings by helm. - # TODO: remove when https://github.com/terraform-aws-modules/terraform-aws-eks/pull/1114 is merged. - chmod 600 "$primary_kubeconfig" - chmod 600 "$secondary_kubeconfig" - # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-acceptance-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -744,7 +775,7 @@ jobs: name: terraform destroy working_directory: *eks-terraform-path command: | - terraform destroy -auto-approve + terraform destroy -var cluster_count=2 -auto-approve when: always - slack/status: @@ -781,7 +812,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-acceptance-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -804,7 +835,7 @@ jobs: fail_only: true failure_message: "OpenShift acceptance tests failed. Check the logs at: ${CIRCLE_BUILD_URL}" - acceptance-kind-1-21: + acceptance-kind-1-23: environment: - TEST_RESULTS: /tmp/test-results machine: @@ -814,16 +845,16 @@ jobs: - checkout - install-prereqs - create-kind-clusters: - version: "v1.21.1" + version: "v1.23.0" - restore_cache: keys: - - consul-helm-modcache-v2-{{ checksum "charts/consul/test/acceptance/go.mod" }} + - consul-helm-modcache-v2-{{ checksum "acceptance/go.mod" }} - run: name: go mod download - working_directory: *acceptance-test-path + working_directory: *acceptance-mod-path command: go mod download - save_cache: - key: consul-helm-modcache-v2-{{ checksum "charts/consul/test/acceptance/go.mod" }} + key: consul-helm-modcache-v2-{{ checksum "acceptance/go.mod" }} paths: - ~/.go_workspace/pkg/mod - run: mkdir -p $TEST_RESULTS @@ -835,90 +866,20 @@ jobs: path: /tmp/test-results - slack/status: fail_only: true - failure_message: "Acceptance tests against Kind with Kubernetes v1.21 failed. Check the logs at: ${CIRCLE_BUILD_URL}" - - update-helm-charts-index: - docker: - - image: docker.mirror.hashicorp.services/circleci/golang:latest - steps: - - checkout - - run: - name: verify chart version matches tag version - working_directory: *charts-consul-path - command: | - GO111MODULE=on go get github.com/mikefarah/yq/v2 - git_tag=$(echo "${CIRCLE_TAG#v}") - chart_tag=$(yq r Chart.yaml version) - if [ "${git_tag}" != "${chart_tag}" ]; then - echo "chart version (${chart_tag}) did not match git version (${git_tag})" - exit 1 - fi - - run: - name: update helm-charts index - command: | - curl --show-error --silent --fail --user "${CIRCLE_TOKEN}:" \ - -X POST \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -d "{\"branch\": \"master\",\"parameters\":{\"SOURCE_REPO\": \"${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}\",\"SOURCE_TAG\": \"${CIRCLE_TAG}\"}}" \ - "${CIRCLE_ENDPOINT}/${CIRCLE_PROJECT}/pipeline" - - slack/status: - fail_only: true - failure_message: "Failed to trigger an update to the helm charts index. Check the logs at: ${CIRCLE_BUILD_URL}" + failure_message: "Acceptance tests against Kind with Kubernetes v1.22 failed. Check the logs at: ${CIRCLE_BUILD_URL}" workflows: version: 2 test-and-build: jobs: - # Fmt, vet, lint control plane and helm code - - go-fmt-and-vet-control-plane - - lint-control-plane - - go-fmt-and-vet-acceptance - - go-fmt-and-vet-helm-gen - # Unit test control plane - - test-control-plane: - requires: - - go-fmt-and-vet-control-plane - - lint-control-plane - - test-enterprise-control-plane: - filters: - branches: - # Forked pull requests have CIRCLE_BRANCH set to pull/XXX. - ignore: /pull\/[0-9]+/ - requires: - - go-fmt-and-vet-control-plane - - lint-control-plane - # Unit tests for go modules in helm and bats tests for templates - - unit-acceptance-framework: - requires: - - go-fmt-and-vet-acceptance - - unit-helm-gen: - requires: - - go-fmt-and-vet-helm-gen - - validate-helm-gen - - unit-test-helm-templates - # Build control plane binaries - - build-distro: - OS: "freebsd linux windows" - ARCH: "386" - name: build-distros-386 - requires: - - test-control-plane - - test-enterprise-control-plane + # Build this one control-plane binary so that acceptance and acceptance-tproxy will run + # The rest of these CircleCI jobs have been migrated to Github Actions. We need to wait until + # the summer of 2022 for larger puplic Github Action VMs be available before the acceptance tests can + # be moved - build-distro: OS: "darwin freebsd linux solaris windows" ARCH: "amd64" name: build-distros-amd64 - requires: - - test-control-plane - - test-enterprise-control-plane - - build-distro: - OS: "linux" - ARCH: "arm arm64" - name: build-distros-arm-arm64 - requires: - - test-control-plane - - test-enterprise-control-plane - dev-upload-docker: context: consul-ci requires: @@ -927,13 +888,9 @@ workflows: - acceptance: requires: - dev-upload-docker - - unit-test-helm-templates - - unit-acceptance-framework - acceptance-tproxy: requires: - dev-upload-docker - - unit-test-helm-templates - - unit-acceptance-framework nightly-acceptance-tests: triggers: - schedule: @@ -946,29 +903,17 @@ workflows: - cleanup-gcp-resources - cleanup-azure-resources - cleanup-eks-resources - - acceptance-openshift: - requires: - - cleanup-azure-resources - - acceptance-gke-1-17: + # Disable until we can use UBI images. + # - acceptance-openshift: + # requires: + # - cleanup-azure-resources + - acceptance-gke-1-20: requires: - cleanup-gcp-resources - - acceptance-eks-1-18: + - acceptance-eks-1-19: requires: - cleanup-eks-resources - - acceptance-aks-1-19: + - acceptance-aks-1-21: requires: - cleanup-azure-resources - - acceptance-kind-1-21 -# update-helm-charts-index: <-- Disable until we can figure out a release workflow. We currently don't want it to trigger on a tag because the tag is pushed by the eco-releases pipeline. -# jobs: -# - helm-chart-pipeline-approval: -# type: approval -# - update-helm-charts-index: -# requires: -# - helm-chart-pipeline-approval -# context: helm-charts-trigger-consul -# filters: -# tags: -# only: /^v.*/ -# branches: -# ignore: /.*/ + - acceptance-kind-1-23 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 35dac8bb77..7809f17ebe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug Report -about: You're experiencing an issue with the Consul Helm chart or consul-k8s-control-plane binary that is different than the documented behavior. -labels: bug +about: You're experiencing an issue with the Consul Helm chart, consul-k8s CLI, or consul-k8s-control-plane binary that is different than the documented behavior. +labels: type/bug --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 0141ec32d7..3296821690 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature Request about: If you have something you think Consul on Kubernetes could improve or add support for. -labels: enhancement +labels: type/enhancement --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 686b2c22fb..4cb0c8338c 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,18 +1,101 @@ --- name: Question -about: If you have a question. -labels: question +about: You'd like to clarify your understanding about a particular area within Consul on Kubernetes. We'd like to help and engage the community through Github! +labels: type/question --- -Please search the existing issues for relevant questions, and use the reaction feature (https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to add upvotes to pre-existing questions. + + #### Question -Please provide as many details as you can, including but not limited to -- Helm command you're running -- consul-k8s-control-plane command you're running -- Any helm values you've configured -- Your current understanding, and what you're trying to figure out + + +### CLI Commands (consul-k8s, consul-k8s-control-plane, helm) + + + +### Helm Configuration + + + +### Logs + + + +### Current understanding and Expected behavior + + + +### Environment details + + -More details will help us answer questions more accurately and with less delay :) +### Additional Context + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0fe76eff6e..b615e69dd1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,5 +9,7 @@ How I expect reviewers to test this PR: Checklist: - [ ] Tests added -- [ ] CHANGELOG entry added (*HashiCorp engineers only, community PRs should not add a changelog entry*) +- [ ] CHANGELOG entry added + > HashiCorp engineers only, community PRs should not add a changelog entry. + > Entries should use present tense (e.g. Add support for...) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000000..21a9892005 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,300 @@ +name: test-and-build +on: + push: + +env: + TEST_RESULTS: /tmp/test-results # path to where test results are saved + CONSUL_VERSION: 1.11.4 # Consul's OSS version to use in tests + CONSUL_ENT_VERSION: 1.11.4+ent # Consul's enterprise version to use in tests + GOTESTSUM_VERSION: 1.6.4 # You cannot use environment variables with workflows. The gotestsum version is hardcoded in the reusable workflows too. + +jobs: + validate-helm-gen: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: 1.17.2 + + - name: Setup go mod cache + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Validate helm gen + working-directory: hack/helm-reference-gen + run: | + go run ./... -validate + + golangci-lint-helm-gen: + uses: hashicorp/consul-k8s/.github/workflows/reusable-golangci-lint.yml@main + with: + directory: hack/helm-reference-gen + go-version: 1.17.2 + #TODO: This is a workaround in order to get pipelines working. godot and staticcheck fail for helm-reference-gen + args: "--no-config --disable-all --enable gofmt,govet" + + unit-helm-gen: + needs: [golangci-lint-helm-gen, validate-helm-gen] + uses: hashicorp/consul-k8s/.github/workflows/reusable-unit.yml@main + with: + directory: hack/helm-reference-gen + go-version: 1.17.2 + + unit-test-helm-templates: + needs: [unit-helm-gen] + runs-on: ubuntu-latest + container: + image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 + options: --user 1001 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Run Unit Tests + working-directory: charts/consul + run: bats --jobs 4 ./test/unit + + lint-control-plane: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: 1.17.2 + + - run: go get -u github.com/hashicorp/lint-consul-retry && lint-consul-retry + + - name: Run lint + working-directory: control-plane + run: go run hack/lint-api-new-client/main.go + + golangci-lint-control-plane: + uses: hashicorp/consul-k8s/.github/workflows/reusable-golangci-lint.yml@main + with: + directory: control-plane + go-version: 1.17.2 + + test-control-plane: + needs: [lint-control-plane, golangci-lint-control-plane] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: 1.17.2 + + - name: Setup go mod cache + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install gotestsum + run: | + wget https://github.com/gotestyourself/gotestsum/releases/download/v${{env.GOTESTSUM_VERSION}}/gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + sudo tar -C /usr/local/bin -xzf gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + rm gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + + - run: mkdir -p ${{env.TEST_RESULTS}} + - run: echo "$HOME/bin" >> $GITHUB_PATH + + - name: Download consul + working-directory: control-plane + run: | + mkdir -p $HOME/bin + wget https://releases.hashicorp.com/consul/${{env.CONSUL_VERSION}}/consul_${{env.CONSUL_VERSION}}_linux_amd64.zip && \ + unzip consul_${{env.CONSUL_VERSION}}_linux_amd64.zip -d $HOME/bin && \ + rm consul_${{env.CONSUL_VERSION}}_linux_amd64.zip + chmod +x $HOME/bin/consul + + - name: Run go tests + working-directory: control-plane + run: | + PACKAGE_NAMES=$(go list ./...) + gotestsum --junitfile ${{env.TEST_RESULTS}}/gotestsum-report.xml -- -p 4 $PACKAGE_NAMES + + test-enterprise-control-plane: + if: github.repository_owner == 'hashicorp' # Do not run on forks as this requires secrets + needs: [lint-control-plane, golangci-lint-control-plane] + runs-on: ubuntu-latest + env: + CONSUL_LICENSE: ${{secrets.CONSUL_LICENSE}} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: 1.17.2 + + - name: Setup go mod cache + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install gotestsum + run: | + wget https://github.com/gotestyourself/gotestsum/releases/download/v${{env.GOTESTSUM_VERSION}}/gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + sudo tar -C /usr/local/bin -xzf gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + rm gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + + - run: mkdir -p ${{env.TEST_RESULTS}} + - run: echo "$HOME/bin" >> $GITHUB_PATH + + - name: Download consul + working-directory: control-plane + run: | + mkdir -p $HOME/bin + wget https://releases.hashicorp.com/consul/${{env.CONSUL_ENT_VERSION}}/consul_${{env.CONSUL_ENT_VERSION}}_linux_amd64.zip && \ + unzip consul_${{env.CONSUL_ENT_VERSION}}_linux_amd64.zip -d $HOME/bin && \ + rm consul_${{env.CONSUL_ENT_VERSION}}_linux_amd64.zip + chmod +x $HOME/bin/consul + + - name: Run go tests + working-directory: control-plane + run: | + PACKAGE_NAMES=$(go list ./...) + gotestsum --junitfile ${{env.TEST_RESULTS}}/gotestsum-report.xml -- -tags=enterprise -p 4 $PACKAGE_NAMES + + build-distros: + needs: [test-control-plane, test-enterprise-control-plane] + runs-on: ubuntu-latest + strategy: + matrix: + include: + - {go: "1.17.2", goos: "linux", goarch: "386"} + - {go: "1.17.2", goos: "linux", goarch: "amd64"} + - {go: "1.17.2", goos: "linux", goarch: "arm"} + - {go: "1.17.2", goos: "linux", goarch: "arm64"} + fail-fast: true + + name: Go ${{ matrix.go }} ${{ matrix.goos }} ${{ matrix.goarch }} build + steps: + - uses: actions/checkout@v2 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Build + working-directory: control-plane + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 0 + run: | + XC_OS=${{ matrix.goos }} XC_ARCH=${{ matrix.goarch }} ./build-support/scripts/build-local.sh + zip -r -j consul-k8s_${{ matrix.goos }}_${{ matrix.goarch }}.zip bin + + - uses: actions/upload-artifact@v2 + with: + name: consul-k8s_${{ matrix.goos }}_${{ matrix.goarch }}.zip + path: control-plane/consul-k8s_${{ matrix.goos }}_${{ matrix.goarch }}.zip + + golangci-lint-acceptance: + uses: hashicorp/consul-k8s/.github/workflows/reusable-golangci-lint.yml@main + with: + directory: acceptance + go-version: 1.17.2 + + unit-acceptance-framework: + needs: golangci-lint-acceptance + uses: hashicorp/consul-k8s/.github/workflows/reusable-unit.yml@main + with: + directory: acceptance/framework + go-version: 1.17.2 + + golangci-lint-cli: + uses: hashicorp/consul-k8s/.github/workflows/reusable-golangci-lint.yml@main + with: + directory: cli + go-version: 1.17.2 + + unit-cli: + needs: golangci-lint-cli + uses: hashicorp/consul-k8s/.github/workflows/reusable-unit.yml@main + with: + directory: cli + go-version: 1.17.2 + +# Disabling for now until we get faster VMs to run acceptance tests. Faster VMs for Github Actions are supposed +# to be available in the summer of 2022. For now, run the dev-upload docker and acceptance tests in CircleCI +# dev-upload-docker: +# if: github.repository_owner == 'hashicorp' # Do not run on forks as this requires secrets +# needs: build-distros +# runs-on: ubuntu-latest +# +# env: +# GITHUB_PULL_REQUEST: ${{github.event.pull_request.number}} +# DOCKER_USER: ${{secrets.DOCKER_USER}} +# DOCKER_PASS: ${{secrets.DOCKER_PASS}} +# steps: +# - uses: actions/checkout@v2 +# +# - run: mkdir -p control-plane/pkg/bin/linux_amd64 +# +# - uses: actions/download-artifact@v3 +# with: +# name: consul-k8s_linux_amd64.zip +# path: control-plane +# +# - name: Docker build +# working-directory: control-plane +# run: | +# unzip consul-k8s_linux_amd64.zip -d ./pkg/bin/linux_amd64 +# make ci.dev-docker-github +# +# acceptance-tproxy: +# needs: [unit-cli, dev-upload-docker, unit-acceptance-framework, unit-test-helm-templates] +# needs: dev-upload-docker +# uses: hashicorp/consul-k8s/.github/workflows/reusable-acceptance.yml@main +# with: +# name: acceptance-tproxy +# directory: acceptance/tests +# go-version: 1.17.2 +# additional-flags: "-use-kind -kubecontext=kind-dc1 -secondary-kubecontext=kind-dc2 -enable-transparent-proxy" +# gotestsum-version: 1.6.4 +# secrets: +# CONSUL_ENT_LICENSE: ${{ secrets.CONSUL_ENT_LICENSE }} +# +# acceptance: +# #needs: [unit-cli, dev-upload-docker, unit-acceptance-framework, unit-test-helm-templates] +# needs: dev-upload-docker +# uses: hashicorp/consul-k8s/.github/workflows/reusable-acceptance.yml@main +# with: +# name: acceptance +# directory: acceptance/tests +# go-version: 1.17.2 +# additional-flags: "-use-kind -kubecontext=kind-dc1 -secondary-kubecontext=kind-dc2" +# gotestsum-version: 1.6.4 +# secrets: +# CONSUL_ENT_LICENSE: ${{ secrets.CONSUL_ENT_LICENSE }} + + diff --git a/.github/workflows/golangci-lint-acceptance.yml b/.github/workflows/golangci-lint-acceptance.yml deleted file mode 100644 index 1f60c2c19b..0000000000 --- a/.github/workflows/golangci-lint-acceptance.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: golangci-lint-acceptance -on: - push: - tags: - - v* - branches: - - main - pull_request: -jobs: - golangci: - name: lint-acceptance - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: golangci-lint-acceptance - uses: golangci/golangci-lint-action@v2 - with: - version: v1.41.1 - # Optional: working directory, useful for monorepos - # TODO: we may need to modify this when monorepo comes, it could be helpful for when we test - # only control-plane components in a PR, or some other scenario. - # working-directory: somedir - working-directory: charts/consul/test/acceptance diff --git a/.github/workflows/golangci-lint-control-plane.yml b/.github/workflows/golangci-lint-control-plane.yml deleted file mode 100644 index 4112818cb0..0000000000 --- a/.github/workflows/golangci-lint-control-plane.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: golangci-lint-control-plane -on: - push: - tags: - - v* - branches: - - main - pull_request: -jobs: - golangci: - name: lint-control-plane - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: golangci-lint - uses: golangci/golangci-lint-action@v2 - with: - version: v1.41.1 - # Optional: working directory, useful for monorepos - # TODO: we may need to modify this when monorepo comes, it could be helpful for when we test - # only control-plane components in a PR, or some other scenario. - # working-directory: somedir - working-directory: control-plane diff --git a/.github/workflows/issue-context-bot.yml b/.github/workflows/issue-context-bot.yml new file mode 100644 index 0000000000..51f37f7ee9 --- /dev/null +++ b/.github/workflows/issue-context-bot.yml @@ -0,0 +1,24 @@ +name: Issue Context Bot + +on: [issues] + +jobs: + add-context: + runs-on: ubuntu-latest + steps: + - uses: wow-actions/auto-comment@v1.0.7 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + issuesTransferred: | + "Hi @{{ author }}, + + It looks like your issue has been transferred to the Consul on Kubernetes repository. + If your issue is a question or bug report, there is some additional context that will + help us solve your issue. Please reply with the following information if it is not + included in your original issue: + + - the [Helm values](https://www.consul.io/docs/k8s/helm) you used to install Consul + on Kubernetes + - the version of Consul on Kubernetes used + - the version of Kubernetes you are using + - if you installed Consul on Kubernetes using Helm or the Consul-K8s CLI" diff --git a/.github/workflows/reusable-acceptance.yml b/.github/workflows/reusable-acceptance.yml new file mode 100644 index 0000000000..56389bb346 --- /dev/null +++ b/.github/workflows/reusable-acceptance.yml @@ -0,0 +1,128 @@ +name: reusable-acceptance + +on: + workflow_call: + inputs: + name: + required: true + type: string + additional-flags: + required: false + type: string + default: "" + consul-k8s-image: + required: false + type: string + default: docker.mirror.hashicorp.services/hashicorpdev/consul-k8s-control-plane:latest + directory: + required: true + type: string + go-version: + required: true + type: string + gotestsum-version: + required: true + type: string + kind-version: + required: false + type: string + default: "v1.22.4" + secrets: + CONSUL_ENT_LICENSE: + required: true + +# Environment variables can only be used at the step level +env: + TEST_RESULTS: /tmp/test-results # path to where test results are saved + CONSUL_ENT_LICENSE: ${{ secrets.CONSUL_ENT_LICENSE }} + +jobs: + job: + runs-on: ubuntu-latest + strategy: + matrix: + include: # I am really sorry for this but I could not find a way to automatically split our tests into several runners. For now, split manually. + - {runner: "0", test-packages: "basic connect consul-dns"} + - {runner: "1", test-packages: "controller example ingress-gateway"} + - {runner: "2", test-packages: "mesh-gateway metrics"} + - {runner: "3", test-packages: "partitions sync terminating-gateway"} + - {runner: "4", test-packages: "vault"} + + fail-fast: true + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: ${{ inputs.go-version }} + + - name: Setup go mod cache + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install gotestsum + run: | + wget https://github.com/gotestyourself/gotestsum/releases/download/v"${{ inputs.gotestsum-version }}"/gotestsum_"${{ inputs.gotestsum-version }}"_linux_amd64.tar.gz + sudo tar -C /usr/local/bin -xzf gotestsum_"${{ inputs.gotestsum-version }}"_linux_amd64.tar.gz + rm gotestsum_"${{ inputs.gotestsum-version }}"_linux_amd64.tar.gz + + - run: mkdir -p ${{ env.TEST_RESULTS }} + + - name: go mod download + working-directory: ${{ inputs.directory }} + run: go mod download + + - name: Create kind clusters + run: | + kind create cluster --name dc1 --image kindest/node:${{ inputs.kind-version }} + kind create cluster --name dc2 --image kindest/node:${{ inputs.kind-version }} + + # We have to run the tests for each package separately so that we can + # exit early if any test fails (-failfast only works within a single + # package). + - name: Run acceptance tests ${{ matrix.runner }} + working-directory: ${{ inputs.directory }} + if: github.repository_owner == 'hashicorp' # This prevents running on forks + run: | + exit_code=0 + echo "Running packages: ${{ matrix.test-packages }}" + for pkg in $(echo ${{ matrix.test-packages }}) + do + fullpkg="github.com/hashicorp/consul-k8s/${{ inputs.directory }}/${pkg}" + echo "Testing package: ${fullpkg}" + if ! gotestsum --jsonfile=jsonfile-${pkg////-} -- ${fullpkg} -p 1 -timeout 2h -failfast \ + ${{ inputs.additional-flags }} \ + -enable-enterprise \ + -enable-multi-cluster \ + -debug-directory=${{ env.TEST_RESULTS }}/debug \ + -consul-k8s-image=${{ inputs.consul-k8s-image }} + then + echo "Tests in ${pkg} failed, aborting early" + exit_code=1 + break + fi + done + gotestsum --raw-command --junitfile "${{ env.TEST_RESULTS }}/gotestsum-report.xml" -- cat jsonfile* + exit $exit_code + + - name: Upload tests + if: always() + uses: actions/upload-artifact@v2 + with: + name: ${{ inputs.name }}-${{ matrix.test-packages }}-gotestsum-report.xml + path: ${{ env.TEST_RESULTS }}/gotestsum-report.xml + + - name: Upload debug (on failure) + if: failure() + uses: actions/upload-artifact@v2 + with: + name: ${{ inputs.name }}-${{ matrix.test-packages }}-debug-info + path: ${{ env.TEST_RESULTS }}/debug diff --git a/.github/workflows/reusable-golangci-lint.yml b/.github/workflows/reusable-golangci-lint.yml new file mode 100644 index 0000000000..5475e7ae75 --- /dev/null +++ b/.github/workflows/reusable-golangci-lint.yml @@ -0,0 +1,29 @@ +name: golangci-lint + +on: + workflow_call: + inputs: + directory: + required: true + type: string + go-version: + required: true + type: string + args: + required: false + type: string + +jobs: + job: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: golangci-lint-${{inputs.directory}} + uses: golangci/golangci-lint-action@v2 + with: + version: v1.41.1 + working-directory: ${{inputs.directory}} + args: ${{inputs.args}} diff --git a/.github/workflows/reusable-unit.yml b/.github/workflows/reusable-unit.yml new file mode 100644 index 0000000000..1183e1922c --- /dev/null +++ b/.github/workflows/reusable-unit.yml @@ -0,0 +1,57 @@ +name: reusable-unit + +on: + workflow_call: + inputs: + directory: + required: true + type: string + go-version: + required: true + type: string + +# Environment variables can only be used at the step level +env: + TEST_RESULTS: /tmp/test-results # path to where test results are saved + GOTESTSUM_VERSION: 1.6.4 + +jobs: + job: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: ${{inputs.go-version}} + + - name: Setup go mod cache + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install gotestsum + run: | + wget https://github.com/gotestyourself/gotestsum/releases/download/v${{env.GOTESTSUM_VERSION}}/gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + sudo tar -C /usr/local/bin -xzf gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + rm gotestsum_${{env.GOTESTSUM_VERSION}}_linux_amd64.tar.gz + + - run: mkdir -p ${{env.TEST_RESULTS}} + + - name: go mod download + working-directory: ${{inputs.directory}} + run: go mod download + + - name: Run tests + working-directory: ${{inputs.directory}} + run: | + gotestsum --junitfile ${{env.TEST_RESULTS}}/gotestsum-report.xml ./... -- -p 4 + diff --git a/.gitignore b/.gitignore index f7de6f7f94..33b40aed54 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ terraform.tfstate* terraform.tfvars values.dev.yaml +bin/ +pkg/ +.idea/ diff --git a/.golangci.yml b/.golangci.yml index 3b0fa6c309..f96c523906 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,7 +2,9 @@ linters: # enables all defaults + the below, `golangci-lint linters` to see the list of active linters. enable: - gofmt - # TODO: re-enable things as we have master cleaned up vs the defaults + - godot + - govet + # TODO: re-enable things as we have main cleaned up vs the defaults #- stylecheck #- goconst #- prealloc diff --git a/CHANGELOG.md b/CHANGELOG.md index 7db21a5eb9..8643d3dce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,396 @@ ## UNRELEASED +BREAKING CHANGES: +* Helm + * Minimum Kubernetes version supported is 1.19 and now matches what is stated in the `README.md` file. [[GH-1049](https://github.com/hashicorp/consul-k8s/pull/1049)] + +IMPROVEMENTS: +* Control Plane + * Upgrade Docker image Alpine version from 3.14 to 3.15. [[GH-1058](https://github.com/hashicorp/consul-k8s/pull/1058)] +* Helm + * API Gateway: Allow controller to read Kubernetes namespaces in order to determine if route is allowed for gateway. [[GH-1092](https://github.com/hashicorp/consul-k8s/pull/1092)] + +BUG FIXES: +* Helm + * Fix PodSecurityPolicies for clients/mesh gateways when hostNetwork is used. [[GH-1090](https://github.com/hashicorp/consul-k8s/pull/1090)] + +## 0.41.1 (February 24, 2022) + +BUG FIXES: +* Helm + * Support Envoy 1.20.2. [[GH-1051](https://github.com/hashicorp/consul-k8s/pull/1051)] + +## 0.41.0 (February 23, 2022) + +FEATURES: +* Support WAN federation via Mesh Gateways with Vault as the secrets backend. [[GH-1016](https://github.com/hashicorp/consul-k8s/pull/1016),[GH-1025](https://github.com/hashicorp/consul-k8s/pull/1025),[GH-1029](https://github.com/hashicorp/consul-k8s/pull/1029),[GH-1038](https://github.com/hashicorp/consul-k8s/pull/1038)] + * **Note**: To use WAN federation with ACLs and Vault, you will need to create a KV secret in Vault that will serve as the replication token with + a random UUID: `vault kv put secret/consul/replication key="$(uuidgen)"`. + * You will need to then provide this secret to both the primary + and the secondary datacenters with `global.acls.replicationToken` values and allow the `global.secretsBackend.vault.manageSystemACLsRole` Vault role to read it. + In the primary datacenter, the Helm chart will create the replication token in Consul using the UUID as the secret ID of the token. +* Connect: Support workaround for pods with multiple ports, by registering a Consul service and injecting an Envoy sidecar and init container per port. [[GH-1012](https://github.com/hashicorp/consul-k8s/pull/1012)] + * Transparent proxying, metrics, and metrics merging are not supported for multi-port pods. + * Multi-port pods should specify annotations in the format, such that the service names and port names correspond with each other in the specified order, i.e. `web` service is listening on `8080`, `web-admin` service is listening on `9090`. + * `consul.hashicorp.com/connect-service': 'web,web-admin` + * `consul.hashicorp.com/connect-service-port': '8080,9090` + +IMPROVEMENTS: +* Helm + * Vault: Allow passing arbitrary annotations to the vault agent. [[GH-1015](https://github.com/hashicorp/consul-k8s/pull/1015)] + * Vault: Add support for customized IP and DNS SANs for server cert in Vault. [[GH-1020](https://github.com/hashicorp/consul-k8s/pull/1020)] + * Vault: Add support for Enterprise License to be configured in Vault. [[GH-1032](https://github.com/hashicorp/consul-k8s/pull/1032)] + * API Gateway: Allow Kubernetes namespace to Consul enterprise namespace mapping for deployed gateways and mesh services. [[GH-1024](https://github.com/hashicorp/consul-k8s/pull/1024)] + +BUG FIXES: +* API Gateway + * Fix issue where if the API gateway controller pods restarted, gateway pods would become disconnected from the secret discovery service. [[GH-1007](https://github.com/hashicorp/consul-k8s/pull/1007)] + * Fix issue where the API gateway controller could not update existing Deployments or Services. [[GH-1014](https://github.com/hashicorp/consul-k8s/pull/1014)] + * Fix issue where the API gateway controller lacked sufficient permissions to bind routes when ACLs were enabled. [[GH-1018](https://github.com/hashicorp/consul-k8s/pull/1018)] + +BREAKING CHANGES: +* Helm + * Rename fields of IngressGateway CRD to fix incorrect names (`gatewayTLSConfig` => `tls`, `gatewayServiceTLSConfig` => `tls`, `gatewayTLSSDSConfig` => `sds`). [[GH-1017](https://github.com/hashicorp/consul-k8s/pull/1017)] + +## 0.40.0 (January 27, 2022) + +BREAKING CHANGES: +* Helm + * Some Consul components from the Helm chart have been renamed to ensure consistency in naming across the components. + This will not be a breaking change if Consul components are not referred to by name externally. Check the PR for the list of renamed components. [[GH-993](https://github.com/hashicorp/consul-k8s/pull/993)][[GH-1000](https://github.com/hashicorp/consul-k8s/pull/1000)] + +FEATURES: +* Helm + * Support Envoy 1.20.1. [[GH-958](https://github.com/hashicorp/consul-k8s/pull/958)] + * Support Consul 1.11.2. [[GH-976](https://github.com/hashicorp/consul-k8s/pull/976)] + * Support [Consul API Gateway](https://github.com/hashicorp/consul-api-gateway) Controller deployment through the Helm chart and provision an ACL token to for API Gateway via server-acl-init [[GH-925](https://github.com/hashicorp/consul-k8s/pull/925)] + +IMPROVEMENTS: +* Helm + * Allow customization of `terminationGracePeriodSeconds` on the ingress gateways. [[GH-947](https://github.com/hashicorp/consul-k8s/pull/947)] + * Support `ui.dashboardURLTemplates.service` value for setting [dashboard URL templates](https://www.consul.io/docs/agent/options#ui_config_dashboard_url_templates_service). [[GH-937](https://github.com/hashicorp/consul-k8s/pull/937)] + * Allow using dash-separated names for config entries when using `kubectl`. [[GH-965](https://github.com/hashicorp/consul-k8s/pull/965)] + * Support Pod Security Policies with Vault integration. [[GH-985](https://github.com/hashicorp/consul-k8s/pull/985)] + * Rename Consul resources to remove resource kind suffixes from the resource names to standardize resource names across the Helm chart. [[GH-993](https://github.com/hashicorp/consul-k8s/pull/993)] + * Append `-client` to the Consul Daemonset name to standardize resource names across the Helm chart. [[GH-1000](https://github.com/hashicorp/consul-k8s/pull/1000)] +* CLI + * Show a diff when upgrading a Consul installation on Kubernetes [[GH-934](https://github.com/hashicorp/consul-k8s/pull/934)] +* Control Plane + * Support the value `$POD_NAME` for the annotation `consul.hashicorp.com/service-meta-*` that will now be interpolated and set to the pod's name in the service's metadata. [[GH-982](https://github.com/hashicorp/consul-k8s/pull/982)] + * Allow managing Consul sidecar resources via annotations. [[GH-956](https://github.com/hashicorp/consul-k8s/pull/956)] + * Support using a backslash to escape commas in `consul.hashicorp.com/service-tags` annotation. [[GH-983](https://github.com/hashicorp/consul-k8s/pull/983)] + * Avoid making unnecessary calls to Consul in the endpoints controller to improve application startup time when Consul is down. [[GH-779](https://github.com/hashicorp/consul-k8s/issues/779)] + +BUG FIXES: +* Helm + * Add `PodDisruptionBudget` Kind when checking for existing versions so that `helm template` can generate the right version. [[GH-923](https://github.com/hashicorp/consul-k8s/pull/923)] +* Control Plane + * Admin Partitions **(Consul Enterprise only)**: Attach anonymous-policy to the anonymous token from non-default partitions to support DNS queries when the default partition is on a VM. [[GH-966](https://github.com/hashicorp/consul-k8s/pull/966)] + +## 0.39.0 (December 15, 2021) + +FEATURES: +* Helm + * Support Consul 1.11.1. [[GH-935](https://github.com/hashicorp/consul-k8s/pull/935)] + * Support Envoy 1.20.0. [[GH-935](https://github.com/hashicorp/consul-k8s/pull/935)] + * Minimum Kubernetes versions supported is 1.18+. [[GH-935](https://github.com/hashicorp/consul-k8s/pull/935)] +* CLI + * **BETA** Add `upgrade` command to modify Consul installation on Kubernetes. [[GH-898](https://github.com/hashicorp/consul-k8s/pull/898)] + +IMPROVEMENTS: +* Control Plane + * Bump `consul-k8s-control-plane` UBI images for OpenShift to use base image `ubi-minimal:8.5`. [[GH-922](https://github.com/hashicorp/consul-k8s/pull/922)] + * Support the value `$POD_NAME` for the annotation `consul.hashicorp.com/service-tags` that will now be interpolated and set to the pod name. [[GH-931](https://github.com/hashicorp/consul-k8s/pull/931)] + + +## 0.38.0 (December 08, 2021) + +BREAKING CHANGES: +* Control Plane + * Update minimum go version for project to 1.17 [[GH-878](https://github.com/hashicorp/consul-k8s/pull/878)] + * Add boolean metric to merged metrics response `consul_merged_service_metrics_success` to indicate if service metrics + were scraped successfully. [[GH-551](https://github.com/hashicorp/consul-k8s/pull/551)] + +FEATURES: +* Vault as a Secrets Backend: Add support for Vault as a secrets backend for Gossip Encryption, Server TLS certs and Service Mesh TLS certificates, + removing the existing usage of Kubernetes Secrets for the respective secrets. [[GH-904](https://github.com/hashicorp/consul-k8s/pull/904/)] + + See the [Consul Kubernetes and Vault documentation](https://www.consul.io/docs/k8s/installation/vault) + for full install instructions. + + Requirements: + * Consul 1.11+ + * Vault 1.9+ and Vault-K8s 0.14+ must be installed with the Vault Agent Injector enabled (`injector.enabled=true`) + into the Kubernetes cluster that Consul is installed into. + * `global.tls.enableAutoEncryption=true` is required for TLS support. + * If TLS is enabled in Vault, `global.secretsBackend.vault.ca` must be provided and should reference a Kube secret + which holds a copy of the Vault CA cert. + * Add boolean metric to merged metrics response `consul_merged_service_metrics_success` to indicate if service metrics were + scraped successfully. [[GH-551](https://github.com/hashicorp/consul-k8s/pull/551)] +* Helm + * Rename `PartitionExports` CRD to `ExportedServices`. [[GH-902](https://github.com/hashicorp/consul-k8s/pull/902)] + +IMPROVEMENTS: +* CLI + * Pre-check in the `install` command to verify the correct license secret exists when using an enterprise Consul image. [[GH-875](https://github.com/hashicorp/consul-k8s/pull/875)] +* Control Plane + * Add a label "managed-by" to every secret the control-plane creates. Only delete said secrets on an uninstall. [[GH-835](https://github.com/hashicorp/consul-k8s/pull/835)] + * Add support for labeling a Kubernetes service with `consul.hashicorp.com/service-ignore` to prevent services from being registered in Consul. [[GH-858](https://github.com/hashicorp/consul-k8s/pull/858)] +* Helm Chart + * Fail an installation/upgrade if WAN federation and Admin Partitions are both enabled. [[GH-892](https://github.com/hashicorp/consul-k8s/issues/892)] + * Add support for setting `ingressClassName` for UI. [[GH-909](https://github.com/hashicorp/consul-k8s/pull/909)] + * Add partition support to Service Resolver, Service Router and Service Splitter CRDs. [[GH-908](https://github.com/hashicorp/consul-k8s/issues/908)] + +BUG FIXES: +* Control Plane: + * Add a workaround to check that the ACL token is replicated to other Consul servers. [[GH-862](https://github.com/hashicorp/consul-k8s/issues/862)] + * Return 500 on prometheus response if unable to get metrics from Envoy. [[GH-551](https://github.com/hashicorp/consul-k8s/pull/551)] + * Don't include body of failed service metrics calls in merged metrics response. [[GH-551](https://github.com/hashicorp/consul-k8s/pull/551)] +* Helm Chart + * Admin Partitions **(Consul Enterprise only)**: Do not mount Consul CA certs to partition-init job if `externalServers.useSystemRoots` is `true`. [[GH-885](https://github.com/hashicorp/consul-k8s/pull/885)] + +## 0.37.0 (November 18, 2021) + +BREAKING CHANGES: +* Previously [UI metrics](https://www.consul.io/docs/connect/observability/ui-visualization) would be enabled when + `global.metrics=false` and `ui.metrics.enabled=-`. If you are no longer seeing UI metrics, + set `global.metrics=true` or `ui.metrics.enabled=true`. [[GH-841](https://github.com/hashicorp/consul-k8s/pull/841)] +* The `enterpriseLicense` section of the values file has been migrated from being under the `server` stanza to being + under the `global` stanza. Migrating the contents of `server.enterpriseLicense` to `global.enterpriseLicense` will + ensure the license job works. [[GH-856](https://github.com/hashicorp/consul-k8s/pull/856)] +* Consul [streaming](https://www.consul.io/docs/agent/options#use_streaming_backend) is re-enabled by default. + Streaming is broken when using multi-DC federation and Consul versions 1.10.0, 1.10.1, 1.10.2. + If you are using those versions and multi-DC federation, you must upgrade to Consul >= 1.10.3 or set: + + ```yaml + client: + extraConfig: | + {"use_streaming_backend": false} + ``` + + [[GH-851](https://github.com/hashicorp/consul-k8s/pull/851)] + +FEATURES: +* Helm Chart + * Add support for Consul services to utilize Consul DNS for service discovery. Set `dns.enableRedirection` to allow services to + use Consul DNS via the Consul DNS Service. [[GH-833](https://github.com/hashicorp/consul-k8s/pull/833)] +* Control Plane + * Connect: Allow services using Connect to utilize Consul DNS to perform service discovery. [[GH-833](https://github.com/hashicorp/consul-k8s/pull/833)] + +IMPROVEMENTS: +* Control Plane + * TLS: Support PKCS1 and PKCS8 private keys for Consul certificate authority. [[GH-843](https://github.com/hashicorp/consul-k8s/pull/843)] + * Connect: Log a warning when ACLs are enabled and the default service account is used. [[GH-842](https://github.com/hashicorp/consul-k8s/pull/842)] + * Update Service Router, Service Splitter and Ingress Gateway CRD with support for RequestHeaders and ResponseHeaders. [[GH-863](https://github.com/hashicorp/consul-k8s/pull/863)] + * Update Ingress Gateway CRD with partition support for the IngressService and TLS Config. [[GH-863](https://github.com/hashicorp/consul-k8s/pull/863)] +* CLI + * Delete jobs, cluster roles, and cluster role bindings on `uninstall`. [[GH-820](https://github.com/hashicorp/consul-k8s/pull/820)] +* Helm Chart + * Add `component` labels to all resources. [[GH-840](https://github.com/hashicorp/consul-k8s/pull/840)] + * Update Consul version to 1.10.4. [[GH-861](https://github.com/hashicorp/consul-k8s/pull/861)] + * Update Service Router, Service Splitter and Ingress Gateway CRD with support for RequestHeaders and ResponseHeaders. [[GH-863](https://github.com/hashicorp/consul-k8s/pull/863)] + * Update Ingress Gateway CRD with partition support for the IngressService and TLS Config. [[GH-863](https://github.com/hashicorp/consul-k8s/pull/863)] + * Re-enable streaming for Consul clients. [[GH-851](https://github.com/hashicorp/consul-k8s/pull/851)] + +BUG FIXES: +* Control Plane + * ACLs: Fix issue where if one or more servers fail to have their ACL tokens set on the initial run of server-acl-init + then on subsequent re-runs of server-acl-init the tokens are never set. [[GH-825](https://github.com/hashicorp/consul-k8s/issues/825)] + * ACLs: Fix issue where if the number of Consul servers is increased, the new servers are never provisioned + an ACL token. [[GH-677](https://github.com/hashicorp/consul-k8s/issues/677)] + * Fix issue where after a `helm upgrade`, users would see `x509: certificate signed by unknown authority.` + errors when modifying config entry resources. [[GH-837](https://github.com/hashicorp/consul-k8s/pull/837)] +* Helm Chart + * **(Consul Enterprise only)** Error on Helm install if a reserved name is used for the admin partition name or a + Consul destination namespace for connect or catalog sync. [[GH-846](https://github.com/hashicorp/consul-k8s/pull/846)] + * Truncate Persistent Volume Claim names when namespace names are too long. [[GH-799](https://github.com/hashicorp/consul-k8s/pull/799)] + * Fix issue where UI metrics would be enabled when `global.metrics=false` and `ui.metrics.enabled=-`. [[GH-841](https://github.com/hashicorp/consul-k8s/pull/841)] + * Populate the federation secret with the generated Gossip key when `global.gossipEncryption.autoGenerate` is set to true. [[GH-854](https://github.com/hashicorp/consul-k8s/pull/854)] + +## 0.36.0 (November 02, 2021) + +BREAKING CHANGES: +* Helm Chart + * The `kube-system` and `local-path-storage` namespaces are now _excluded_ from connect injection by default on Kubernetes versions >= 1.21. If you wish to enable injection on those namespaces, set `connectInject.namespaceSelector` to `null`. [[GH-726](https://github.com/hashicorp/consul-k8s/pull/726)] + +IMPROVEMENTS: +* Helm Chart + * Automatic retry for `gossip-encryption-autogenerate-job` on failure [[GH-789](https://github.com/hashicorp/consul-k8s/pull/789)] + * `kube-system` and `local-path-storage` namespaces are now excluded from connect injection by default on Kubernetes versions >= 1.21. This prevents deadlock issues when `kube-system` components go down and allows Kind to work without changing the failure policy of the mutating webhook. [[GH-726](https://github.com/hashicorp/consul-k8s/pull/726)] + * Add support for services across Admin Partitions to communicate using mesh gateways. [[GH-807](https://github.com/hashicorp/consul-k8s/pull/807)] + * Documentation for the installation can be found [here](https://github.com/hashicorp/consul-k8s/blob/main/docs/admin-partitions-with-acls.md). + * Add support for PartitionExports CRD to enable cross-partition networking. [[GH-802](https://github.com/hashicorp/consul-k8s/pull/802)] +* CLI + * Add `status` command. [[GH-768](https://github.com/hashicorp/consul-k8s/pull/768)] + * Add `-verbose`, `-v` flag to the `consul-k8s install` command, which outputs all logs emitted from the installation. By default, verbose is set to `false` to hide logs that show resources are not ready. [[GH-810](https://github.com/hashicorp/consul-k8s/pull/810)] + * Set `prometheus.enabled` to true and enable all metrics for Consul K8s when installing via the `demo` preset. [[GH-809](https://github.com/hashicorp/consul-k8s/pull/809)] + * Set `controller.enabled` to `true` when installing via the `demo` preset. [[GH818](https://github.com/hashicorp/consul-k8s/pull/818)] + * Set `global.gossipEncryption.autoGenerate` to `true` and `global.tls.enableAutoEncrypt` to `true` when installing via the `secure` preset. [[GH818](https://github.com/hashicorp/consul-k8s/pull/818)] +* Control Plane + * Add support for partition-exports config entry as a Custom Resource Definition to help manage cross-partition networking. [[GH-802](https://github.com/hashicorp/consul-k8s/pull/802)] + +## 0.35.0 (October 19, 2021) + +FEATURES: +* Control Plane + * Add `gossip-encryption-autogenerate` subcommand to generate a random 32 byte Kubernetes secret to be used as a gossip encryption key. [[GH-772](https://github.com/hashicorp/consul-k8s/pull/772)] + * Add support for `partition-exports` config entry. [[GH-802](https://github.com/hashicorp/consul-k8s/pull/802)], [[GH-803](https://github.com/hashicorp/consul-k8s/pull/803)] +* Helm Chart + * Add automatic generation of gossip encryption with `global.gossipEncryption.autoGenerate=true`. [[GH-738](https://github.com/hashicorp/consul-k8s/pull/738)] + * Add support for configuring resources for mesh gateway `service-init` container. [[GH-758](https://github.com/hashicorp/consul-k8s/pull/758)] + * Add support for `PartitionExports` CRD. [[GH-802](https://github.com/hashicorp/consul-k8s/pull/802)], [[GH-803](https://github.com/hashicorp/consul-k8s/pull/803)] + +IMPROVEMENTS: +* Control Plane + * Upgrade Docker image Alpine version from 3.13 to 3.14. [[GH-737](https://github.com/hashicorp/consul-k8s/pull/737)] + * CRDs: tune failure backoff so invalid config entries are re-synced more quickly. [[GH-788](https://github.com/hashicorp/consul-k8s/pull/788)] +* Helm Chart + * Enable adding extra containers to server and client Pods. [[GH-749](https://github.com/hashicorp/consul-k8s/pull/749)] + * ACL support for Admin Partitions. **(Consul Enterprise only)** + **BETA** [[GH-766](https://github.com/hashicorp/consul-k8s/pull/766)] + * This feature now enabled ACL support for Admin Partitions. The server-acl-init job now creates a Partition token. This token + can be used to bootstrap new partitions as well as manage ACLs in the non-default partitions. + * Partition to partition networking is disabled if ACLs are enabled. + * Documentation for the installation can be found [here](https://github.com/hashicorp/consul-k8s/blob/main/docs/admin-partitions-with-acls.md). +* CLI + * Add `version` command. [[GH-741](https://github.com/hashicorp/consul-k8s/pull/741)] + * Add `uninstall` command. [[GH-725](https://github.com/hashicorp/consul-k8s/pull/725)] + +## 0.34.1 (September 17, 2021) + +BUG FIXES: +* Helm + * Fix consul-k8s image version in values file. [[GH-732](https://github.com/hashicorp/consul-k8s/pull/732)] + +## 0.34.0 (September 17, 2021) + +FEATURES: +* CLI + * The `consul-k8s` CLI enables users to deploy and operate Consul on Kubernetes. + * Support `consul-k8s install` command. [[GH-713](https://github.com/hashicorp/consul-k8s/pull/713)] +* Helm Chart + * Add support for Admin Partitions. **(Consul Enterprise only)** + **ALPHA** [[GH-729](https://github.com/hashicorp/consul-k8s/pull/729)] + * This feature allows Consul to be deployed across multiple Kubernetes clusters while sharing a single set of Consul +servers. The services on each cluster can be independently managed. This feature is an alpha feature. It requires: + * a flat pod and node network in order for inter-partition networking to work. + * TLS to be enabled. + * Consul Namespaces enabled. + + Transparent Proxy is unsupported for cross partition communication. + +To enable Admin Partitions on the server cluster use the following config. + +```yaml +global: + enableConsulNamespaces: true + tls: + enabled: true + image: hashicorp/consul-enterprise:1.11.0-ent-alpha + adminPartitions: + enabled: true +server: + exposeGossipAndRPCPorts: true + enterpriseLicense: + secretName: license + secretKey: key +connectInject: + enabled: true + transparentProxy: + defaultEnabled: false + consulNamespaces: + mirroringK8S: true +controller: + enabled: true +``` + +Identify the LoadBalancer External IP of the `partition-service` + +```bash +kubectl get svc consul-consul-partition-service -o json | jq -r '.status.loadBalancer.ingress[0].ip' +``` + +Migrate the TLS CA credentials from the server cluster to the workload clusters + +```bash +kubectl get secret consul-consul-ca-key --context "server-context" -o yaml | kubectl apply --context "workload-context" -f - +kubectl get secret consul-consul-ca-cert --context "server-context" -o yaml | kubectl apply --context "workload-context" -f - +``` + +Configure the workload cluster using the following config. + +```yaml +global: + enabled: false + enableConsulNamespaces: true + image: hashicorp/consul-enterprise:1.11.0-ent-alpha + adminPartitions: + enabled: true + name: "alpha" # Name of Admin Partition + tls: + enabled: true + caCert: + secretName: consul-consul-ca-cert + secretKey: tls.crt + caKey: + secretName: consul-consul-ca-key + secretKey: tls.key +server: + enterpriseLicense: + secretName: license + secretKey: key +externalServers: + enabled: true + hosts: [ "loadbalancer IP" ] # external IP of partition service LB + tlsServerName: server.dc1.consul +client: + enabled: true + exposeGossipPorts: true + join: [ "loadbalancer IP" ] # external IP of partition service LB +connectInject: + enabled: true + consulNamespaces: + mirroringK8S: true +controller: + enabled: true +``` + +This should lead to the workload cluster having only Consul agents that connect with the Consul server. Services in this +cluster behave like independent services. They can be configured to communicate with services in other partitions by +configuring the upstream configuration on the individual services. + +* Control Plane + * Add support for Admin Partitions. **(Consul Enterprise only)** ** + ALPHA** [[GH-729](https://github.com/hashicorp/consul-k8s/pull/729)] + * Add Partition-Init job that runs in Kubernetes clusters that do not have servers running to provision Admin + Partitions. + * Update endpoints-controller, config-entry controller and config entries to add partition config to them. + IMPROVEMENTS: * Helm Chart * Add ability to specify port for ui service. [[GH-604](https://github.com/hashicorp/consul-k8s/pull/604)] * Use `policy/v1` for Consul server `PodDisruptionBudget` if supported. [[GH-606](https://github.com/hashicorp/consul-k8s/pull/606)] - * Added readiness and liveness checks to the connect inject deployment. [[GH-626](https://github.com/hashicorp/consul-k8s/pull/626)] + * Add readiness, liveness and startup probes to the connect inject deployment. [[GH-626](https://github.com/hashicorp/consul-k8s/pull/626)][[GH-701](https://github.com/hashicorp/consul-k8s/pull/701)] * Add support for setting container security contexts on client and server Pods. [[GH-620](https://github.com/hashicorp/consul-k8s/pull/620)] + * Update Envoy image to 1.18.4 [[GH-699](https://github.com/hashicorp/consul-k8s/pull/699)] + * Add configuration for webhook-cert-manager tolerations [[GH-712](https://github.com/hashicorp/consul-k8s/pull/712)] + * Update default Consul version to 1.10.2 [[GH-718](https://github.com/hashicorp/consul-k8s/pull/718)] * Control Plane - * Added health endpoint to the connect inject webhook that will be healthy when webhook certs are present and not empty. [[GH-626](https://github.com/hashicorp/consul-k8s/pull/626)] + * Add health endpoint to the connect inject webhook that will be healthy when webhook certs are present and not empty. [[GH-626](https://github.com/hashicorp/consul-k8s/pull/626)] * Catalog Sync: Fix issue registering NodePort services with wrong IPs when a node has multiple IP addresses. [[GH-619](https://github.com/hashicorp/consul-k8s/pull/619)] + * Allow registering the same service in multiple namespaces. [[GH-697](https://github.com/hashicorp/consul-k8s/pull/697)] + +BUG FIXES: +* Helm Chart + * Disable [streaming](https://www.consul.io/docs/agent/options#use_streaming_backend) on Consul clients because it is currently not supported when + doing mesh gateway federation. If you wish to enable it, override the setting using `client.extraConfig`: + ```yaml + client: + extraConfig: | + {"use_streaming_backend": true} + ``` + [[GH-718](https://github.com/hashicorp/consul-k8s/pull/718)] ## 0.33.0 (August 12, 2021) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2628d1e23..f9c6861bdb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,35 @@ -# Contributing - -To build and install the control plane binary `consul-k8s` locally, Go version 1.11.4+ is required because this repository uses go modules and go 1.11.4 introduced changes to checksumming of modules to correct a symlink problem. +# Contributing to Consul on Kubernetes + +1. [Contributing 101](#contributing-101) + 1. [Building and running `consul-k8s-control-plane`](#building-and-running-consul-k8s-control-plane) + 1. [Building and running the `consul-k8s` CLI](#building-and-running-the-consul-k8s-cli) + 1. [Making changes to consul-k8s](#making-changes-to-consul-k8s) + 1. [Running linters locally](#running-linters-locally) + 1. [Rebasing contributions against main](#rebasing-contributions-against-main) +1. [Creating a new CRD](#creating-a-new-crd) + 1. [The Structs](#the-structs) + 1. [Spec Methods](#spec-methods) + 1. [Spec Tests](#spec-tests) + 1. [Controller](#controller) + 1. [Webhook](#webhook) + 1. [Update command.go](#update-commandgo) + 1. [Generating YAML](#generating-yaml) + 1. [Updating consul-helm](#updating-consul-helm) + 1. [Testing a new CRD](#testing-a-new-crd) + 1. [Update Consul K8s acceptance tests](#update-consul-k8s-acceptance-tests) +1. [Adding a new ACL Token](#adding-a-new-acl-token) +1. [Testing the Helm chart](#testing-the-helm-chart) + 1. [Running the tests](#running-the-tests) + 1. [Writing Unit tests](#writing-unit-tests) + 1. [Writing Acceptance tests](#writing-acceptance-tests) +1. [Helm Reference Docs](#helm-reference-docs) + + +## Contributing 101 + +### Building and running `consul-k8s-control-plane` + +To build and install the control plane binary `consul-k8s-control-plane` locally, Go version 1.17.0+ is required. You will also need to install the Docker engine: - [Docker for Mac](https://docs.docker.com/engine/installation/mac/) @@ -10,48 +39,113 @@ You will also need to install the Docker engine: Clone the repository: ```shell -$ git clone https://github.com/hashicorp/consul-k8s.git +git clone https://github.com/hashicorp/consul-k8s.git ``` -Change directories into the appropriate folder: +Compile the `consul-k8s-control-plane` binary for your local machine: ```shell -$ cd control-plane +make control-plane-dev ``` -To compile the `consul-k8s` binary for your local machine: +This will compile the `consul-k8s-control-plane` binary into `control-plane/bin/consul-k8s-control-plane` as +well as your `$GOPATH` and run the test suite. + +Run the tests: ```shell -$ make dev +make control-plane-test ``` -This will compile the `consul-k8s` binary into `bin/consul-k8s` as -well as your `$GOPATH` and run the test suite. +Run a specific test in the suite. Change directory into `control-plane`. + +```shell +go test ./... -run SomeTestFunction_name +``` -Or run the following to generate all binaries: +To create a docker image with your local changes: ```shell -$ make dist +make control-plane-dev-docker ``` -If you just want to run the tests: +To use your Docker image in a dev deployment of Consul K8s, push the image to Docker Hub or your own registry. Deploying from local images is not supported. + +``` +docker tag consul-k8s-control-plane-dev /consul-k8s-control-plane-dev +docker push /consul-k8s-control-plane-dev +``` + +Create a `values.dev.yaml` file that includes the `global.imageK8S` flag to point to dev images you just pushed: + +```yaml +global: + tls: + enabled: true + imageK8S: /consul-k8s-control-plane-dev +server: + replicas: 1 +connectInject: + enabled: true +ui: + enabled: true + service: + enabled: true +controller: + enabled: true +``` + +Run a `helm install` from the project root directory to target your dev version of the Helm chart. ```shell -$ make test +helm install consul --create-namespace -n consul -f ./values.dev.yaml ./charts/consul ``` -Or to run a specific test in the suite: +### Building and running the `consul-k8s` CLI + +Change directory into the `cli` folder where the golang code resides. ```shell -go test ./... -run SomeTestFunction_name +cd cli ``` -To create a docker image with your local changes: +Build the CLI binary using the following command ```shell -$ make dev-docker +go build -o bin/consul-k8s ``` +Run the CLI as follows + +```shell +./bin/consul-k8s version +consul-k8s 0.36.0-dev +``` + +### Making changes to consul-k8s + +The first step to making changes is to fork Consul K8s. Afterwards, the easiest way +to work on the fork is to set it as a remote of the Consul K8s project: + +1. Rename the existing remote's name: `git remote rename origin upstream`. +1. Add your fork as a remote by running + `git remote add origin `. For example: + `git remote add origin https://github.com/myusername/consul-k8s`. +1. Checkout a feature branch: `git checkout -t -b new-feature` +1. Make changes (i.e. `git commit -am 'message'`) +1. Push changes to the fork when ready to submit PR: + `git push -u origin new-feature` + +>Note: If you make any changes to the code, run `gofmt -s -w` to automatically format the code according to Go standards. + +### Running linters locally +[`golangci-lint`](https://golangci-lint.run/) is used in CI to enforce coding and style standards and help catch bugs ahead of time. +The configuration that CI runs is stored in `.golangci.yml` at the top level of the repository. +Please ensure your code passes by running `golangci-lint run` at the top level of the repository and addressing +any issues prior to submitting a PR. + +Version 1.41.1 or higher of [`golangci-lint`](https://github.com/golangci/golangci-lint/releases/tag/v1.41.1) is currently required. + ### Rebasing contributions against main PRs in this repo are merged using the [`rebase`](https://git-scm.com/docs/git-rebase) method. This keeps @@ -64,6 +158,8 @@ automatic rebasing when the PR is accepted into the code. However, if there are a warning on the PR that reads "This branch cannot be rebased due to conflicts"), you will need to manually rebase the branch on main, fixing any conflicts along the way before the code can be merged. +--- + ## Creating a new CRD ### The Structs @@ -379,15 +475,98 @@ rebase the branch on main, fixing any conflicts along the way before the code ca } ``` -### Update consul-helm Acceptance Tests -1. Add a test resource to `test/acceptance/tests/fixtures/crds/ingressgateway.yaml`. Ideally it requires +### Update consul-k8s Acceptance Tests +1. Add a test resource to `acceptance/tests/fixtures/crds/ingressgateway.yaml`. Ideally it requires no other resources. For example, I used a `tcp` service so it didn't require a `ServiceDefaults` resource to set its protocol to something else. -1. Update `charts/consul/test/acceptance/tests/controller/controller_test.go` and `charts/consul/test/acceptance/tests/controller/controller_namespaces_test.go`. +1. Update `acceptance/tests/controller/controller_test.go` and `acceptance/tests/controller/controller_namespaces_test.go`. 1. Test locally, then submit a PR that uses your Docker image as `global.imageK8S`. - -## Testing Helm Chart +--- + +## Adding a new ACL Token + +Checklist for getting server-acl-init to generate a new ACL token. The examples in this checklist use +a token named `foo`. + +### Control Plane + +* `control-plane/subcommand/server-acl-init/command.go` + * Add `flagCreateFooToken bool` to vars list + * Initialize flag in `init` + + ```go + c.flags.BoolVar(&c.flagCreateFooToken, "create-foo-token", false, + "") + ``` + * Add `if` statement in `Run` to create your token (follow placement of other tokens). + You'll need to decide if you need a local token (use `createLocalACL()`) or a global token (use `createGlobalACL()`). + + ```go + if c.flagCreateFooToken { + err := c.createLocalACL("foo", fooRules, consulDC, isPrimary, consulClient) + if err != nil { + c.log.Error(err.Error()) + return 1 + } + } + ``` +* `control-plane/subcommand/server-acl-init/rules.go` + * Add a function that outputs your rules using a template + (if the rules don't need to be templated just use a `const string`): + ```go + func (c *Command) fooRules() (string, error) { + ``` +* `control-plane/subcommand/server-acl-init/rules_test.go` + * Add test following the pattern of other tests (`TestFooRules`) +* `control-plane/subcommand/server-acl-init/command_test.go` + * Add test cases using your flag to the following tests: + * `TestRun_TokensPrimaryDC` + * `TestRun_TokensReplicatedDC` + * `TestRun_TokensWithProvidedBootstrapToken` + +### Helm + +* `charts/consul/templates/server-acl-init-job.yaml` + * Add conditional to set your flag: + + ```yaml + {{- if .Values.foo.enabled }} + -create-foo-token=true \ + {{- end }} +* `charts/consul/test/unit/server-acl-init-job.bats` + * Test the conditional: + + ```bash + #-------------------------------------------------------------------- + # foo + + @test "serverACLInit/Job: -create-foo-token not set by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("create-foo-token"))' | tee /dev/stderr) + [ "${actual}" = "false" ] + } + + @test "serverACLInit/Job: -create-foo-token set when foo.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'foo.enabled=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("create-foo-token"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + } + ``` + + + + +## Testing the Helm Chart The Helm chart ships with both unit and acceptance tests. The unit tests don't require any active Kubernetes cluster and complete @@ -403,7 +582,7 @@ The acceptance tests require a Kubernetes cluster with a configured `kubectl`. ```bash brew install python-yq ``` -* [Helm 3](https://helm.sh) (Helm 2 is not supported) +* [Helm 3](https://helm.sh) (Currently, must use v3.6.3. Also, Helm 2 is not supported) ```bash brew install kubernetes-helm ``` @@ -432,10 +611,14 @@ To run a specific test by name use the `--filter` flag: bats ./charts/consul/test/unit/.bats --filter "my test name" #### Acceptance Tests - +##### Pre-requisites +* [gox](https://github.com/mitchellh/gox) (v1.14+) + ```bash + brew install gox + ``` To run the acceptance tests: - cd charts/consul/test/acceptance/tests + cd acceptance/tests go test ./... -p 1 The above command will run all tests that can run against a single Kubernetes cluster, @@ -612,14 +795,14 @@ If you are adding a feature that fits thematically with one of the existing test then you need to add your test cases to the existing test files. Otherwise, you will need to create a new test suite. -We recommend to start by either copying the [example test](test/acceptance/tests/example/example_test.go) -or the whole [example test suite](test/acceptance/tests/example), +We recommend to start by either copying the [example test](acceptance/tests/example/example_test.go) +or the whole [example test suite](acceptance/tests/example), depending on the test you need to add. #### Adding Test Suites -To add a test suite, copy the [example test suite](test/acceptance/tests/example) -and uncomment the code you need in the [`main_test.go`](test/acceptance/tests/example/main_test.go) file. +To add a test suite, copy the [example test suite](acceptance/tests/example) +and uncomment the code you need in the [`main_test.go`](acceptance/tests/example/main_test.go) file. At a minimum, this file needs to contain the following: @@ -661,7 +844,7 @@ func TestMain(m *testing.M) { #### Example Test -We recommend using the [example test](test/acceptance/tests/example/example_test.go) +We recommend using the [example test](acceptance/tests/example/example_test.go) as a starting point for adding your tests. To write a test, you need access to the environment and context to run it against. @@ -695,7 +878,7 @@ func TestExample(t *testing.T) { } ``` -Please see [mesh gateway tests](test/acceptance/tests/mesh-gateway/mesh_gateway_test.go) +Please see [mesh gateway tests](acceptance/tests/mesh-gateway/mesh_gateway_test.go) for an example of how to use write a test that uses multiple contexts. #### Writing Assertions @@ -754,32 +937,34 @@ Here are some things to consider before adding a test: For example, we don't expect acceptance tests to include all the permutations of the consul-k8s commands and their respective flags. Something like that should be tested in the consul-k8s repository. +--- + ## Helm Reference Docs -The helm reference docs (https://www.consul.io/docs/k8s/helm) are automatically +The Helm reference docs (https://www.consul.io/docs/k8s/helm) are automatically generated from our `values.yaml` file. ### Generating Helm Reference Docs To generate the docs and update the `helm.mdx` file: -1. Fork `hashicorp/consul` (https://github.com/hashicorp/consul) on GitHub +1. Fork `hashicorp/consul` (https://github.com/hashicorp/consul) on GitHub. 1. Clone your fork: ```shell-session - git clone https://github.com/your-username/consul.git + git clone https://github.com//consul.git ``` -1. Change directory into your `consul-helm` repo: +1. Change directory into your `consul-k8s` repo: ```shell-session - cd /path/to/consul-helm + cd /path/to/consul-k8s ``` -1. Run `make gen-docs` using the path to your consul (not consul-helm) repo: +1. Run `make gen-helm-docs` using the path to your consul (not consul-k8s) repo: ```shell-session - make gen-docs consul= + make gen-helm-docs consul= # Examples: - # make gen-docs consul=/Users/my-name/code/hashicorp/consul - # make gen-docs consul=../consul + # make gen-helm-docs consul=/Users/my-name/code/hashicorp/consul + # make gen-helm-docs consul=../consul ``` -1. Open up a pull request to `hashicorp/consul` (in addition to your `hashicorp/consul-helm` pull request) +1. Open up a pull request to `hashicorp/consul` (in addition to your `hashicorp/consul-k8s` pull request) ### values.yaml Annotations diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..f9de460b05 --- /dev/null +++ b/Makefile @@ -0,0 +1,111 @@ +# ===========> Helm Targets + +gen-helm-docs: ## Generate Helm reference docs from values.yaml and update Consul website. Usage: make gen-helm-docs consul=. + @cd hack/helm-reference-gen; go run ./... $(consul) + +copy-crds-to-chart: ## Copy generated CRD YAML into charts/consul. Usage: make copy-crds-to-chart + @cd hack/copy-crds-to-chart; go run ./... + +bats-tests: ## Run Helm chart bats tests. + bats --jobs 4 charts/consul/test/unit + + + + +# ===========> Control Plane Targets + +control-plane-dev: ## Build consul-k8s-control-plane binary. + @$(SHELL) $(CURDIR)/control-plane/build-support/scripts/build-local.sh -o $(GOOS) -a $(GOARCH) + +control-plane-dev-docker: ## Build consul-k8s-control-plane dev Docker image. + @$(SHELL) $(CURDIR)/control-plane/build-support/scripts/build-local.sh -o linux -a amd64 + @DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build -t '$(DEV_IMAGE)' \ + --build-arg 'GIT_COMMIT=$(GIT_COMMIT)' \ + --build-arg 'GIT_DIRTY=$(GIT_DIRTY)' \ + --build-arg 'GIT_DESCRIBE=$(GIT_DESCRIBE)' \ + -f $(CURDIR)/control-plane/build-support/docker/Dev.dockerfile $(CURDIR)/control-plane + +control-plane-test: ## Run go test for the control plane. + cd control-plane; go test ./... + +control-plane-ent-test: ## Run go test with Consul enterprise tests. The consul binary in your PATH must be Consul Enterprise. + cd control-plane; go test ./... -tags=enterprise + +control-plane-cov: ## Run go test with code coverage. + cd control-plane; go test ./... -coverprofile=coverage.out; go tool cover -html=coverage.out + +control-plane-clean: ## Delete bin and pkg dirs. + @rm -rf \ + $(CURDIR)/control-plane/bin \ + $(CURDIR)/control-plane/pkg + +control-plane-lint: ## Run linter in the control-plane directory. + cd control-plane; golangci-lint run -c ../.golangci.yml + +ctrl-generate: get-controller-gen ## Run CRD code generation. + cd control-plane; $(CONTROLLER_GEN) object:headerFile="build-support/controller/boilerplate.go.txt" paths="./..." + + + + +# ===========> CLI Targets + +cli-lint: ## Run linter in the control-plane directory. + cd cli; golangci-lint run -c ../.golangci.yml + + + + +# ===========> Acceptance Tests Targets + +acceptance-lint: ## Run linter in the control-plane directory. + cd acceptance; golangci-lint run -c ../.golangci.yml + + +# ===========> Shared Targets + +help: ## Show targets and their descriptions. + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +lint: ## Run linter in the control-plane, cli, and acceptance directories. + for p in control-plane cli acceptance; do cd $$p; golangci-lint run --path-prefix $$p -c ../.golangci.yml; cd ..; done + +ctrl-manifests: get-controller-gen ## Generate CRD manifests. + cd control-plane; $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + make copy-crds-to-chart + +get-controller-gen: ## Download controller-gen program needed for operator SDK. +ifeq (, $(shell which controller-gen)) + @{ \ + set -e ;\ + CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ + cd $$CONTROLLER_GEN_TMP_DIR ;\ + go mod init tmp ;\ + go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.6.0 ;\ + rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ + } +CONTROLLER_GEN=$(shell go env GOPATH)/bin/controller-gen +else +CONTROLLER_GEN=$(shell which controller-gen) +endif + +# ===========> CI Targets + +ci.aws-acceptance-test-cleanup: ## Deletes AWS resources left behind after failed acceptance tests. + @cd hack/aws-acceptance-test-cleanup; go run ./... -auto-approve + + + + +# ===========> Makefile config + +.DEFAULT_GOAL := help +.PHONY: gen-helm-docs copy-crds-to-chart bats-tests help ci.aws-acceptance-test-cleanup +SHELL = bash +GOOS?=$(shell go env GOOS) +GOARCH?=$(shell go env GOARCH) +DEV_IMAGE?=consul-k8s-control-plane-dev +GIT_COMMIT?=$(shell git rev-parse --short HEAD) +GIT_DIRTY?=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true) +GIT_DESCRIBE?=$(shell git describe --tags --always) +CRD_OPTIONS ?= "crd:trivialVersions=true,allowDangerousTypes=true" diff --git a/README.md b/README.md index 3934202546..6a25910b9a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -# Consul + Kubernetes (consul-k8s) - -> :warning: **Please note**: This repository has recently been merged with [consul-helm](https://www.consul.io/docs/platform/k8s/index.html). For more information on this change, please see [PR #1051](https://github.com/hashicorp/consul-helm/issues/1051). - ---- +

+ Consul logo + Consul on Kubernetes +

**We're looking for feedback on how folks are using Consul on Kubernetes. Please fill out our brief [survey](https://hashicorp.sjc1.qualtrics.com/jfe/form/SV_4MANbw1BUku7YhL)!** ## Overview -The `consul-k8s` binary includes first-class integrations between Consul and +The `consul-k8s-control-plane` binary includes first-class integrations between Consul and Kubernetes. The project encapsulates multiple use cases such as syncing services, injecting Connect sidecars, and more. The Kubernetes integrations with Consul are @@ -41,18 +40,20 @@ by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com). `consul-k8s` is distributed in multiple forms: * The recommended installation method is the official - [Consul Helm chart](https://github.com/hashicorp/consul-k8s/tree/merge-repos/charts/consul). This will + [Consul Helm chart](https://github.com/hashicorp/consul-k8s/tree/main/charts/consul). This will automatically configure the Consul and Kubernetes integration to run within an existing Kubernetes cluster. - * A [Docker image `hashicorp/consul-k8s`](https://hub.docker.com/r/hashicorp/consul-k8s) is available. This can be used to manually run `consul-k8s` within a scheduled environment. + * A [Docker image `hashicorp/consul-k8s-control-plane`](https://hub.docker.com/r/hashicorp/consul-k8s-control-plane) is available. This can be used to manually run `consul-k8s-control-plane` within a scheduled environment. + + * Consul K8s CLI, distributed as `consul-k8s`, can be used to install and uninstall Consul Kubernetes. See the [Consul K8s CLI Reference](https://www.consul.io/docs/k8s/k8s-cli) for more details on usage. * Raw binaries are available in the [HashiCorp releases directory](https://releases.hashicorp.com/consul-k8s/). These can be used to run `consul-k8s` directly or build custom packages. ## Helm -Within the 'charts/consul' directory is the official HashiCorp Helm chart for installing +Within the ['charts/consul'](charts/consul) directory is the official HashiCorp Helm chart for installing and configuring Consul on Kubernetes. This chart supports multiple use cases of Consul on Kubernetes, depending on the values provided. @@ -61,30 +62,33 @@ use Consul with Kubernetes, please see the [Consul and Kubernetes documentation](https://www.consul.io/docs/platform/k8s/index.html). ### Prerequisites - * **Helm 3.0+** (Helm 2 is not supported) - * **Kubernetes 1.17+** - This is the earliest version of Kubernetes tested. - It is possible that this chart works with earlier versions but it is + * **Helm 3.2+** (Helm 2 is not supported) + * **Kubernetes 1.19+** - This is the earliest version of Kubernetes tested. + It is possible that this chart works with earlier versions, but it is untested. ### Usage Detailed installation instructions for Consul on Kubernetes are found [here](https://www.consul.io/docs/k8s/installation/overview). -1. Add the HashiCorp Helm Repository: +1. Add the HashiCorp Helm repository: + + ``` bash + helm repo add hashicorp https://helm.releases.hashicorp.com + ``` - $ helm repo add hashicorp https://helm.releases.hashicorp.com - "hashicorp" has been added to your repositories - -2. Ensure you have access to the consul chart: - - $ helm search repo hashicorp/consul - NAME CHART VERSION APP VERSION DESCRIPTION - hashicorp/consul 0.33.0 1.10.0 Official HashiCorp Consul Chart +2. Ensure you have access to the Consul Helm chart and you see the latest chart version listed. If you have previously added the + HashiCorp Helm repository, run `helm repo update`. -3. Now you're ready to install Consul! To install Consul with the default configuration using Helm 3 run: + ``` bash + helm search repo hashicorp/consul + ``` - $ helm install consul hashicorp/consul --set global.name=consul - NAME: consul +3. Now you're ready to install Consul! To install Consul with the default configuration using Helm 3.2 run the following command below. + This will create a `consul` Kubernetes namespace if not already present, and install Consul on the dedicated namespace. + + ``` bash + helm install consul hashicorp/consul --set global.name=consul --create-namespace -n consul Please see the many options supported in the `values.yaml` file. These are also fully documented directly on the @@ -93,4 +97,4 @@ file. These are also fully documented directly on the # Tutorials You can find examples and complete tutorials on how to deploy Consul on -Kubernetes using Helm on the [HashiCorp Learn website](https://learn.hashicorp.com/consul). +Kubernetes using Helm on the [HashiCorp Learn website](https://learn.hashicorp.com/collections/consul/kubernetes). diff --git a/charts/consul/test/acceptance/framework/config/config.go b/acceptance/framework/config/config.go similarity index 61% rename from charts/consul/test/acceptance/framework/config/config.go rename to acceptance/framework/config/config.go index 4aab38f7d7..c89947aa9b 100644 --- a/charts/consul/test/acceptance/framework/config/config.go +++ b/acceptance/framework/config/config.go @@ -1,7 +1,6 @@ package config import ( - "errors" "fmt" "io/ioutil" "path/filepath" @@ -14,7 +13,8 @@ import ( // HelmChartPath is the path to the helm chart. // Note: this will need to be changed if this file is moved. const ( - HelmChartPath = "../../../.." + HelmChartPath = "../../../charts/consul" + CLIPath = "../../../cli" LicenseSecretName = "license" LicenseSecretKey = "key" ) @@ -55,11 +55,6 @@ type TestConfig struct { func (t *TestConfig) HelmValuesFromConfig() (map[string]string, error) { helmValues := map[string]string{} - // If Kind is being used they use a pod to provision the underlying PV which will hang if we - // use "Fail" for the webhook failurePolicy. - if t.UseKind { - setIfNotEmpty(helmValues, "connectInject.failurePolicy", "Ignore") - } // Set the enterprise image first if enterprise tests are enabled. // It can be overwritten by the -consul-image flag later. if t.EnableEnterprise { @@ -68,11 +63,11 @@ func (t *TestConfig) HelmValuesFromConfig() (map[string]string, error) { return nil, err } setIfNotEmpty(helmValues, "global.image", entImage) - } - if t.EnterpriseLicense != "" { - setIfNotEmpty(helmValues, "server.enterpriseLicense.secretName", LicenseSecretName) - setIfNotEmpty(helmValues, "server.enterpriseLicense.secretKey", LicenseSecretKey) + if t.EnterpriseLicense != "" { + setIfNotEmpty(helmValues, "global.enterpriseLicense.secretName", LicenseSecretName) + setIfNotEmpty(helmValues, "global.enterpriseLicense.secretKey", LicenseSecretKey) + } } if t.EnableOpenshift { @@ -91,38 +86,56 @@ func (t *TestConfig) HelmValuesFromConfig() (map[string]string, error) { return helmValues, nil } -// entImage parses out consul version from Chart.yaml +type values struct { + Global globalValues `yaml:"global"` +} + +type globalValues struct { + Image string `yaml:"image"` +} + +// entImage parses out consul version from values.yaml // and sets global.image to the consul enterprise image with that version. func (t *TestConfig) entImage() (string, error) { if t.helmChartPath == "" { t.helmChartPath = HelmChartPath } - // Unmarshal Chart.yaml to get appVersion (i.e. Consul version) - chart, err := ioutil.ReadFile(filepath.Join(t.helmChartPath, "Chart.yaml")) + // Unmarshal values.yaml to current global.image value. + valuesContents, err := ioutil.ReadFile(filepath.Join(t.helmChartPath, "values.yaml")) if err != nil { return "", err } - var chartMap map[string]interface{} - err = yaml.Unmarshal(chart, &chartMap) + var v values + err = yaml.Unmarshal(valuesContents, &v) if err != nil { return "", err } - appVersion, ok := chartMap["appVersion"].(string) - if !ok { - return "", errors.New("unable to cast chartMap.appVersion to string") + // Check if the image contains digest instead of a tag. + // If it does, we want to use that image instead rather than + // trying to change the tag to an enterprise tag. + if strings.Contains(v.Global.Image, "@sha256") { + return v.Global.Image, nil } + + // Otherwise, assume that we have an image tag with a version in it. + consulImageSplits := strings.Split(v.Global.Image, ":") + if len(consulImageSplits) != 2 { + return "", fmt.Errorf("could not determine consul version from global.image: %s", v.Global.Image) + } + consulImageVersion := consulImageSplits[1] + var preRelease string // Handle versions like 1.9.0-rc1. - if strings.Contains(appVersion, "-") { - split := strings.Split(appVersion, "-") - appVersion = split[0] + if strings.Contains(consulImageVersion, "-") { + split := strings.Split(consulImageVersion, "-") + consulImageVersion = split[0] preRelease = fmt.Sprintf("-%s", split[1]) } - return fmt.Sprintf("hashicorp/consul-enterprise:%s-ent%s", appVersion, preRelease), nil + return fmt.Sprintf("hashicorp/consul-enterprise:%s-ent%s", consulImageVersion, preRelease), nil } // setIfNotEmpty sets key to val in map m if value is not empty. diff --git a/charts/consul/test/acceptance/framework/config/config_test.go b/acceptance/framework/config/config_test.go similarity index 72% rename from charts/consul/test/acceptance/framework/config/config_test.go rename to acceptance/framework/config/config_test.go index 3e1679be88..feb82ceb38 100644 --- a/charts/consul/test/acceptance/framework/config/config_test.go +++ b/acceptance/framework/config/config_test.go @@ -58,12 +58,15 @@ func TestConfig_HelmValuesFromConfig(t *testing.T) { { "sets ent license secret", TestConfig{ + EnableEnterprise: true, EnterpriseLicense: "ent-license", + ConsulImage: "consul:test-version", }, map[string]string{ - "server.enterpriseLicense.secretName": "license", - "server.enterpriseLicense.secretKey": "key", + "global.enterpriseLicense.secretName": "license", + "global.enterpriseLicense.secretKey": "key", "connectInject.transparentProxy.defaultEnabled": "false", + "global.image": "consul:test-version", }, }, { @@ -109,46 +112,49 @@ func TestConfig_HelmValuesFromConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { values, err := tt.testConfig.HelmValuesFromConfig() require.NoError(t, err) - require.Equal(t, values, tt.want) + require.Equal(t, tt.want, values) }) } } func TestConfig_HelmValuesFromConfig_EntImage(t *testing.T) { tests := []struct { - appVersion string - expImage string - expErr string + consulImage string + expImage string + expErr string }{ { - appVersion: "1.9.0", - expImage: "hashicorp/consul-enterprise:1.9.0-ent", + consulImage: "hashicorp/consul:1.9.0", + expImage: "hashicorp/consul-enterprise:1.9.0-ent", + }, + { + consulImage: "hashicorp/consul:1.8.5-rc1", + expImage: "hashicorp/consul-enterprise:1.8.5-ent-rc1", }, { - appVersion: "1.8.5-rc1", - expImage: "hashicorp/consul-enterprise:1.8.5-ent-rc1", + consulImage: "hashicorp/consul:1.7.0-beta3", + expImage: "hashicorp/consul-enterprise:1.7.0-ent-beta3", }, { - appVersion: "1.7.0-beta3", - expImage: "hashicorp/consul-enterprise:1.7.0-ent-beta3", + consulImage: "invalid", + expErr: "could not determine consul version from global.image: invalid", }, { - appVersion: "1", - expErr: "unable to cast chartMap.appVersion to string", + consulImage: "hashicorp/consul@sha256:oioi2452345kjhlkh", + expImage: "hashicorp/consul@sha256:oioi2452345kjhlkh", }, } for _, tt := range tests { - t.Run(tt.appVersion, func(t *testing.T) { + t.Run(tt.consulImage, func(t *testing.T) { - // Write Chart.yaml to a temp dir which will then get parsed. - chartYAML := fmt.Sprintf(`apiVersion: v1 -name: consul -appVersion: %s -`, tt.appVersion) + // Write values.yaml to a temp dir which will then get parsed. + valuesYAML := fmt.Sprintf(`global: + image: %s +`, tt.consulImage) tmp, err := ioutil.TempDir("", "") require.NoError(t, err) defer os.RemoveAll(tmp) - require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "Chart.yaml"), []byte(chartYAML), 0644)) + require.NoError(t, ioutil.WriteFile(filepath.Join(tmp, "values.yaml"), []byte(valuesYAML), 0644)) cfg := TestConfig{ EnableEnterprise: true, diff --git a/acceptance/framework/consul/cli_cluster.go b/acceptance/framework/consul/cli_cluster.go new file mode 100644 index 0000000000..b62140d783 --- /dev/null +++ b/acceptance/framework/consul/cli_cluster.go @@ -0,0 +1,307 @@ +package consul + +import ( + "context" + "fmt" + "os/exec" + "strings" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/helm" + terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" + terratestLogger "github.com/gruntwork-io/terratest/modules/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + consulNS = "consul" + CLIReleaseName = "consul" +) + +// CLICluster. +type CLICluster struct { + ctx environment.TestContext + namespace string + helmOptions *helm.Options + kubectlOptions *terratestk8s.KubectlOptions + values map[string]string + releaseName string + kubernetesClient kubernetes.Interface + kubeConfig string + kubeContext string + noCleanupOnFailure bool + debugDirectory string + logger terratestLogger.TestLogger +} + +// NewCLICluster creates a new Consul cluster struct which can be used to create +// and destroy a Consul cluster using the Consul K8s CLI. +func NewCLICluster( + t *testing.T, + helmValues map[string]string, + ctx environment.TestContext, + cfg *config.TestConfig, + releaseName string, +) *CLICluster { + // Create the namespace so the PSPs, SCCs, and enterprise secret can be + // created in the right namespace. + createOrUpdateNamespace(t, ctx.KubernetesClient(t), consulNS) + + if cfg.EnablePodSecurityPolicies { + configurePodSecurityPolicies(t, ctx.KubernetesClient(t), cfg, consulNS) + } + + if cfg.EnableOpenshift && cfg.EnableTransparentProxy { + configureSCCs(t, ctx.KubernetesClient(t), cfg, consulNS) + } + + if cfg.EnterpriseLicense != "" { + createOrUpdateLicenseSecret(t, ctx.KubernetesClient(t), cfg, consulNS) + } + + // Deploy with the following defaults unless helmValues overwrites it. + values := defaultValues() + valuesFromConfig, err := cfg.HelmValuesFromConfig() + require.NoError(t, err) + + // Merge all helm values + helpers.MergeMaps(values, valuesFromConfig) + helpers.MergeMaps(values, helmValues) + + logger := terratestLogger.New(logger.TestLogger{}) + + kopts := ctx.KubectlOptions(t) + kopts.Namespace = consulNS + hopts := &helm.Options{ + SetValues: values, + KubectlOptions: kopts, + Logger: logger, + } + + return &CLICluster{ + ctx: ctx, + helmOptions: hopts, + kubectlOptions: kopts, + namespace: cfg.KubeNamespace, + values: values, + releaseName: releaseName, + kubernetesClient: ctx.KubernetesClient(t), + kubeConfig: cfg.Kubeconfig, + kubeContext: cfg.KubeContext, + noCleanupOnFailure: cfg.NoCleanupOnFailure, + debugDirectory: cfg.DebugDirectory, + logger: logger, + } +} + +// Create uses the `consul-k8s install` command to create a Consul cluster. The command itself will fail if there are +// prior installations of Consul in the cluster so it is sufficient to run the install command without a pre-check. +func (c *CLICluster) Create(t *testing.T) { + t.Helper() + + // Make sure we delete the cluster if we receive an interrupt signal and + // register cleanup so that we delete the cluster when test finishes. + helpers.Cleanup(t, c.noCleanupOnFailure, func() { + c.Destroy(t) + }) + + // Set the args for running the install command. + args := []string{"install"} + args = c.setKube(args) + + for k, v := range c.values { + args = append(args, "-set", fmt.Sprintf("%s=%s", k, v)) + } + + // Match the timeout for the helm tests. + args = append(args, "-timeout", "15m") + args = append(args, "-auto-approve") + + out, err := c.runCLI(args) + if err != nil { + c.logger.Logf(t, "error running command `consul-k8s %s`: %s", strings.Join(args, " "), err.Error()) + c.logger.Logf(t, "command stdout: %s", string(out)) + } + require.NoError(t, err) + + k8s.WaitForAllPodsToBeReady(t, c.kubernetesClient, consulNS, fmt.Sprintf("release=%s", c.releaseName)) +} + +// Upgrade uses the `consul-k8s upgrade` command to upgrade a Consul cluster. +func (c *CLICluster) Upgrade(t *testing.T, helmValues map[string]string) { + t.Helper() + + k8s.WritePodsDebugInfoIfFailed(t, c.kubectlOptions, c.debugDirectory, "release="+c.releaseName) + if t.Failed() { + c.logger.Logf(t, "skipping upgrade due to previous failure") + return + } + + // Set the args for running the upgrade command. + args := []string{"upgrade"} + args = c.setKube(args) + + helpers.MergeMaps(c.helmOptions.SetValues, helmValues) + for k, v := range c.helmOptions.SetValues { + args = append(args, "-set", fmt.Sprintf("%s=%s", k, v)) + } + + // Match the timeout for the helm tests. + args = append(args, "-timeout", "15m") + args = append(args, "-auto-approve") + + out, err := c.runCLI(args) + if err != nil { + c.logger.Logf(t, "error running command `consul-k8s %s`: %s", strings.Join(args, " "), err.Error()) + c.logger.Logf(t, "command stdout: %s", string(out)) + } + require.NoError(t, err) + + k8s.WaitForAllPodsToBeReady(t, c.kubernetesClient, consulNS, fmt.Sprintf("release=%s", c.releaseName)) +} + +// Destroy uses the `consul-k8s uninstall` command to destroy a Consul cluster. +func (c *CLICluster) Destroy(t *testing.T) { + t.Helper() + + k8s.WritePodsDebugInfoIfFailed(t, c.kubectlOptions, c.debugDirectory, "release="+c.releaseName) + + // Set the args for running the uninstall command. + args := []string{"uninstall"} + args = c.setKube(args) + args = append(args, "-auto-approve", "-wipe-data") + + // Use `go run` so that the CLI is recompiled and therefore uses the local + // charts directory rather than the directory from whenever it was last + // compiled. + out, err := c.runCLI(args) + if err != nil { + c.logger.Logf(t, "error running command `consul-k8s %s`: %s", strings.Join(args, " "), err.Error()) + c.logger.Logf(t, "command stdout: %s", string(out)) + } + require.NoError(t, err) +} + +func (c *CLICluster) SetupConsulClient(t *testing.T, secure bool) *api.Client { + t.Helper() + + namespace := c.kubectlOptions.Namespace + config := api.DefaultConfig() + localPort := terratestk8s.GetAvailablePort(t) + remotePort := 8500 // use non-secure by default + + if secure { + // Overwrite remote port to HTTPS. + remotePort = 8501 + + // It's OK to skip TLS verification for local traffic. + config.TLSConfig.InsecureSkipVerify = true + config.Scheme = "https" + + // Get the ACL token. First, attempt to read it from the bootstrap token (this will be true in primary Consul servers). + // If the bootstrap token doesn't exist, it means we are running against a secondary cluster + // and will try to read the replication token from the federation secret. + // In secondary servers, we don't create a bootstrap token since ACLs are only bootstrapped in the primary. + // Instead, we provide a replication token that serves the role of the bootstrap token. + + aclSecretName := fmt.Sprintf("%s-consul-bootstrap-acl-token", c.releaseName) + if c.releaseName == CLIReleaseName { + aclSecretName = "consul-bootstrap-acl-token" + } + aclSecret, err := c.kubernetesClient.CoreV1().Secrets(namespace).Get(context.Background(), aclSecretName, metav1.GetOptions{}) + if err != nil && errors.IsNotFound(err) { + federationSecret := fmt.Sprintf("%s-consul-federation", c.releaseName) + if c.releaseName == CLIReleaseName { + federationSecret = "consul-federation" + } + aclSecret, err = c.kubernetesClient.CoreV1().Secrets(namespace).Get(context.Background(), federationSecret, metav1.GetOptions{}) + require.NoError(t, err) + config.Token = string(aclSecret.Data["replicationToken"]) + } else if err == nil { + config.Token = string(aclSecret.Data["token"]) + } else { + require.NoError(t, err) + } + } + + serverPod := fmt.Sprintf("%s-consul-server-0", c.releaseName) + if c.releaseName == CLIReleaseName { + serverPod = "consul-server-0" + } + tunnel := terratestk8s.NewTunnelWithLogger( + c.kubectlOptions, + terratestk8s.ResourceTypePod, + serverPod, + localPort, + remotePort, + c.logger) + + // Retry creating the port forward since it can fail occasionally. + retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 3}, t, func(r *retry.R) { + // NOTE: It's okay to pass in `t` to ForwardPortE despite being in a retry + // because we're using ForwardPortE (not ForwardPort) so the `t` won't + // get used to fail the test, just for logging. + require.NoError(r, tunnel.ForwardPortE(t)) + }) + + t.Cleanup(func() { + tunnel.Close() + }) + + config.Address = fmt.Sprintf("127.0.0.1:%d", localPort) + consulClient, err := api.NewClient(config) + require.NoError(t, err) + + return consulClient +} + +func createOrUpdateNamespace(t *testing.T, client kubernetes.Interface, namespace string) { + _, err := client.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{}) + if errors.IsNotFound(err) { + _, err := client.CoreV1().Namespaces().Create(context.Background(), &v1.Namespace{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + } else { + require.NoError(t, err) + } +} + +// setKube adds the args for KubeConfig and KubeCluster if they have been set on the CLICluster. +func (c *CLICluster) setKube(args []string) []string { + kubeconfig := c.kubeConfig + if kubeconfig != "" { + args = append(args, "-kubeconfig", kubeconfig) + } + + kubecontext := c.kubeContext + if kubecontext != "" { + args = append(args, "-context", kubecontext) + } + + return args +} + +// runCLI runs the CLI with the given args. +// Use `go run` so that the CLI is recompiled and therefore uses the local +// charts directory rather than the directory from whenever it was last compiled. +func (c *CLICluster) runCLI(args []string) ([]byte, error) { + cmd := exec.Command("go", append([]string{"run", "."}, args...)...) + cmd.Dir = config.CLIPath + return cmd.Output() +} diff --git a/acceptance/framework/consul/cluster.go b/acceptance/framework/consul/cluster.go new file mode 100644 index 0000000000..15f4dd7722 --- /dev/null +++ b/acceptance/framework/consul/cluster.go @@ -0,0 +1,32 @@ +package consul + +import ( + "testing" + + "github.com/hashicorp/consul/api" +) + +// Cluster represents a consul cluster object. +type Cluster interface { + // SetupConsulClient returns a new Consul client. + SetupConsulClient(t *testing.T, secure bool) *api.Client + + // Create creates a new Consul Cluster. + Create(t *testing.T) + + // Upgrade modifies the cluster in-place by merging the helm values + // from the initial install with helmValues. Any keys that were previously set + // will be overridden by the helmValues keys. + Upgrade(t *testing.T, helmValues map[string]string) + + // Destroy destroys the cluster + Destroy(t *testing.T) +} + +// ClusterKind represents the kind of Consul cluster being used (e.g. "Helm" or "CLI"). +type ClusterKind int + +const ( + Helm ClusterKind = iota + CLI +) diff --git a/charts/consul/test/acceptance/framework/consul/consul_cluster.go b/acceptance/framework/consul/helm_cluster.go similarity index 77% rename from charts/consul/test/acceptance/framework/consul/consul_cluster.go rename to acceptance/framework/consul/helm_cluster.go index 1d2ec6906e..d1f092d5e5 100644 --- a/charts/consul/test/acceptance/framework/consul/consul_cluster.go +++ b/acceptance/framework/consul/helm_cluster.go @@ -2,7 +2,6 @@ package consul import ( "context" - "encoding/json" "fmt" "strings" "testing" @@ -11,11 +10,11 @@ import ( "github.com/gruntwork-io/terratest/modules/helm" terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" terratestLogger "github.com/gruntwork-io/terratest/modules/logger" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/config" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/environment" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" @@ -27,20 +26,14 @@ import ( "k8s.io/client-go/kubernetes" ) -// Cluster represents a consul cluster object -type Cluster interface { - Create(t *testing.T) - Destroy(t *testing.T) - // Upgrade runs helm upgrade. It will merge the helm values from the - // initial install with helmValues. Any keys that were previously set - // will be overridden by the helmValues keys. - Upgrade(t *testing.T, helmValues map[string]string) - SetupConsulClient(t *testing.T, secure bool) *api.Client -} - // HelmCluster implements Cluster and uses Helm -// to create, destroy, and upgrade consul +// to create, destroy, and upgrade consul. type HelmCluster struct { + // ACLToken is an optional ACL token that will be used to create + // a Consul API client. If not provided, we will attempt to read + // a bootstrap token from a Kubernetes secret stored in the cluster. + ACLToken string + ctx environment.TestContext helmOptions *helm.Options releaseName string @@ -56,8 +49,7 @@ func NewHelmCluster( ctx environment.TestContext, cfg *config.TestConfig, releaseName string, -) Cluster { - +) *HelmCluster { if cfg.EnablePodSecurityPolicies { configurePodSecurityPolicies(t, ctx.KubernetesClient(t), cfg, ctx.KubectlOptions(t).Namespace) } @@ -71,22 +63,13 @@ func NewHelmCluster( } // Deploy with the following defaults unless helmValues overwrites it. - values := map[string]string{ - "server.replicas": "1", - "server.bootstrapExpect": "1", - "connectInject.envoyExtraArgs": "--log-level debug", - "connectInject.logLevel": "debug", - // Disable DNS since enabling it changes the policy for the anonymous token, - // which could result in tests passing due to that token having privileges to read services - // (false positive). - "dns.enabled": "false", - } + values := defaultValues() valuesFromConfig, err := cfg.HelmValuesFromConfig() require.NoError(t, err) // Merge all helm values - mergeMaps(values, valuesFromConfig) - mergeMaps(values, helmValues) + helpers.MergeMaps(values, valuesFromConfig) + helpers.MergeMaps(values, helmValues) logger := terratestLogger.New(logger.TestLogger{}) @@ -124,11 +107,11 @@ func (h *HelmCluster) Create(t *testing.T) { }) // Fail if there are any existing installations of the Helm chart. - h.checkForPriorInstallations(t) + helpers.CheckForPriorInstallations(t, h.kubernetesClient, h.helmOptions, "consul-helm", "chart=consul-helm") helm.Install(t, h.helmOptions, config.HelmChartPath, h.releaseName) - helpers.WaitForAllPodsToBeReady(t, h.kubernetesClient, h.helmOptions.KubectlOptions.Namespace, fmt.Sprintf("release=%s", h.releaseName)) + k8s.WaitForAllPodsToBeReady(t, h.kubernetesClient, h.helmOptions.KubectlOptions.Namespace, fmt.Sprintf("release=%s", h.releaseName)) } func (h *HelmCluster) Destroy(t *testing.T) { @@ -223,9 +206,9 @@ func (h *HelmCluster) Destroy(t *testing.T) { func (h *HelmCluster) Upgrade(t *testing.T, helmValues map[string]string) { t.Helper() - mergeMaps(h.helmOptions.SetValues, helmValues) + helpers.MergeMaps(h.helmOptions.SetValues, helmValues) helm.Upgrade(t, h.helmOptions, config.HelmChartPath, h.releaseName) - helpers.WaitForAllPodsToBeReady(t, h.kubernetesClient, h.helmOptions.KubectlOptions.Namespace, fmt.Sprintf("release=%s", h.releaseName)) + k8s.WaitForAllPodsToBeReady(t, h.kubernetesClient, h.helmOptions.KubectlOptions.Namespace, fmt.Sprintf("release=%s", h.releaseName)) } func (h *HelmCluster) SetupConsulClient(t *testing.T, secure bool) *api.Client { @@ -244,28 +227,34 @@ func (h *HelmCluster) SetupConsulClient(t *testing.T, secure bool) *api.Client { config.TLSConfig.InsecureSkipVerify = true config.Scheme = "https" - // Get the ACL token. First, attempt to read it from the bootstrap token (this will be true in primary Consul servers). - // If the bootstrap token doesn't exist, it means we are running against a secondary cluster - // and will try to read the replication token from the federation secret. - // In secondary servers, we don't create a bootstrap token since ACLs are only bootstrapped in the primary. - // Instead, we provide a replication token that serves the role of the bootstrap token. - aclSecret, err := h.kubernetesClient.CoreV1().Secrets(namespace).Get(context.Background(), h.releaseName+"-consul-bootstrap-acl-token", metav1.GetOptions{}) - if err != nil && errors.IsNotFound(err) { - federationSecret := fmt.Sprintf("%s-consul-federation", h.releaseName) - aclSecret, err = h.kubernetesClient.CoreV1().Secrets(namespace).Get(context.Background(), federationSecret, metav1.GetOptions{}) - require.NoError(t, err) - config.Token = string(aclSecret.Data["replicationToken"]) - } else if err == nil { - config.Token = string(aclSecret.Data["token"]) + // If an ACL token is provided, we'll use that instead of trying to find it. + if h.ACLToken != "" { + config.Token = h.ACLToken } else { - require.NoError(t, err) + // Get the ACL token. First, attempt to read it from the bootstrap token (this will be true in primary Consul servers). + // If the bootstrap token doesn't exist, it means we are running against a secondary cluster + // and will try to read the replication token from the federation secret. + // In secondary servers, we don't create a bootstrap token since ACLs are only bootstrapped in the primary. + // Instead, we provide a replication token that serves the role of the bootstrap token. + aclSecret, err := h.kubernetesClient.CoreV1().Secrets(namespace).Get(context.Background(), h.releaseName+"-consul-bootstrap-acl-token", metav1.GetOptions{}) + if err != nil && errors.IsNotFound(err) { + federationSecret := fmt.Sprintf("%s-consul-federation", h.releaseName) + aclSecret, err = h.kubernetesClient.CoreV1().Secrets(namespace).Get(context.Background(), federationSecret, metav1.GetOptions{}) + require.NoError(t, err) + config.Token = string(aclSecret.Data["replicationToken"]) + } else if err == nil { + config.Token = string(aclSecret.Data["token"]) + } else { + require.NoError(t, err) + } } } + serverPod := fmt.Sprintf("%s-consul-server-0", h.releaseName) tunnel := terratestk8s.NewTunnelWithLogger( h.helmOptions.KubectlOptions, terratestk8s.ResourceTypePod, - fmt.Sprintf("%s-consul-server-0", h.releaseName), + serverPod, localPort, remotePort, h.logger) @@ -289,49 +278,6 @@ func (h *HelmCluster) SetupConsulClient(t *testing.T, secure bool) *api.Client { return consulClient } -// checkForPriorInstallations checks if there is an existing Helm release -// for this Helm chart already installed. If there is, it fails the tests. -func (h *HelmCluster) checkForPriorInstallations(t *testing.T) { - t.Helper() - - var helmListOutput string - // Check if there's an existing cluster and fail if there is one. - // We may need to retry since this is the first command run once the Kube - // cluster is created and sometimes the API server returns errors. - retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 3}, t, func(r *retry.R) { - var err error - // NOTE: It's okay to pass in `t` to RunHelmCommandAndGetOutputE despite being in a retry - // because we're using RunHelmCommandAndGetOutputE (not RunHelmCommandAndGetOutput) so the `t` won't - // get used to fail the test, just for logging. - helmListOutput, err = helm.RunHelmCommandAndGetOutputE(t, h.helmOptions, "list", "--output", "json") - require.NoError(r, err) - }) - - var installedReleases []map[string]string - - err := json.Unmarshal([]byte(helmListOutput), &installedReleases) - require.NoError(t, err, "unmarshalling %q", helmListOutput) - - for _, r := range installedReleases { - require.NotContains(t, r["chart"], "consul", fmt.Sprintf("detected an existing installation of Consul %s, release name: %s", r["chart"], r["name"])) - } - - // Wait for all pods in the "default" namespace to exit. A previous - // release may not be listed by Helm but its pods may still be terminating. - retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 60}, t, func(r *retry.R) { - consulPods, err := h.kubernetesClient.CoreV1().Pods(h.helmOptions.KubectlOptions.Namespace).List(context.Background(), metav1.ListOptions{}) - require.NoError(r, err) - if len(consulPods.Items) > 0 { - var podNames []string - for _, p := range consulPods.Items { - podNames = append(podNames, p.Name) - } - r.Errorf("pods from previous installation still running: %s", strings.Join(podNames, ", ")) - } - }) - -} - // configurePodSecurityPolicies creates a simple pod security policy, a cluster role to allow access to the PSP, // and a role binding that binds the default service account in the helm installation namespace to the cluster role. // We bind the default service account for tests that are spinning up pods without a service account set so that @@ -507,10 +453,20 @@ func configureSCCs(t *testing.T, client kubernetes.Interface, cfg *config.TestCo }) } -// mergeValues will merge the values in b with values in a and save in a. -// If there are conflicts, the values in b will overwrite the values in a. -func mergeMaps(a, b map[string]string) { - for k, v := range b { - a[k] = v +func defaultValues() map[string]string { + values := map[string]string{ + "server.replicas": "1", + "server.bootstrapExpect": "1", + "connectInject.envoyExtraArgs": "--log-level debug", + "connectInject.logLevel": "debug", + // Disable DNS since enabling it changes the policy for the anonymous token, + // which could result in tests passing due to that token having privileges to read services + // (false positive). + "dns.enabled": "false", + + // Enable trace logs for servers and clients. + "server.extraConfig": `"{\"log_level\": \"TRACE\"}"`, + "client.extraConfig": `"{\"log_level\": \"TRACE\"}"`, } + return values } diff --git a/charts/consul/test/acceptance/framework/consul/consul_cluster_test.go b/acceptance/framework/consul/helm_cluster_test.go similarity index 81% rename from charts/consul/test/acceptance/framework/consul/consul_cluster_test.go rename to acceptance/framework/consul/helm_cluster_test.go index bc0bf28386..3fb6710c8b 100644 --- a/charts/consul/test/acceptance/framework/consul/consul_cluster_test.go +++ b/acceptance/framework/consul/helm_cluster_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" @@ -30,6 +30,8 @@ func TestNewHelmCluster(t *testing.T) { "connectInject.logLevel": "debug", "connectInject.transparentProxy.defaultEnabled": "false", "dns.enabled": "false", + "server.extraConfig": `"{\"log_level\": \"TRACE\"}"`, + "client.extraConfig": `"{\"log_level\": \"TRACE\"}"`, }, }, { @@ -42,6 +44,8 @@ func TestNewHelmCluster(t *testing.T) { "connectInject.logLevel": "debug", "connectInject.transparentProxy.defaultEnabled": "true", "dns.enabled": "true", + "server.extraConfig": `"{\"foo\": \"bar\"}"`, + "client.extraConfig": `"{\"foo\": \"bar\"}"`, "feature.enabled": "true", }, want: map[string]string{ @@ -52,6 +56,8 @@ func TestNewHelmCluster(t *testing.T) { "connectInject.logLevel": "debug", "connectInject.transparentProxy.defaultEnabled": "true", "dns.enabled": "true", + "server.extraConfig": `"{\"foo\": \"bar\"}"`, + "client.extraConfig": `"{\"foo\": \"bar\"}"`, "feature.enabled": "true", }, }, @@ -59,7 +65,7 @@ func TestNewHelmCluster(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cluster := NewHelmCluster(t, tt.helmValues, &ctx{}, &config.TestConfig{ConsulImage: "test-config-image"}, "test") - require.Equal(t, cluster.(*HelmCluster).helmOptions.SetValues, tt.want) + require.Equal(t, cluster.helmOptions.SetValues, tt.want) }) } } diff --git a/charts/consul/test/acceptance/framework/environment/environment.go b/acceptance/framework/environment/environment.go similarity index 73% rename from charts/consul/test/acceptance/framework/environment/environment.go rename to acceptance/framework/environment/environment.go index ae6540884b..15121b97e3 100644 --- a/charts/consul/test/acceptance/framework/environment/environment.go +++ b/acceptance/framework/environment/environment.go @@ -5,8 +5,7 @@ import ( "testing" "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/config" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -18,7 +17,7 @@ const ( ) // TestEnvironment represents the infrastructure environment of the test, -// such as the kubernetes cluster(s) the test is running against +// such as the kubernetes cluster(s) the test is running against. type TestEnvironment interface { DefaultContext(t *testing.T) TestContext Context(t *testing.T, name string) TestContext @@ -87,6 +86,27 @@ type kubernetesContext struct { options *k8s.KubectlOptions } +// KubernetesContextFromOptions returns the Kubernetes context from options. +// If context is explicitly set in options, it returns that context. +// Otherwise, it returns the current context. +func KubernetesContextFromOptions(t *testing.T, options *k8s.KubectlOptions) string { + t.Helper() + + // First, check if context set in options and return that + if options.ContextName != "" { + return options.ContextName + } + + // Otherwise, get current context from config + configPath, err := options.GetConfigPath(t) + require.NoError(t, err) + + rawConfig, err := k8s.LoadConfigFromPath(configPath).RawConfig() + require.NoError(t, err) + + return rawConfig.CurrentContext +} + func (k kubernetesContext) KubectlOptions(t *testing.T) *k8s.KubectlOptions { if k.options != nil { return k.options @@ -107,7 +127,7 @@ func (k kubernetesContext) KubectlOptions(t *testing.T) *k8s.KubectlOptions { rawConfig, err := k8s.LoadConfigFromPath(configPath).RawConfig() require.NoError(t, err) - contextName := helpers.KubernetesContextFromOptions(t, k.options) + contextName := KubernetesContextFromOptions(t, k.options) if rawConfig.Contexts[contextName].Namespace != "" { k.options.Namespace = rawConfig.Contexts[contextName].Namespace } else { @@ -117,12 +137,26 @@ func (k kubernetesContext) KubectlOptions(t *testing.T) *k8s.KubectlOptions { return k.options } +// KubernetesClientFromOptions takes KubectlOptions and returns Kubernetes API client. +func KubernetesClientFromOptions(t *testing.T, options *k8s.KubectlOptions) kubernetes.Interface { + configPath, err := options.GetConfigPath(t) + require.NoError(t, err) + + config, err := k8s.LoadApiClientConfigE(configPath, options.ContextName) + require.NoError(t, err) + + client, err := kubernetes.NewForConfig(config) + require.NoError(t, err) + + return client +} + func (k kubernetesContext) KubernetesClient(t *testing.T) kubernetes.Interface { if k.client != nil { return k.client } - k.client = helpers.KubernetesClientFromOptions(t, k.KubectlOptions(t)) + k.client = KubernetesClientFromOptions(t, k.KubectlOptions(t)) return k.client } diff --git a/charts/consul/test/acceptance/framework/flags/flags.go b/acceptance/framework/flags/flags.go similarity index 98% rename from charts/consul/test/acceptance/framework/flags/flags.go rename to acceptance/framework/flags/flags.go index 345d0b2d37..ca22722ec6 100644 --- a/charts/consul/test/acceptance/framework/flags/flags.go +++ b/acceptance/framework/flags/flags.go @@ -6,7 +6,7 @@ import ( "os" "sync" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" ) type TestFlags struct { diff --git a/charts/consul/test/acceptance/framework/flags/flags_test.go b/acceptance/framework/flags/flags_test.go similarity index 100% rename from charts/consul/test/acceptance/framework/flags/flags_test.go rename to acceptance/framework/flags/flags_test.go diff --git a/acceptance/framework/helpers/helpers.go b/acceptance/framework/helpers/helpers.go new file mode 100644 index 0000000000..17dd805123 --- /dev/null +++ b/acceptance/framework/helpers/helpers.go @@ -0,0 +1,150 @@ +package helpers + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/helm" + "github.com/hashicorp/consul/api" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// RandomName generates a random string with a 'test-' prefix. +func RandomName() string { + return fmt.Sprintf("test-%s", strings.ToLower(random.UniqueId())) +} + +// CheckForPriorInstallations checks if there is an existing Helm release +// for this Helm chart already installed. If there is, it fails the tests. +func CheckForPriorInstallations(t *testing.T, client kubernetes.Interface, options *helm.Options, chartName, labelSelector string) { + t.Helper() + + var helmListOutput string + // Check if there's an existing cluster and fail if there is one. + // We may need to retry since this is the first command run once the Kube + // cluster is created and sometimes the API server returns errors. + retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 3}, t, func(r *retry.R) { + var err error + // NOTE: It's okay to pass in `t` to RunHelmCommandAndGetOutputE despite being in a retry + // because we're using RunHelmCommandAndGetOutputE (not RunHelmCommandAndGetOutput) so the `t` won't + // get used to fail the test, just for logging. + helmListOutput, err = helm.RunHelmCommandAndGetOutputE(t, options, "list", "--output", "json") + require.NoError(r, err) + }) + + var installedReleases []map[string]string + + err := json.Unmarshal([]byte(helmListOutput), &installedReleases) + require.NoError(t, err, "unmarshalling %q", helmListOutput) + + for _, r := range installedReleases { + require.NotContains(t, r["chart"], chartName, fmt.Sprintf("detected an existing installation of %s %s, release name: %s", chartName, r["chart"], r["name"])) + } + + // Wait for all pods in the "default" namespace to exit. A previous + // release may not be listed by Helm but its pods may still be terminating. + retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 60}, t, func(r *retry.R) { + pods, err := client.CoreV1().Pods(options.KubectlOptions.Namespace).List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) + require.NoError(r, err) + if len(pods.Items) > 0 { + var podNames []string + for _, p := range pods.Items { + podNames = append(podNames, p.Name) + } + r.Errorf("pods from previous installation still running: %s", strings.Join(podNames, ", ")) + } + }) +} + +// SetupInterruptHandler sets up a goroutine that will wait for interrupt signals +// and call cleanup function when it catches it. +func SetupInterruptHandler(cleanup func()) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("\r- Ctrl+C pressed in Terminal. Cleaning up resources.") + cleanup() + os.Exit(1) + }() +} + +// Cleanup will both register a cleanup function with t +// and SetupInterruptHandler to make sure resources get cleaned up +// if an interrupt signal is caught. +func Cleanup(t *testing.T, noCleanupOnFailure bool, cleanup func()) { + t.Helper() + + // Always clean up when an interrupt signal is caught. + SetupInterruptHandler(cleanup) + + // If noCleanupOnFailure is set, don't clean up resources if tests fail. + // We need to wrap the cleanup function because t that is passed in to this function + // might not have the information on whether the test has failed yet. + wrappedCleanupFunc := func() { + if !(noCleanupOnFailure && t.Failed()) { + logger.Logf(t, "cleaning up resources for %s", t.Name()) + cleanup() + } else { + logger.Log(t, "skipping resource cleanup") + } + } + + t.Cleanup(wrappedCleanupFunc) +} + +// VerifyFederation checks that the WAN federation between servers is successful +// by first checking members are alive from the perspective of both servers. +// If secure is true, it will also check that the ACL replication is running on the secondary server. +func VerifyFederation(t *testing.T, primaryClient, secondaryClient *api.Client, releaseName string, secure bool) { + retrier := &retry.Timer{Timeout: 5 * time.Minute, Wait: 1 * time.Second} + start := time.Now() + + // Check that server in dc1 is healthy from the perspective of the server in dc2, and vice versa. + // We're calling the Consul health API, as opposed to checking serf membership status, + // because we need to make sure that the federated servers can make API calls and forward requests + // from one server to another. From running tests in CI for a while and using serf membership status before, + // we've noticed that the status could be "alive" as soon as the server in the secondary cluster joins the primary + // and then switch to "failed". This would require us to check that the status is "alive" is showing consistently for + // some amount of time, which could be quite flakey. Calling the API in another datacenter allows us to check that + // each server can forward calls to another, which is what we need for connect. + retry.RunWith(retrier, t, func(r *retry.R) { + secondaryServerHealth, _, err := primaryClient.Health().Node(fmt.Sprintf("%s-consul-server-0", releaseName), &api.QueryOptions{Datacenter: "dc2"}) + require.NoError(r, err) + require.Equal(r, secondaryServerHealth.AggregatedStatus(), api.HealthPassing) + + primaryServerHealth, _, err := secondaryClient.Health().Node(fmt.Sprintf("%s-consul-server-0", releaseName), &api.QueryOptions{Datacenter: "dc1"}) + require.NoError(r, err) + require.Equal(r, primaryServerHealth.AggregatedStatus(), api.HealthPassing) + + if secure { + replicationStatus, _, err := secondaryClient.ACL().Replication(nil) + require.NoError(r, err) + require.True(r, replicationStatus.Enabled) + require.True(r, replicationStatus.Running) + } + }) + + logger.Logf(t, "Took %s to verify federation", time.Since(start)) +} + +// MergeMaps will merge the values in b with values in a and save in a. +// If there are conflicts, the values in b will overwrite the values in a. +func MergeMaps(a, b map[string]string) { + for k, v := range b { + a[k] = v + } +} diff --git a/acceptance/framework/helpers/helpers_test.go b/acceptance/framework/helpers/helpers_test.go new file mode 100644 index 0000000000..c1b4a916e2 --- /dev/null +++ b/acceptance/framework/helpers/helpers_test.go @@ -0,0 +1,45 @@ +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMergeMaps(t *testing.T) { + cases := map[string]struct { + a, b, expected map[string]string + }{ + "b overwrites a": { + a: map[string]string{ + "foo": "bar", + }, + b: map[string]string{ + "foo": "baz", + }, + expected: map[string]string{ + "foo": "baz", + }, + }, + "no overlap": { + a: map[string]string{ + "foo": "bar", + }, + b: map[string]string{ + "bar": "baz", + }, + expected: map[string]string{ + "foo": "bar", + "bar": "baz", + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actual := c.a + MergeMaps(actual, c.b) + require.Equal(t, c.expected, actual) + }) + } +} diff --git a/charts/consul/test/acceptance/framework/k8s/debug.go b/acceptance/framework/k8s/debug.go similarity index 95% rename from charts/consul/test/acceptance/framework/k8s/debug.go rename to acceptance/framework/k8s/debug.go index d68df761b0..0754f24131 100644 --- a/charts/consul/test/acceptance/framework/k8s/debug.go +++ b/acceptance/framework/k8s/debug.go @@ -10,8 +10,8 @@ import ( "github.com/gruntwork-io/terratest/modules/k8s" terratestLogger "github.com/gruntwork-io/terratest/modules/logger" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -23,9 +23,9 @@ func WritePodsDebugInfoIfFailed(t *testing.T, kubectlOptions *k8s.KubectlOptions if t.Failed() { // Create k8s client from kubectl options. - client := helpers.KubernetesClientFromOptions(t, kubectlOptions) + client := environment.KubernetesClientFromOptions(t, kubectlOptions) - contextName := helpers.KubernetesContextFromOptions(t, kubectlOptions) + contextName := environment.KubernetesContextFromOptions(t, kubectlOptions) // Create a directory for the test. testDebugDirectory := filepath.Join(debugDirectory, t.Name(), contextName) diff --git a/charts/consul/test/acceptance/framework/k8s/deploy.go b/acceptance/framework/k8s/deploy.go similarity index 72% rename from charts/consul/test/acceptance/framework/k8s/deploy.go rename to acceptance/framework/k8s/deploy.go index be8ee98d6f..09272d5382 100644 --- a/charts/consul/test/acceptance/framework/k8s/deploy.go +++ b/acceptance/framework/k8s/deploy.go @@ -8,16 +8,14 @@ import ( "time" "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/util/yaml" ) -const staticClientName = "static-client" - // Deploy creates a Kubernetes deployment by applying configuration stored at filepath, // sets up a cleanup function and waits for the deployment to become available. func Deploy(t *testing.T, options *k8s.KubectlOptions, noCleanupOnFailure bool, debugDirectory string, filepath string) { @@ -71,38 +69,43 @@ func DeployKustomize(t *testing.T, options *k8s.KubectlOptions, noCleanupOnFailu RunKubectl(t, options, "wait", "--for=condition=available", "--timeout=5m", fmt.Sprintf("deploy/%s", deployment.Name)) } -// CheckStaticServerConnection execs into a pod of the deployment given by deploymentName +// CheckStaticServerConnection execs into a pod of sourceApp // and runs a curl command with the provided curlArgs. // This function assumes that the connection is made to the static-server and expects the output -// to be "hello world" in a case of success. +// to be "hello world" by default, or expectedSuccessOutput in a case of success. // If expectSuccess is true, it will expect connection to succeed, // otherwise it will expect failure due to intentions. -func CheckStaticServerConnection(t *testing.T, options *k8s.KubectlOptions, expectSuccess bool, failureMessages []string, curlArgs ...string) { +func CheckStaticServerConnection(t *testing.T, options *k8s.KubectlOptions, sourceApp string, expectSuccess bool, failureMessages []string, expectedSuccessOutput string, curlArgs ...string) { t.Helper() - CheckStaticServerConnectionMultipleFailureMessages(t, options, expectSuccess, failureMessages, curlArgs...) + CheckStaticServerConnectionMultipleFailureMessages(t, options, sourceApp, expectSuccess, failureMessages, expectedSuccessOutput, curlArgs...) } -// CheckStaticServerConnectionMultipleFailureMessages execs into a pod of the deployment given by deploymentName +// CheckStaticServerConnectionMultipleFailureMessages execs into a pod of sourceApp // and runs a curl command with the provided curlArgs. // This function assumes that the connection is made to the static-server and expects the output -// to be "hello world" in a case of success. +// to be "hello world" by default, or expectedSuccessOutput in a case of success. // If expectSuccess is true, it will expect connection to succeed, // otherwise it will expect failure due to intentions. If multiple failureMessages are provided it will assert // on the existence of any of them. -func CheckStaticServerConnectionMultipleFailureMessages(t *testing.T, options *k8s.KubectlOptions, expectSuccess bool, failureMessages []string, curlArgs ...string) { +func CheckStaticServerConnectionMultipleFailureMessages(t *testing.T, options *k8s.KubectlOptions, sourceApp string, expectSuccess bool, failureMessages []string, expectedSuccessOutput string, curlArgs ...string) { t.Helper() + expectedOutput := "hello world" + if expectedSuccessOutput != "" { + expectedOutput = expectedSuccessOutput + } + retrier := &retry.Timer{Timeout: 80 * time.Second, Wait: 2 * time.Second} - args := []string{"exec", "deploy/" + staticClientName, "-c", staticClientName, "--", "curl", "-vvvsSf"} + args := []string{"exec", "deploy/" + sourceApp, "-c", sourceApp, "--", "curl", "-vvvsSf"} args = append(args, curlArgs...) retry.RunWith(retrier, t, func(r *retry.R) { output, err := RunKubectlAndGetOutputE(t, options, args...) if expectSuccess { require.NoError(r, err) - require.Contains(r, output, "hello world") + require.Contains(r, output, expectedOutput) } else { require.Error(r, err) require.Condition(r, func() bool { @@ -118,23 +121,33 @@ func CheckStaticServerConnectionMultipleFailureMessages(t *testing.T, options *k }) } +// CheckStaticServerConnectionSuccessfulWithMessage is just like CheckStaticServerConnectionSuccessful +// but it asserts on a non-default expected message. +func CheckStaticServerConnectionSuccessfulWithMessage(t *testing.T, options *k8s.KubectlOptions, sourceApp string, message string, curlArgs ...string) { + t.Helper() + start := time.Now() + CheckStaticServerConnectionMultipleFailureMessages(t, options, sourceApp, true, nil, message, curlArgs...) + logger.Logf(t, "Took %s to check if static server connection was successful", time.Since(start)) +} + // CheckStaticServerConnectionSuccessful is just like CheckStaticServerConnection // but it always expects a successful connection. -func CheckStaticServerConnectionSuccessful(t *testing.T, options *k8s.KubectlOptions, curlArgs ...string) { +func CheckStaticServerConnectionSuccessful(t *testing.T, options *k8s.KubectlOptions, sourceApp string, curlArgs ...string) { t.Helper() start := time.Now() - CheckStaticServerConnection(t, options, true, nil, curlArgs...) + CheckStaticServerConnection(t, options, sourceApp, true, nil, "", curlArgs...) logger.Logf(t, "Took %s to check if static server connection was successful", time.Since(start)) } // CheckStaticServerConnectionFailing is just like CheckStaticServerConnection // but it always expects a failing connection with various errors. -func CheckStaticServerConnectionFailing(t *testing.T, options *k8s.KubectlOptions, curlArgs ...string) { +func CheckStaticServerConnectionFailing(t *testing.T, options *k8s.KubectlOptions, sourceApp string, curlArgs ...string) { t.Helper() - CheckStaticServerConnection(t, options, false, []string{ + CheckStaticServerConnection(t, options, sourceApp, false, []string{ "curl: (52) Empty reply from server", "curl: (7) Failed to connect", - }, curlArgs...) + "curl: (56) Recv failure: Connection reset by peer", + }, "", curlArgs...) } // labelMapToString takes a label map[string]string diff --git a/acceptance/framework/k8s/helpers.go b/acceptance/framework/k8s/helpers.go new file mode 100644 index 0000000000..1b895a5e17 --- /dev/null +++ b/acceptance/framework/k8s/helpers.go @@ -0,0 +1,127 @@ +package k8s + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// KubernetesAPIServerHostFromOptions returns the Kubernetes API server host from options. +func KubernetesAPIServerHostFromOptions(t *testing.T, options *terratestk8s.KubectlOptions) string { + t.Helper() + + configPath, err := options.GetConfigPath(t) + require.NoError(t, err) + + config, err := terratestk8s.LoadApiClientConfigE(configPath, options.ContextName) + require.NoError(t, err) + + return config.Host +} + +// WaitForAllPodsToBeReady waits until all pods with the provided podLabelSelector +// are in the ready status. It checks every second for 11 minutes. +// If there is at least one container in a pod that isn't ready after that, +// it fails the test. +func WaitForAllPodsToBeReady(t *testing.T, client kubernetes.Interface, namespace, podLabelSelector string) { + t.Helper() + + logger.Logf(t, "Waiting for pods with label %q to be ready.", podLabelSelector) + + // Wait up to 11m. + // On Azure, volume provisioning can sometimes take close to 5 min, + // so we need to give a bit more time for pods to become healthy. + counter := &retry.Counter{Count: 11 * 60, Wait: 1 * time.Second} + retry.RunWith(counter, t, func(r *retry.R) { + pods, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: podLabelSelector}) + require.NoError(r, err) + require.NotEmpty(r, pods.Items) + + var notReadyPods []string + for _, pod := range pods.Items { + if !IsReady(pod) { + notReadyPods = append(notReadyPods, pod.Name) + } + } + if len(notReadyPods) > 0 { + r.Errorf("%d pods are not ready: %s", len(notReadyPods), strings.Join(notReadyPods, ",")) + } + }) + logger.Log(t, "Finished waiting for pods to be ready.") +} + +// IsReady returns true if pod is ready. +func IsReady(pod corev1.Pod) bool { + if pod.Status.Phase == corev1.PodPending { + return false + } + + for _, cond := range pod.Status.Conditions { + if cond.Type == corev1.PodReady { + if cond.Status == corev1.ConditionTrue { + return true + } else { + return false + } + } + } + + return false +} + +// KubernetesAPIServerHost returns the Kubernetes API server URL depending on test configuration. +func KubernetesAPIServerHost(t *testing.T, cfg *config.TestConfig, ctx environment.TestContext) string { + var k8sAPIHost string + // When running on kind, the kube API address in kubeconfig will have a localhost address + // which will not work from inside the container. That's why we need to use the endpoints address instead + // which will point the node IP. + if cfg.UseKind { + // The Kubernetes AuthMethod host is read from the endpoints for the Kubernetes service. + kubernetesEndpoint, err := ctx.KubernetesClient(t).CoreV1().Endpoints("default").Get(context.Background(), "kubernetes", metav1.GetOptions{}) + require.NoError(t, err) + k8sAPIHost = fmt.Sprintf("https://%s:%d", kubernetesEndpoint.Subsets[0].Addresses[0].IP, kubernetesEndpoint.Subsets[0].Ports[0].Port) + } else { + k8sAPIHost = KubernetesAPIServerHostFromOptions(t, ctx.KubectlOptions(t)) + } + + return k8sAPIHost +} + +// ServiceHost returns a host for a Kubernetes service depending on test configuration. +func ServiceHost(t *testing.T, cfg *config.TestConfig, ctx environment.TestContext, serviceName string) string { + if cfg.UseKind { + nodeList, err := ctx.KubernetesClient(t).CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + // Get the address of the (only) node from the Kind cluster. + return nodeList.Items[0].Status.Addresses[0].Address + } else { + var host string + // It can take some time for the load balancers to be ready and have an IP/Hostname. + // Wait for 60 seconds before failing. + retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 60}, t, func(r *retry.R) { + svc, err := ctx.KubernetesClient(t).CoreV1().Services(ctx.KubectlOptions(t).Namespace).Get(context.Background(), serviceName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotEmpty(r, svc.Status.LoadBalancer.Ingress) + // On AWS, load balancers have a hostname for ingress, while on Azure and GCP + // load balancers have IPs. + if svc.Status.LoadBalancer.Ingress[0].Hostname != "" { + host = svc.Status.LoadBalancer.Ingress[0].Hostname + } else { + host = svc.Status.LoadBalancer.Ingress[0].IP + } + }) + return host + } +} diff --git a/charts/consul/test/acceptance/framework/k8s/kubectl.go b/acceptance/framework/k8s/kubectl.go similarity index 98% rename from charts/consul/test/acceptance/framework/k8s/kubectl.go rename to acceptance/framework/k8s/kubectl.go index de49b3c46d..318cde217e 100644 --- a/charts/consul/test/acceptance/framework/k8s/kubectl.go +++ b/acceptance/framework/k8s/kubectl.go @@ -8,7 +8,7 @@ import ( "github.com/gruntwork-io/terratest/modules/k8s" terratestLogger "github.com/gruntwork-io/terratest/modules/logger" "github.com/gruntwork-io/terratest/modules/shell" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" ) diff --git a/charts/consul/test/acceptance/framework/logger/logger.go b/acceptance/framework/logger/logger.go similarity index 100% rename from charts/consul/test/acceptance/framework/logger/logger.go rename to acceptance/framework/logger/logger.go diff --git a/charts/consul/test/acceptance/framework/suite/suite.go b/acceptance/framework/suite/suite.go similarity index 81% rename from charts/consul/test/acceptance/framework/suite/suite.go rename to acceptance/framework/suite/suite.go index d421b0b91b..365737f66f 100644 --- a/charts/consul/test/acceptance/framework/suite/suite.go +++ b/acceptance/framework/suite/suite.go @@ -6,9 +6,9 @@ import ( "io/ioutil" "testing" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/config" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/environment" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/flags" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/flags" ) type suite struct { diff --git a/acceptance/framework/vault/vault_cluster.go b/acceptance/framework/vault/vault_cluster.go new file mode 100644 index 0000000000..6e265ddb51 --- /dev/null +++ b/acceptance/framework/vault/vault_cluster.go @@ -0,0 +1,405 @@ +package vault + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/helm" + terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" + terratestLogger "github.com/gruntwork-io/terratest/modules/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/control-plane/helper/cert" + "github.com/hashicorp/consul/sdk/testutil/retry" + vapi "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + releaseLabel = "app.kubernetes.io/instance=" +) + +// VaultCluster represents a vault installation. +type VaultCluster struct { + ctx environment.TestContext + + helmOptions *helm.Options + releaseName string + vaultClient *vapi.Client + + kubectlOptions *terratestk8s.KubectlOptions + + kubernetesClient kubernetes.Interface + + noCleanupOnFailure bool + debugDirectory string + logger terratestLogger.TestLogger +} + +// NewVaultCluster creates a VaultCluster which will be used to install Vault using Helm. +func NewVaultCluster(t *testing.T, ctx environment.TestContext, cfg *config.TestConfig, releaseName string, helmValues map[string]string) *VaultCluster { + + logger := terratestLogger.New(logger.TestLogger{}) + + kopts := ctx.KubectlOptions(t) + + values := defaultHelmValues(releaseName) + if cfg.EnablePodSecurityPolicies { + values["global.psp.enable"] = "true" + } + + helpers.MergeMaps(values, helmValues) + vaultHelmOpts := &helm.Options{ + SetValues: values, + KubectlOptions: kopts, + Logger: logger, + } + + helm.AddRepo(t, vaultHelmOpts, "hashicorp", "https://helm.releases.hashicorp.com") + // Ignoring the error from `helm repo update` as it could fail due to stale cache or unreachable servers and we're + // asserting a chart version on Install which would fail in an obvious way should this not succeed. + _, err := helm.RunHelmCommandAndGetOutputE(t, &helm.Options{}, "repo", "update") + if err != nil { + logger.Logf(t, "Unable to update helm repository, proceeding anyway: %s.", err) + } + + return &VaultCluster{ + ctx: ctx, + helmOptions: vaultHelmOpts, + kubectlOptions: kopts, + kubernetesClient: ctx.KubernetesClient(t), + noCleanupOnFailure: cfg.NoCleanupOnFailure, + debugDirectory: cfg.DebugDirectory, + logger: logger, + releaseName: releaseName, + } +} + +// VaultClient returns the vault client. +func (v *VaultCluster) VaultClient(*testing.T) *vapi.Client { return v.vaultClient } + +// SetupVaultClient sets up and returns a Vault Client. +func (v *VaultCluster) SetupVaultClient(t *testing.T) *vapi.Client { + t.Helper() + + if v.vaultClient != nil { + return v.vaultClient + } + + config := vapi.DefaultConfig() + localPort := terratestk8s.GetAvailablePort(t) + remotePort := 8200 // use non-secure by default + + serverPod := fmt.Sprintf("%s-vault-0", v.releaseName) + tunnel := terratestk8s.NewTunnelWithLogger( + v.helmOptions.KubectlOptions, + terratestk8s.ResourceTypePod, + serverPod, + localPort, + remotePort, + v.logger) + + // Retry creating the port forward since it can fail occasionally. + retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 60}, t, func(r *retry.R) { + // NOTE: It's okay to pass in `t` to ForwardPortE despite being in a retry + // because we're using ForwardPortE (not ForwardPort) so the `t` won't + // get used to fail the test, just for logging. + require.NoError(r, tunnel.ForwardPortE(t)) + }) + + t.Cleanup(func() { + tunnel.Close() + }) + + config.Address = fmt.Sprintf("https://127.0.0.1:%d", localPort) + // We don't need to verify TLS for localhost traffic. + err := config.ConfigureTLS(&vapi.TLSConfig{Insecure: true}) + require.NoError(t, err) + vaultClient, err := vapi.NewClient(config) + require.NoError(t, err) + return vaultClient +} + +// bootstrap sets up Kubernetes auth method and enables secrets engines. +func (v *VaultCluster) bootstrap(t *testing.T) { + if !v.serverEnabled() { + v.logger.Logf(t, "skipping bootstrapping Vault because Vault server is not enabled") + return + } + v.vaultClient = v.SetupVaultClient(t) + + // Enable the KV-V2 Secrets engine. + err := v.vaultClient.Sys().Mount("consul", &vapi.MountInput{ + Type: "kv-v2", + Config: vapi.MountConfigInput{}, + }) + require.NoError(t, err) + + // Enable the PKI Secrets engine. + err = v.vaultClient.Sys().Mount("pki", &vapi.MountInput{ + Type: "pki", + Config: vapi.MountConfigInput{}, + }) + require.NoError(t, err) + + namespace := v.helmOptions.KubectlOptions.Namespace + vaultServerServiceAccountName := fmt.Sprintf("%s-vault", v.releaseName) + v.ConfigureAuthMethod(t, v.vaultClient, "kubernetes", "https://kubernetes.default.svc", vaultServerServiceAccountName, namespace) +} + +// ConfigureAuthMethod configures the auth method in Vault from the provided service account name and namespace, +// kubernetes host and auth path. +// We need to take vaultClient here in case this Vault cluster does not have a server to run API commands against. +func (v *VaultCluster) ConfigureAuthMethod(t *testing.T, vaultClient *vapi.Client, authPath, k8sHost, saName, saNS string) { + v.logger.Logf(t, "enabling kubernetes auth method on %s path", authPath) + err := vaultClient.Sys().EnableAuthWithOptions(authPath, &vapi.EnableAuthOptions{ + Type: "kubernetes", + }) + require.NoError(t, err) + + // To configure the auth method, we need to read the token and the CA cert from the auth method's + // service account token. + // The JWT token and CA cert is what Vault server will use to validate service account token + // with the Kubernetes API. + var sa *corev1.ServiceAccount + retry.Run(t, func(r *retry.R) { + sa, err = v.kubernetesClient.CoreV1().ServiceAccounts(saNS).Get(context.Background(), saName, metav1.GetOptions{}) + require.NoError(r, err) + require.Len(r, sa.Secrets, 1) + }) + + v.logger.Logf(t, "updating vault kubernetes auth config for %s auth path", authPath) + tokenSecret, err := v.kubernetesClient.CoreV1().Secrets(saNS).Get(context.Background(), sa.Secrets[0].Name, metav1.GetOptions{}) + require.NoError(t, err) + _, err = vaultClient.Logical().Write(fmt.Sprintf("auth/%s/config", authPath), map[string]interface{}{ + "token_reviewer_jwt": string(tokenSecret.Data["token"]), + "kubernetes_ca_cert": string(tokenSecret.Data["ca.crt"]), + "kubernetes_host": k8sHost, + }) + require.NoError(t, err) +} + +// Create installs Vault via Helm and then calls bootstrap to initialize it. +func (v *VaultCluster) Create(t *testing.T, ctx environment.TestContext) { + t.Helper() + + // Make sure we delete the cluster if we receive an interrupt signal and + // register cleanup so that we delete the cluster when test finishes. + helpers.Cleanup(t, v.noCleanupOnFailure, func() { + v.Destroy(t) + }) + + // Fail if there are any existing installations of the Helm chart. + helpers.CheckForPriorInstallations(t, v.kubernetesClient, v.helmOptions, "", v.releaseLabelSelector()) + + v.createTLSCerts(t) + + // Install Vault. + helm.Install(t, v.helmOptions, "hashicorp/vault", v.releaseName) + + v.initAndUnseal(t) + + // Wait for the injector and vault server pods to become Ready. + k8s.WaitForAllPodsToBeReady(t, v.kubernetesClient, v.helmOptions.KubectlOptions.Namespace, v.releaseLabelSelector()) + + // Now call bootstrap(). + v.bootstrap(t) +} + +// Destroy issues a helm delete and deletes the PVC + any helm secrets related to the release that are leftover. +func (v *VaultCluster) Destroy(t *testing.T) { + t.Helper() + + k8s.WritePodsDebugInfoIfFailed(t, v.kubectlOptions, v.debugDirectory, v.releaseLabelSelector()) + // Ignore the error returned by the helm delete here so that we can + // always idempotently clean up resources in the cluster. + _ = helm.DeleteE(t, v.helmOptions, v.releaseName, true) + + err := v.kubernetesClient.CoreV1().PersistentVolumeClaims(v.helmOptions.KubectlOptions.Namespace).DeleteCollection(context.Background(), + metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: v.releaseLabelSelector()}) + require.NoError(t, err) +} + +func defaultHelmValues(releaseName string) map[string]string { + certSecret := certSecretName(releaseName) + caSecret := CASecretName(releaseName) + + serverConfig := fmt.Sprintf(` + listener "tcp" { + address = "[::]:8200" + cluster_address = "[::]:8201" + tls_cert_file = "/vault/userconfig/%s/tls.crt" + tls_key_file = "/vault/userconfig/%s/tls.key" + tls_client_ca_file = "/vault/userconfig/%s/tls.crt" + } + + storage "file" { + path = "/vault/data" + }`, certSecret, certSecret, caSecret) + + return map[string]string{ + "global.tlsDisable": "false", + "server.extraEnvironmentVars.VAULT_CACERT": fmt.Sprintf("/vault/userconfig/%s/tls.crt", caSecret), + "server.extraVolumes[0].name": caSecret, + "server.extraVolumes[0].type": "secret", + "server.extraVolumes[1].name": certSecret, + "server.extraVolumes[1].type": "secret", + "server.standalone.enabled": "true", + "server.standalone.config": serverConfig, + "injector.enabled": "true", + "ui.enabled": "true", + } +} + +// certSecretName returns the Kubernetes secret name of the certificate and key +// for the Vault server. +func certSecretName(releaseName string) string { + return fmt.Sprintf("%s-vault-server-tls", releaseName) +} + +// CASecretName returns the Kubernetes secret name of the CA for the Vault server. +func CASecretName(releaseName string) string { + return fmt.Sprintf("%s-vault-ca", releaseName) +} + +// Address is the in-cluster API address of the Vault server. +func (v *VaultCluster) Address() string { + return fmt.Sprintf("https://%s-vault:8200", v.releaseName) +} + +// releaseLabelSelector returns label selector that selects all pods +// from a Vault installation. +func (v *VaultCluster) releaseLabelSelector() string { + return fmt.Sprintf("%s=%s", releaseLabel, v.releaseName) +} + +// serverEnabled returns true if this Vault cluster has a server. +func (v *VaultCluster) serverEnabled() bool { + serverEnabled, ok := v.helmOptions.SetValues["server.enabled"] + // Server is enabled by default in the Vault Helm chart, so it's enabled either when that helm value is + // not provided or when it's not explicitly disabled. + return !ok || serverEnabled != "false" +} + +// createTLSCerts generates a self-signed CA and uses it to generate +// certificate and key for the Vault server. It then saves those as +// Kubernetes secrets. +func (v *VaultCluster) createTLSCerts(t *testing.T) { + if !v.serverEnabled() { + v.logger.Logf(t, "skipping generating Vault TLS certificates because Vault server is not enabled") + return + } + + v.logger.Logf(t, "generating Vault TLS certificates") + + namespace := v.helmOptions.KubectlOptions.Namespace + + // Generate CA and cert and create secrets for them. + signer, _, caPem, caCertTmpl, err := cert.GenerateCA("Vault CA") + require.NoError(t, err) + vaultService := fmt.Sprintf("%s-vault", v.releaseName) + certSANs := []string{ + vaultService, + fmt.Sprintf("%s.default", vaultService), + fmt.Sprintf("%s.default.svc", vaultService), + } + certPem, keyPem, err := cert.GenerateCert("Vault server", 24*time.Hour, caCertTmpl, signer, certSANs) + require.NoError(t, err) + + t.Cleanup(func() { + if !v.noCleanupOnFailure { + // We're ignoring error here because secret deletion is best-effort. + _ = v.kubernetesClient.CoreV1().Secrets(namespace).Delete(context.Background(), certSecretName(v.releaseName), metav1.DeleteOptions{}) + _ = v.kubernetesClient.CoreV1().Secrets(namespace).Delete(context.Background(), CASecretName(v.releaseName), metav1.DeleteOptions{}) + } + }) + + _, err = v.kubernetesClient.CoreV1().Secrets(namespace).Create(context.Background(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: certSecretName(v.releaseName), + }, + Data: map[string][]byte{ + corev1.TLSCertKey: []byte(certPem), + corev1.TLSPrivateKeyKey: []byte(keyPem), + }, + Type: corev1.SecretTypeTLS, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = v.kubernetesClient.CoreV1().Secrets(namespace).Create(context.Background(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: CASecretName(v.releaseName), + Namespace: namespace, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: []byte(caPem), + }, + Type: corev1.SecretTypeOpaque, + }, metav1.CreateOptions{}) + require.NoError(t, err) +} + +// initAndUnseal initializes and unseals Vault. +// Once initialized, it saves the Vault root token into a Kubernetes secret. +func (v *VaultCluster) initAndUnseal(t *testing.T) { + if !v.serverEnabled() { + v.logger.Logf(t, "skipping initializing and unsealing Vault because Vault server is not enabled") + return + } + + v.logger.Logf(t, "initializing and unsealing Vault") + namespace := v.helmOptions.KubectlOptions.Namespace + retrier := &retry.Timer{Timeout: 2 * time.Minute, Wait: 1 * time.Second} + retry.RunWith(retrier, t, func(r *retry.R) { + // Wait for vault server pod to be running so that we can create Vault client without errors. + serverPod, err := v.kubernetesClient.CoreV1().Pods(namespace).Get(context.Background(), fmt.Sprintf("%s-vault-0", v.releaseName), metav1.GetOptions{}) + require.NoError(r, err) + require.Equal(r, serverPod.Status.Phase, corev1.PodRunning) + + // Set up the client so that we can make API calls to initialize and unseal. + v.vaultClient = v.SetupVaultClient(t) + + // Initialize Vault with 1 secret share. We don't need to + // more key shares for this test installation. + initResp, err := v.vaultClient.Sys().Init(&vapi.InitRequest{ + SecretShares: 1, + SecretThreshold: 1, + }) + require.NoError(r, err) + v.vaultClient.SetToken(initResp.RootToken) + + // Unseal Vault with the unseal key we got when initialized it. + // There should be one unseal key since we're only using one secret share. + _, err = v.vaultClient.Sys().Unseal(initResp.KeysB64[0]) + require.NoError(r, err) + }) + + v.logger.Logf(t, "successfully initialized and unsealed Vault") + + rootTokenSecret := fmt.Sprintf("%s-vault-root-token", v.releaseName) + v.logger.Logf(t, "saving Vault root token to %q Kubernetes secret", rootTokenSecret) + + helpers.Cleanup(t, v.noCleanupOnFailure, func() { + _ = v.kubernetesClient.CoreV1().Secrets(namespace).Delete(context.Background(), rootTokenSecret, metav1.DeleteOptions{}) + }) + _, err := v.kubernetesClient.CoreV1().Secrets(namespace).Create(context.Background(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: rootTokenSecret, + Namespace: namespace, + }, + Data: map[string][]byte{ + "token": []byte(v.vaultClient.Token()), + }, + Type: corev1.SecretTypeOpaque, + }, metav1.CreateOptions{}) + require.NoError(t, err) +} diff --git a/acceptance/go.mod b/acceptance/go.mod new file mode 100644 index 0000000000..3c31530573 --- /dev/null +++ b/acceptance/go.mod @@ -0,0 +1,104 @@ +module github.com/hashicorp/consul-k8s/acceptance + +go 1.17 + +require ( + github.com/gruntwork-io/terratest v0.31.2 + github.com/hashicorp/consul-k8s/control-plane v0.0.0-20211207212234-aea9efea5638 + github.com/hashicorp/consul/api v1.12.0 + github.com/hashicorp/consul/sdk v0.9.0 + github.com/hashicorp/vault/api v1.2.0 + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.22.2 + k8s.io/apimachinery v0.22.2 + k8s.io/client-go v0.22.2 +) + +require ( + cloud.google.com/go v0.54.0 // indirect + github.com/armon/go-metrics v0.3.9 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/aws/aws-sdk-go v1.30.27 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/evanphx/json-patch v4.11.0+incompatible // indirect + github.com/fatih/color v1.12.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect + github.com/go-logr/logr v0.4.0 // indirect + github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/googleapis/gnostic v0.5.5 // indirect + github.com/gruntwork-io/gruntwork-cli v0.7.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v0.16.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.0.1 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/go-version v1.2.0 // indirect + github.com/hashicorp/golang-lru v0.5.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/serf v0.9.6 // indirect + github.com/hashicorp/vault/sdk v0.2.1 // indirect + github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/jmespath/go-jmespath v0.3.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.13 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.0 // indirect + github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/onsi/ginkgo v1.16.4 // indirect + github.com/onsi/gomega v1.15.0 // indirect + github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pquerna/otp v1.2.0 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/urfave/cli v1.22.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect + golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect + golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 // indirect + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect + google.golang.org/grpc v1.38.0 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/klog/v2 v2.9.0 // indirect + k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect + k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) diff --git a/charts/consul/test/acceptance/go.sum b/acceptance/go.sum similarity index 50% rename from charts/consul/test/acceptance/go.sum rename to acceptance/go.sum index f91e3dc13c..1953df0322 100644 --- a/charts/consul/test/acceptance/go.sum +++ b/acceptance/go.sum @@ -1,3 +1,4 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -5,29 +6,47 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +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= github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v44.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v46.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= github.com/Azure/go-autorest/autorest v0.11.0/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.5/go.mod h1:foo3aIXRQ90zFve3r0QiDsrjGDUwWhKl0ZOQy1CT14k= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.0/go.mod h1:QRTvSZQpxqm8mSErhnbI+tANIBAKP7B+UIE2z4ypUO0= github.com/Azure/go-autorest/autorest/azure/auth v0.5.1/go.mod h1:ea90/jvmnAwDrSooLH4sRIehEPtG/EPUXavDh31MnA4= github.com/Azure/go-autorest/autorest/azure/cli v0.4.0/go.mod h1:JljT387FplPzBA31vUcvsetLKF3pec5bdAxjVU4kI2s= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= @@ -40,66 +59,128 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935 github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= +github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.27.1 h1:MXnqY6SlWySaZAqNnXThOvjRFdiiOuKtC6i7baFdNdU= +github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.30.27 h1:9gPjZWVDSoQrBO2AvqrWObS6KAZByfEJxQoCYo4ZfK0= +github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= +github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= +github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/digitalocean/godo v1.7.5/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU= +github.com/digitalocean/godo v1.10.0/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -107,11 +188,11 @@ github.com/docker/cli v0.0.0-20200109221225-a4f60165b7a3/go.mod h1:JLrzqnKDaYBop github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -120,81 +201,143 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 h1:yY9rWGoXv1U5pl4gxqlULARMQD7x0QG85lqEXTWysik= github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -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/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= 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= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU= github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= +github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-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-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 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/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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.0.0-20200110202235-f4fb41bf00a3/go.mod h1:2wIuQute9+hhWqvL3vEI7YB0EKluF4WcPzI1eAliazk= +github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= @@ -203,131 +346,250 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 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.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/gruntwork-io/gruntwork-cli v0.7.0 h1:YgSAmfCj9c61H+zuvHwKfYUwlMhu5arnQQLM4RH+CYs= github.com/gruntwork-io/gruntwork-cli v0.7.0/go.mod h1:jp6Z7NcLF2avpY8v71fBx6hds9eOFPELSuD/VPv7w00= github.com/gruntwork-io/terratest v0.31.2 h1:xvYHA80MUq5kx670dM18HInewOrrQrAN+XbVVtytUHg= github.com/gruntwork-io/terratest v0.31.2/go.mod h1:EEgJie28gX/4AD71IFqgMj6e99KP5mi81hEtzmDjxTo= -github.com/hashicorp/consul/api v1.9.0 h1:T6dKIWcaihG2c21YUi0BMAHbJanVXiYuz+mPgqxY3N4= -github.com/hashicorp/consul/api v1.9.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= +github.com/hashicorp/consul-k8s/control-plane v0.0.0-20211207212234-aea9efea5638 h1:z68s6H6O3RjxDmNvou/2/3UBrsJkrMcNzI0IQN5scAM= +github.com/hashicorp/consul-k8s/control-plane v0.0.0-20211207212234-aea9efea5638/go.mod h1:7ZeaiADGbvJDuoWAT8UKj6KCcLsFUk+34OkUGMVtdXg= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.10.1-0.20211116182834-e6956893fb6f/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/consul/sdk v0.9.0 h1:NGSHAU7X3yDCjo8WBUbNOtD3BSqv8u0vu3+zNxgmxQI= +github.com/hashicorp/consul/sdk v0.9.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f/go.mod h1:D4eo8/CN92vm9/9UDG+ldX1/fMFa4kpl8qzyTolus8o= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= +github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE= +github.com/hashicorp/vault/api v1.2.0 h1:ysGFc6XRGbv05NsWPzuO5VTv68Lj8jtwATxRLFOpP9s= +github.com/hashicorp/vault/api v1.2.0/go.mod h1:dAjw0T5shMnrfH7Q/Mst+LrcTKvStZBVs1PICEDpUqY= +github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= +github.com/hashicorp/vault/sdk v0.2.1 h1:S4O6Iv/dyKlE9AUTXGa7VOvZmsCvg36toPKgV4f2P4M= +github.com/hashicorp/vault/sdk v0.2.1/go.mod h1:WfUiO1vYzfBkz1TmoE4ZGU7HD0T0Cl/rZwaxjBkgN4U= +github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= -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.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joyent/triton-go v0.0.0-20180628001255-830d2b111e62/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= +github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f/go.mod h1:KDSfL7qe5ZfQqvlDMkVjCztbmcpp/c8M77vhQP8ZPvk= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.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.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/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/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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/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= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/linode/linodego v0.7.1/go.mod h1:ga11n3ivecUrPCHN0rANxKmfWBJVkOXfLMZinAbj2sY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.31 h1:sJFOl9BgwbYAWOGEwr61FU28pqsBNdpRBnhGXtO06Oo= github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +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 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.0 h1:/x0XQ6h+3U3nAyk1yx+bHPURrKa9sVVvYbuqZ7pIAtI= +github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +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 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -339,26 +601,60 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2/go.mod h1:TLb2Sg7HQcgGdloNxkrmtgDNR9uVYF3lfdFIN4Ro6Sk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/oracle/oci-go-sdk v7.1.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+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-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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= @@ -371,45 +667,90 @@ github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prY github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03/go.mod h1:gRAiPF5C5Nd0eyyRdqIu9qTiFSoZzpTq727b5B8fkkU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/zerolog v1.4.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= 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 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 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= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/sean-/conswriter v0.0.0-20180208195008-f5ae3917a627/go.mod h1:7zjs06qF79/FKAJpBvFx3P8Ww4UTIMAe+lpNXDHziac= +github.com/sean-/pager v0.0.0-20180208200047-666be9bf53b5/go.mod h1:BeybITEsBEg6qbIiqJ6/Bqeq25bCLbL7YFmpaFfJDuM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d/go.mod h1:Cw4GTlQccdRGSEf6KiMju767x0NEHE0YIVPJSaXjlsw= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/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/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= @@ -418,31 +759,78 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci 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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vdemeester/k8s-pkg-credentialprovider v0.0.0-20200107171650-7c61ffa44238/go.mod h1:JwQJCMWpUDqjZrB5jpw0f5VbN7U95zxFy1ZDpoEarGo= +github.com/vmware/govmomi v0.18.0/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 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.6.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/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/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= @@ -450,15 +838,24 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -467,18 +864,27 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -486,18 +892,41 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn 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/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -509,8 +938,10 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -520,44 +951,95 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190403152447-81d4e9dc473e/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/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= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/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-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -568,12 +1050,14 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -581,15 +1065,40 @@ golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191205215504-7b8c8591a921/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200113040837-eac381796e91/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= @@ -598,13 +1107,19 @@ google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNV google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -613,16 +1128,43 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -631,80 +1173,133 @@ 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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 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.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= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.1/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 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= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= -k8s.io/api v0.19.3 h1:GN6ntFnv44Vptj/b+OnMW7FmzkpDoIDLZRvKX3XH9aU= +k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= +k8s.io/api v0.19.3/go.mod h1:VF+5FT1B74Pw3KxMdKyinLo+zynBaMBiAfGMuldcNDs= k8s.io/api v0.19.3/go.mod h1:VF+5FT1B74Pw3KxMdKyinLo+zynBaMBiAfGMuldcNDs= +k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= +k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= +k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= +k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= +k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA= k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc= +k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/apimachinery v0.19.3/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= +k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= +k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= +k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= -k8s.io/client-go v0.19.3 h1:ctqR1nQ52NUs6LpI0w+a5U+xjYwflFwA13OJKcicMxg= +k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= k8s.io/client-go v0.19.3/go.mod h1:+eEMktZM+MG0KO+PTkci8xnbCZHvj9TqR6Q1XDUIJOM= +k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= +k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= k8s.io/cloud-provider v0.17.0/go.mod h1:Ze4c3w2C0bRsjkBUoHpFi+qWe3ob1wI2/7cUn+YQIDE= k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc= +k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= k8s.io/csi-translation-lib v0.17.0/go.mod h1:HEF7MEz7pOLJCnxabi45IPkhSsE/KmxPQksuCrHKWls= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 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.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/legacy-cloud-providers v0.17.0/go.mod h1:DdzaepJ3RtRy+e5YhNtrCYwlgyK87j/5+Yfp0L9Syp8= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/controller-runtime v0.10.2/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= -sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= 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/charts/consul/test/acceptance/tests/basic/basic_test.go b/acceptance/tests/basic/basic_test.go similarity index 51% rename from charts/consul/test/acceptance/tests/basic/basic_test.go rename to acceptance/tests/basic/basic_test.go index cc3cdc9877..5b5c389933 100644 --- a/charts/consul/test/acceptance/tests/basic/basic_test.go +++ b/acceptance/tests/basic/basic_test.go @@ -1,15 +1,18 @@ package basic import ( + "context" "fmt" "strconv" + "strings" "testing" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Test that the basic installation, i.e. just @@ -39,9 +42,10 @@ func TestBasicInstallation(t *testing.T) { t.Run(name, func(t *testing.T) { releaseName := helpers.RandomName() helmValues := map[string]string{ - "global.acls.manageSystemACLs": strconv.FormatBool(c.secure), - "global.tls.enabled": strconv.FormatBool(c.secure), - "global.tls.enableAutoEncrypt": strconv.FormatBool(c.autoEncrypt), + "global.acls.manageSystemACLs": strconv.FormatBool(c.secure), + "global.tls.enabled": strconv.FormatBool(c.secure), + "global.gossipEncryption.autoGenerate": strconv.FormatBool(c.secure), + "global.tls.enableAutoEncrypt": strconv.FormatBool(c.autoEncrypt), } consulCluster := consul.NewHelmCluster(t, helmValues, suite.Environment().DefaultContext(t), suite.Config(), releaseName) @@ -63,6 +67,24 @@ func TestBasicInstallation(t *testing.T) { kv, _, err := client.KV().Get(randomKey, nil) require.NoError(t, err) require.Equal(t, kv.Value, randomValue) + + // Check that autogenerated gossip encryption key is being used + if c.secure { + secretName := fmt.Sprintf("%s-consul-gossip-encryption-key", releaseName) + secretKey := "key" + + keyring, err := client.Operator().KeyringList(nil) + require.NoError(t, err) + + testContext := suite.Environment().DefaultContext(t) + secret, err := testContext.KubernetesClient(t).CoreV1().Secrets(testContext.KubectlOptions(t).Namespace).Get(context.Background(), secretName, metav1.GetOptions{}) + require.NoError(t, err) + gossipEncryptionKey := strings.TrimSpace(string(secret.Data[secretKey])) + + require.Len(t, keyring, 2) + require.Contains(t, keyring[0].Keys, gossipEncryptionKey) + require.Contains(t, keyring[1].Keys, gossipEncryptionKey) + } }) } } diff --git a/charts/consul/test/acceptance/tests/basic/main_test.go b/acceptance/tests/basic/main_test.go similarity index 63% rename from charts/consul/test/acceptance/tests/basic/main_test.go rename to acceptance/tests/basic/main_test.go index 79905fb380..fa858ee4ae 100644 --- a/charts/consul/test/acceptance/tests/basic/main_test.go +++ b/acceptance/tests/basic/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/acceptance/tests/connect/connect_helper.go b/acceptance/tests/connect/connect_helper.go new file mode 100644 index 0000000000..4a8efc3319 --- /dev/null +++ b/acceptance/tests/connect/connect_helper.go @@ -0,0 +1,218 @@ +package connect + +import ( + "context" + "strconv" + "testing" + + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + staticClientName = "static-client" + staticServerName = "static-server" +) + +// ConnectHelper configures a Consul cluster for connect injection tests. +// It also provides helper methods to exercise the connect functionality. +type ConnectHelper struct { + // ClusterKind is the kind of Consul cluster to use (e.g. "Helm", "CLI"). + ClusterKind consul.ClusterKind + + // Secure configures the Helm chart for the test to use ACL tokens. + Secure bool + + // AutoEncrypt configures the Helm chart for the test to use AutoEncrypt. + AutoEncrypt bool + + // HelmValues are the additional helm values to use when installing or + // upgrading the cluster beyond connectInject.enabled, global.tls.enabled, + // global.tls.enableAutoEncrypt, global.acls.mangageSystemACLs which are + // set by the Secure and AutoEncrypt fields. + HelmValues map[string]string + + // RelaseName is the name of the Consul cluster. + ReleaseName string + + Ctx environment.TestContext + Cfg *config.TestConfig + + // consulCluster is the cluster to use for the test. + consulCluster consul.Cluster + + // consulClient is the client used to test service mesh connectivity. + consulClient *api.Client +} + +// Setup creates a new cluster using the New*Cluster function and assigns it +// to the consulCluster field. +func (c *ConnectHelper) Setup(t *testing.T) { + switch c.ClusterKind { + case consul.Helm: + c.consulCluster = consul.NewHelmCluster(t, c.helmValues(), c.Ctx, c.Cfg, c.ReleaseName) + case consul.CLI: + c.consulCluster = consul.NewCLICluster(t, c.helmValues(), c.Ctx, c.Cfg, c.ReleaseName) + } +} + +// Install uses the consulCluster to install Consul onto the Kubernetes cluster. +func (c *ConnectHelper) Install(t *testing.T) { + logger.Log(t, "Installing Consul cluster") + c.consulCluster.Create(t) + c.consulClient = c.consulCluster.SetupConsulClient(t, c.Secure) +} + +// Upgrade uses the existing Consul cluster and upgrades it using Helm values +// set by the Secure, AutoEncrypt, and HelmValues fields. +func (c *ConnectHelper) Upgrade(t *testing.T) { + require.NotNil(t, c.consulCluster, "consulCluster must be set before calling Upgrade (Call Install first).") + require.NotNil(t, c.consulClient, "consulClient must be set before calling Upgrade (Call Install first).") + + logger.Log(t, "upgrading Consul cluster") + c.consulCluster.Upgrade(t, c.helmValues()) +} + +// DeployClientAndServer deploys a client and server pod to the Kubernetes +// cluster which will be used to test service mesh connectivity. If the Secure +// flag is true, a pre-check is done to ensure that the ACL tokens for the test +// are deleted. The status of the deployment and injection is checked after the +// deployment is complete to ensure success. +func (c *ConnectHelper) DeployClientAndServer(t *testing.T) { + // Check that the ACL token is deleted. + if c.Secure { + // We need to register the cleanup function before we create the + // deployments because golang will execute them in reverse order + // (i.e. the last registered cleanup function will be executed first). + t.Cleanup(func() { + retry.Run(t, func(r *retry.R) { + tokens, _, err := c.consulClient.ACL().TokenList(nil) + require.NoError(r, err) + for _, token := range tokens { + require.NotContains(r, token.Description, staticServerName) + require.NotContains(r, token.Description, staticClientName) + } + }) + }) + } + + logger.Log(t, "creating static-server and static-client deployments") + + k8s.DeployKustomize(t, c.Ctx.KubectlOptions(t), c.Cfg.NoCleanupOnFailure, c.Cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + if c.Cfg.EnableTransparentProxy { + k8s.DeployKustomize(t, c.Ctx.KubectlOptions(t), c.Cfg.NoCleanupOnFailure, c.Cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") + } else { + k8s.DeployKustomize(t, c.Ctx.KubectlOptions(t), c.Cfg.NoCleanupOnFailure, c.Cfg.DebugDirectory, "../fixtures/cases/static-client-inject") + } + + // Check that both static-server and static-client have been injected and + // now have 2 containers. + for _, labelSelector := range []string{"app=static-server", "app=static-client"} { + podList, err := c.Ctx.KubernetesClient(t).CoreV1().Pods(c.Ctx.KubectlOptions(t).Namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: labelSelector, + }) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Len(t, podList.Items[0].Spec.Containers, 2) + } +} + +// TestConnectionFailureWithoutIntention ensures the connection to the static +// server fails when no intentions are configured. +func (c *ConnectHelper) TestConnectionFailureWithoutIntention(t *testing.T) { + logger.Log(t, "checking that the connection is not successful because there's no intention") + if c.Cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionFailing(t, c.Ctx.KubectlOptions(t), staticClientName, "http://static-server") + } else { + k8s.CheckStaticServerConnectionFailing(t, c.Ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") + } +} + +// CreateIntention creates an intention for the static-server pod to connect to +// the static-client pod. +func (c *ConnectHelper) CreateIntention(t *testing.T) { + logger.Log(t, "creating intention") + _, _, err := c.consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: staticServerName, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Action: api.IntentionActionAllow, + }, + }, + }, nil) + require.NoError(t, err) +} + +// TestConnectionSuccessful ensures the static-server pod can connect to the +// static-client pod once the intention is set. +func (c *ConnectHelper) TestConnectionSuccess(t *testing.T) { + logger.Log(t, "checking that connection is successful") + if c.Cfg.EnableTransparentProxy { + // todo: add an assertion that the traffic is going through the proxy + k8s.CheckStaticServerConnectionSuccessful(t, c.Ctx.KubectlOptions(t), staticClientName, "http://static-server") + } else { + k8s.CheckStaticServerConnectionSuccessful(t, c.Ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") + } +} + +// TestConnectionFailureWhenUnhealthy sets the static-server pod to be unhealthy +// and ensures the connection fails. It restores the pod to a healthy state +// after this check. +func (c *ConnectHelper) TestConnectionFailureWhenUnhealthy(t *testing.T) { + // Test that kubernetes readiness status is synced to Consul. + // Create a file called "unhealthy" at "/tmp/" so that the readiness probe + // of the static-server pod fails. + logger.Log(t, "testing k8s -> consul health checks sync by making the static-server unhealthy") + k8s.RunKubectl(t, c.Ctx.KubectlOptions(t), "exec", "deploy/"+staticServerName, "--", "touch", "/tmp/unhealthy") + + // The readiness probe should take a moment to be reflected in Consul, + // CheckStaticServerConnection will retry until Consul marks the service + // instance unavailable for mesh traffic, causing the connection to fail. + // We are expecting a "connection reset by peer" error because in a case of + // health checks, there will be no healthy proxy host to connect to. + // That's why we can't assert that we receive an empty reply from server, + // which is the case when a connection is unsuccessful due to intentions in + // other tests. + logger.Log(t, "checking that connection is unsuccessful") + if c.Cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, c.Ctx.KubectlOptions(t), staticClientName, false, []string{ + "curl: (56) Recv failure: Connection reset by peer", + "curl: (52) Empty reply from server", + "curl: (7) Failed to connect to static-server port 80: Connection refused", + }, "", "http://static-server") + } else { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, c.Ctx.KubectlOptions(t), staticClientName, false, []string{ + "curl: (56) Recv failure: Connection reset by peer", + "curl: (52) Empty reply from server", + }, "", "http://localhost:1234") + } + + // Return the static-server to a "healthy state". + k8s.RunKubectl(t, c.Ctx.KubectlOptions(t), "exec", "deploy/"+staticServerName, "--", "rm", "/tmp/unhealthy") +} + +// helmValues uses the Secure and AutoEncrypt fields to set values for the Helm +// Chart which are merged with the HelmValues field with the values set by the +// Secure and AutoEncrypt fields taking precedence. +func (c *ConnectHelper) helmValues() map[string]string { + helmValues := map[string]string{ + "connectInject.enabled": "true", + "global.tls.enabled": strconv.FormatBool(c.Secure), + "global.tls.enableAutoEncrypt": strconv.FormatBool(c.AutoEncrypt), + "global.acls.manageSystemACLs": strconv.FormatBool(c.Secure), + } + + helpers.MergeMaps(helmValues, c.HelmValues) + + return helmValues +} diff --git a/charts/consul/test/acceptance/tests/connect/connect_inject_namespaces_test.go b/acceptance/tests/connect/connect_inject_namespaces_test.go similarity index 89% rename from charts/consul/test/acceptance/tests/connect/connect_inject_namespaces_test.go rename to acceptance/tests/connect/connect_inject_namespaces_test.go index 56948b215e..815b28dea4 100644 --- a/charts/consul/test/acceptance/tests/connect/connect_inject_namespaces_test.go +++ b/acceptance/tests/connect/connect_inject_namespaces_test.go @@ -8,10 +8,10 @@ import ( "testing" terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" @@ -177,36 +177,41 @@ func TestConnectInjectNamespaces(t *testing.T) { if c.secure { logger.Log(t, "checking that the connection is not successful because there's no intention") if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, staticClientName, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) } else { - k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, "http://localhost:1234") + k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, staticClientName, "http://localhost:1234") } - intention := &api.Intention{ - SourceName: staticClientName, - SourceNS: staticClientNamespace, - DestinationName: staticServerName, - DestinationNS: staticServerNamespace, - Action: api.IntentionActionAllow, + intention := &api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: staticServerName, + Namespace: staticServerNamespace, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Namespace: staticClientNamespace, + Action: api.IntentionActionAllow, + }, + }, } // Set the destination namespace to be the same // unless mirrorK8S is true. if !c.mirrorK8S { - intention.SourceNS = c.destinationNamespace - intention.DestinationNS = c.destinationNamespace + intention.Namespace = c.destinationNamespace + intention.Sources[0].Namespace = c.destinationNamespace } logger.Log(t, "creating intention") - _, err := consulClient.Connect().IntentionUpsert(intention, nil) + _, _, err := consulClient.ConfigEntries().Set(intention, nil) require.NoError(t, err) } logger.Log(t, "checking that connection is successful") if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) } else { - k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, "http://localhost:1234") } // Test that kubernetes readiness status is synced to Consul. @@ -221,9 +226,9 @@ func TestConnectInjectNamespaces(t *testing.T) { // from server, which is the case when a connection is unsuccessful due to intentions in other tests. logger.Log(t, "checking that connection is unsuccessful") if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.%s", staticServerNamespace)) } else { - k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "http://localhost:1234") + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "", "http://localhost:1234") } }) } diff --git a/acceptance/tests/connect/connect_inject_test.go b/acceptance/tests/connect/connect_inject_test.go new file mode 100644 index 0000000000..c8737db0b6 --- /dev/null +++ b/acceptance/tests/connect/connect_inject_test.go @@ -0,0 +1,430 @@ +package connect + +import ( + "context" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestConnectInject tests that Connect works in a default and a secure installation. +func TestConnectInject(t *testing.T) { + cases := map[string]struct { + clusterKind consul.ClusterKind + releaseName string + secure bool + autoEncrypt bool + }{ + "Helm install without secure or auto-encrypt": { + clusterKind: consul.Helm, + releaseName: helpers.RandomName(), + }, + "Helm install with secure": { + clusterKind: consul.Helm, + releaseName: helpers.RandomName(), + secure: true, + }, + "Helm install with secure and auto-encrypt": { + clusterKind: consul.Helm, + releaseName: helpers.RandomName(), + secure: true, + autoEncrypt: true, + }, + "CLI install without secure or auto-encrypt": { + clusterKind: consul.CLI, + releaseName: consul.CLIReleaseName, + }, + "CLI install with secure": { + clusterKind: consul.CLI, + releaseName: consul.CLIReleaseName, + secure: true, + }, + "CLI install with secure and auto-encrypt": { + clusterKind: consul.CLI, + releaseName: consul.CLIReleaseName, + secure: true, + autoEncrypt: true, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + cfg := suite.Config() + ctx := suite.Environment().DefaultContext(t) + + connHelper := ConnectHelper{ + ClusterKind: c.clusterKind, + Secure: c.secure, + AutoEncrypt: c.autoEncrypt, + ReleaseName: c.releaseName, + Ctx: ctx, + Cfg: cfg, + } + + connHelper.Setup(t) + + connHelper.Install(t) + connHelper.DeployClientAndServer(t) + if c.secure { + connHelper.TestConnectionFailureWithoutIntention(t) + connHelper.CreateIntention(t) + } + connHelper.TestConnectionSuccess(t) + connHelper.TestConnectionFailureWhenUnhealthy(t) + }) + } +} + +// TestConnectInjectOnUpgrade tests that Connect works before and after an +// upgrade is performed on the cluster. +func TestConnectInjectOnUpgrade(t *testing.T) { + cases := map[string]struct { + clusterKind consul.ClusterKind + releaseName string + initial, upgrade map[string]string + }{ + "CLI upgrade changes nothing": { + clusterKind: consul.CLI, + releaseName: consul.CLIReleaseName, + }, + "CLI upgrade to enable ingressGateway": { + clusterKind: consul.CLI, + releaseName: consul.CLIReleaseName, + initial: map[string]string{}, + upgrade: map[string]string{ + "ingressGateways.enabled": "true", + "ingressGateways.defaults.replicas": "1", + }, + }, + "CLI upgrade to enable UI": { + clusterKind: consul.CLI, + releaseName: consul.CLIReleaseName, + initial: map[string]string{}, + upgrade: map[string]string{ + "ui.enabled": "true", + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + cfg := suite.Config() + ctx := suite.Environment().DefaultContext(t) + + connHelper := ConnectHelper{ + ClusterKind: c.clusterKind, + HelmValues: c.initial, + ReleaseName: c.releaseName, + Ctx: ctx, + Cfg: cfg, + } + + connHelper.Setup(t) + + connHelper.Install(t) + connHelper.DeployClientAndServer(t) + connHelper.TestConnectionSuccess(t) + connHelper.TestConnectionFailureWhenUnhealthy(t) + + connHelper.HelmValues = c.upgrade + + connHelper.Upgrade(t) + connHelper.TestConnectionSuccess(t) + connHelper.TestConnectionFailureWhenUnhealthy(t) + }) + } +} + +// Test the endpoints controller cleans up force-killed pods. +func TestConnectInject_CleanupKilledPods(t *testing.T) { + cases := []struct { + secure bool + autoEncrypt bool + }{ + {false, false}, + {true, false}, + {true, true}, + } + + for _, c := range cases { + name := fmt.Sprintf("secure: %t; auto-encrypt: %t", c.secure, c.autoEncrypt) + t.Run(name, func(t *testing.T) { + cfg := suite.Config() + ctx := suite.Environment().DefaultContext(t) + + helmValues := map[string]string{ + "connectInject.enabled": "true", + "global.tls.enabled": strconv.FormatBool(c.secure), + "global.tls.enableAutoEncrypt": strconv.FormatBool(c.autoEncrypt), + "global.acls.manageSystemACLs": strconv.FormatBool(c.secure), + } + + releaseName := helpers.RandomName() + consulCluster := consul.NewHelmCluster(t, helmValues, ctx, cfg, releaseName) + + consulCluster.Create(t) + + logger.Log(t, "creating static-client deployment") + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") + + logger.Log(t, "waiting for static-client to be registered with Consul") + consulClient := consulCluster.SetupConsulClient(t, c.secure) + retry.Run(t, func(r *retry.R) { + for _, name := range []string{"static-client", "static-client-sidecar-proxy"} { + instances, _, err := consulClient.Catalog().Service(name, "", nil) + r.Check(err) + + if len(instances) != 1 { + r.Errorf("expected 1 instance of %s", name) + } + } + }) + + ns := ctx.KubectlOptions(t).Namespace + pods, err := ctx.KubernetesClient(t).CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{LabelSelector: "app=static-client"}) + require.NoError(t, err) + require.Len(t, pods.Items, 1) + podName := pods.Items[0].Name + + logger.Logf(t, "force killing the static-client pod %q", podName) + var gracePeriod int64 = 0 + err = ctx.KubernetesClient(t).CoreV1().Pods(ns).Delete(context.Background(), podName, metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod}) + require.NoError(t, err) + + logger.Log(t, "ensuring pod is deregistered") + retry.Run(t, func(r *retry.R) { + for _, name := range []string{"static-client", "static-client-sidecar-proxy"} { + instances, _, err := consulClient.Catalog().Service(name, "", nil) + r.Check(err) + + for _, instance := range instances { + if strings.Contains(instance.ServiceID, podName) { + r.Errorf("%s is still registered", instance.ServiceID) + } + } + } + }) + }) + } +} + +// Test that when Consul clients are restarted and lose all their registrations, +// the services get re-registered and can continue to talk to each other. +func TestConnectInject_RestartConsulClients(t *testing.T) { + cfg := suite.Config() + ctx := suite.Environment().DefaultContext(t) + + helmValues := map[string]string{ + "connectInject.enabled": "true", + } + + releaseName := helpers.RandomName() + consulCluster := consul.NewHelmCluster(t, helmValues, ctx, cfg, releaseName) + + consulCluster.Create(t) + + logger.Log(t, "creating static-server and static-client deployments") + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + if cfg.EnableTransparentProxy { + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") + } else { + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") + } + + logger.Log(t, "checking that connection is successful") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://static-server") + } else { + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") + } + + logger.Log(t, "restarting Consul client daemonset") + k8s.RunKubectl(t, ctx.KubectlOptions(t), "rollout", "restart", fmt.Sprintf("ds/%s-consul-client", releaseName)) + k8s.RunKubectl(t, ctx.KubectlOptions(t), "rollout", "status", fmt.Sprintf("ds/%s-consul-client", releaseName)) + + logger.Log(t, "checking that connection is still successful") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://static-server") + } else { + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") + } +} + +const multiport = "multiport" +const multiportAdmin = "multiport-admin" + +// Test that Connect works for an application with multiple ports. The multiport application is a Pod listening on +// two ports. This tests inbound connections to each port of the multiport app, and outbound connections from the +// multiport app to static-server. +func TestConnectInject_MultiportServices(t *testing.T) { + cases := []struct { + secure bool + autoEncrypt bool + }{ + {false, false}, + {true, false}, + {true, true}, + } + + for _, c := range cases { + name := fmt.Sprintf("secure: %t; auto-encrypt: %t", c.secure, c.autoEncrypt) + t.Run(name, func(t *testing.T) { + cfg := suite.Config() + ctx := suite.Environment().DefaultContext(t) + + // Multi port apps don't work with transparent proxy. + if cfg.EnableTransparentProxy { + t.Skipf("skipping this test because transparent proxy is enabled") + } + + helmValues := map[string]string{ + "connectInject.enabled": "true", + + "global.tls.enabled": strconv.FormatBool(c.secure), + "global.tls.enableAutoEncrypt": strconv.FormatBool(c.autoEncrypt), + "global.acls.manageSystemACLs": strconv.FormatBool(c.secure), + } + + releaseName := helpers.RandomName() + consulCluster := consul.NewHelmCluster(t, helmValues, ctx, cfg, releaseName) + + consulCluster.Create(t) + + consulClient := consulCluster.SetupConsulClient(t, c.secure) + + // Check that the ACL token is deleted. + if c.secure { + // We need to register the cleanup function before we create the deployments + // because golang will execute them in reverse order i.e. the last registered + // cleanup function will be executed first. + t.Cleanup(func() { + retrier := &retry.Timer{Timeout: 5 * time.Minute, Wait: 1 * time.Second} + retry.RunWith(retrier, t, func(r *retry.R) { + tokens, _, err := consulClient.ACL().TokenList(nil) + require.NoError(r, err) + for _, token := range tokens { + require.NotContains(r, token.Description, multiport) + require.NotContains(r, token.Description, multiportAdmin) + require.NotContains(r, token.Description, staticClientName) + require.NotContains(r, token.Description, staticServerName) + } + }) + }) + } + + logger.Log(t, "creating multiport static-server and static-client deployments") + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/bases/multiport-app") + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject-multiport") + + // Check that static-client has been injected and now has 2 containers. + podList, err := ctx.KubernetesClient(t).CoreV1().Pods(ctx.KubectlOptions(t).Namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: "app=static-client", + }) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Len(t, podList.Items[0].Spec.Containers, 2) + + // Check that multiport has been injected and now has 4 containers. + podList, err = ctx.KubernetesClient(t).CoreV1().Pods(ctx.KubectlOptions(t).Namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: "app=multiport", + }) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Len(t, podList.Items[0].Spec.Containers, 4) + + if c.secure { + logger.Log(t, "checking that the connection is not successful because there's no intention") + k8s.CheckStaticServerConnectionFailing(t, ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") + k8s.CheckStaticServerConnectionFailing(t, ctx.KubectlOptions(t), staticClientName, "http://localhost:2234") + + logger.Log(t, fmt.Sprintf("creating intention for %s", multiport)) + _, _, err := consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: multiport, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Action: api.IntentionActionAllow, + }, + }, + }, nil) + require.NoError(t, err) + logger.Log(t, fmt.Sprintf("creating intention for %s", multiportAdmin)) + _, _, err = consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: multiportAdmin, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Action: api.IntentionActionAllow, + }, + }, + }, nil) + require.NoError(t, err) + } + + // Check connection from static-client to multiport. + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") + + // Check connection from static-client to multiport-admin. + k8s.CheckStaticServerConnectionSuccessfulWithMessage(t, ctx.KubectlOptions(t), staticClientName, "hello world from 9090 admin", "http://localhost:2234") + + // Now that we've checked inbound connections to a multi port pod, check outbound connection from multi port + // pod to static-server. + + // Deploy static-server. + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + + // For outbound connections from the multi port pod, only intentions from the first service in the multiport + // pod need to be created, since all upstream connections are made through the first service's envoy proxy. + if c.secure { + logger.Log(t, "checking that the connection is not successful because there's no intention") + + k8s.CheckStaticServerConnectionFailing(t, ctx.KubectlOptions(t), multiport, "http://localhost:3234") + + logger.Log(t, fmt.Sprintf("creating intention for %s", staticServerName)) + _, _, err := consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: staticServerName, + Sources: []*api.SourceIntention{ + { + Name: multiport, + Action: api.IntentionActionAllow, + }, + }, + }, nil) + require.NoError(t, err) + } + + // Check the connection from the multi port pod to static-server. + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), multiport, "http://localhost:3234") + + // Test that kubernetes readiness status is synced to Consul. This will make the multi port pods unhealthy + // and check inbound connections to the multi port pods' services. + // Create the files so that the readiness probes of the multi port pod fails. + logger.Log(t, "testing k8s -> consul health checks sync by making the multiport unhealthy") + k8s.RunKubectl(t, ctx.KubectlOptions(t), "exec", "deploy/"+multiport, "--", "touch", "/tmp/unhealthy-multiport") + logger.Log(t, "testing k8s -> consul health checks sync by making the multiport-admin unhealthy") + k8s.RunKubectl(t, ctx.KubectlOptions(t), "exec", "deploy/"+multiport, "--", "touch", "/tmp/unhealthy-multiport-admin") + + // The readiness probe should take a moment to be reflected in Consul, CheckStaticServerConnection will retry + // until Consul marks the service instance unavailable for mesh traffic, causing the connection to fail. + // We are expecting a "connection reset by peer" error because in a case of health checks, + // there will be no healthy proxy host to connect to. That's why we can't assert that we receive an empty reply + // from server, which is the case when a connection is unsuccessful due to intentions in other tests. + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, ctx.KubectlOptions(t), staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "", "http://localhost:1234") + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, ctx.KubectlOptions(t), staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "", "http://localhost:2234") + }) + } +} diff --git a/charts/consul/test/acceptance/tests/connect/main_test.go b/acceptance/tests/connect/main_test.go similarity index 63% rename from charts/consul/test/acceptance/tests/connect/main_test.go rename to acceptance/tests/connect/main_test.go index f57bf14e07..a2b5925bed 100644 --- a/charts/consul/test/acceptance/tests/connect/main_test.go +++ b/acceptance/tests/connect/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/consul-dns/consul_dns_test.go b/acceptance/tests/consul-dns/consul_dns_test.go similarity index 92% rename from charts/consul/test/acceptance/tests/consul-dns/consul_dns_test.go rename to acceptance/tests/consul-dns/consul_dns_test.go index 3814bf7672..126b97df81 100644 --- a/charts/consul/test/acceptance/tests/consul-dns/consul_dns_test.go +++ b/acceptance/tests/consul-dns/consul_dns_test.go @@ -6,9 +6,9 @@ import ( "strconv" "testing" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/charts/consul/test/acceptance/tests/consul-dns/main_test.go b/acceptance/tests/consul-dns/main_test.go similarity index 63% rename from charts/consul/test/acceptance/tests/consul-dns/main_test.go rename to acceptance/tests/consul-dns/main_test.go index 2c5096e90f..848f30ad8f 100644 --- a/charts/consul/test/acceptance/tests/consul-dns/main_test.go +++ b/acceptance/tests/consul-dns/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/controller/controller_namespaces_test.go b/acceptance/tests/controller/controller_namespaces_test.go similarity index 89% rename from charts/consul/test/acceptance/tests/controller/controller_namespaces_test.go rename to acceptance/tests/controller/controller_namespaces_test.go index 157d7e5fc6..b590d05b9e 100644 --- a/charts/consul/test/acceptance/tests/controller/controller_namespaces_test.go +++ b/acceptance/tests/controller/controller_namespaces_test.go @@ -7,10 +7,10 @@ import ( "testing" "time" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" @@ -74,9 +74,10 @@ func TestControllerNamespaces(t *testing.T) { ctx := suite.Environment().DefaultContext(t) helmValues := map[string]string{ - "global.enableConsulNamespaces": "true", - "controller.enabled": "true", - "connectInject.enabled": "true", + "global.enableConsulNamespaces": "true", + "global.adminPartitions.enabled": "true", + "controller.enabled": "true", + "connectInject.enabled": "true", // When mirroringK8S is set, this setting is ignored. "connectInject.consulNamespaces.consulDestinationNamespace": c.destinationNamespace, @@ -122,7 +123,7 @@ func TestControllerNamespaces(t *testing.T) { // Retry the kubectl apply because we've seen sporadic // "connection refused" errors where the mutating webhook // endpoint fails initially. - out, err := k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "apply", "-n", KubeNS, "-f", "../fixtures/crds") + out, err := k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "apply", "-n", KubeNS, "-k", "../fixtures/cases/crds-ent") require.NoError(r, err, out) // NOTE: No need to clean up because the namespace will be deleted. }) @@ -153,6 +154,13 @@ func TestControllerNamespaces(t *testing.T) { require.True(r, ok, "could not cast to ProxyConfigEntry") require.Equal(r, api.MeshGatewayModeLocal, proxyDefaultEntry.MeshGateway.Mode) + // exported-services + entry, _, err = consulClient.ConfigEntries().Get(api.ExportedServices, "default", defaultOpts) + require.NoError(r, err) + exportedServicesEntry, ok := entry.(*api.ExportedServicesConfigEntry) + require.True(r, ok, "could not cast to ExportedServicesConfigEntry") + require.Equal(r, "frontend", exportedServicesEntry.Services[0].Name) + // mesh entry, _, err = consulClient.ConfigEntries().Get(api.MeshConfig, "mesh", defaultOpts) require.NoError(r, err) @@ -220,6 +228,10 @@ func TestControllerNamespaces(t *testing.T) { patchMeshGatewayMode := "remote" k8s.RunKubectl(t, ctx.KubectlOptions(t), "patch", "-n", KubeNS, "proxydefaults", "global", "-p", fmt.Sprintf(`{"spec":{"meshGateway":{"mode": "%s"}}}`, patchMeshGatewayMode), "--type=merge") + logger.Log(t, "patching partition-exports custom resource") + patchServiceName := "backend" + k8s.RunKubectl(t, ctx.KubectlOptions(t), "patch", "-n", KubeNS, "exportedservices", "default", "-p", fmt.Sprintf(`{"spec":{"services":[{"name": "%s", "namespace": "front", "consumers":[{"partition": "foo"}]}]}}`, patchServiceName), "--type=merge") + logger.Log(t, "patching mesh custom resource") k8s.RunKubectl(t, ctx.KubectlOptions(t), "patch", "-n", KubeNS, "mesh", "mesh", "-p", fmt.Sprintf(`{"spec":{"transparentProxy":{"meshDestinationsOnly": %t}}}`, false), "--type=merge") @@ -264,6 +276,13 @@ func TestControllerNamespaces(t *testing.T) { require.True(r, ok, "could not cast to ProxyConfigEntry") require.Equal(r, api.MeshGatewayModeRemote, proxyDefaultsEntry.MeshGateway.Mode) + // partition-exports + entry, _, err = consulClient.ConfigEntries().Get(api.ExportedServices, "default", defaultOpts) + require.NoError(r, err) + exportedServicesEntry, ok := entry.(*api.ExportedServicesConfigEntry) + require.True(r, ok, "could not cast to ExportedServicesConfigEntry") + require.Equal(r, "backend", exportedServicesEntry.Services[0].Name) + // mesh entry, _, err = consulClient.ConfigEntries().Get(api.MeshConfig, "mesh", defaultOpts) require.NoError(r, err) @@ -321,6 +340,9 @@ func TestControllerNamespaces(t *testing.T) { logger.Log(t, "deleting proxy-defaults custom resource") k8s.RunKubectl(t, ctx.KubectlOptions(t), "delete", "-n", KubeNS, "proxydefaults", "global") + logger.Log(t, "deleting partition-exports custom resource") + k8s.RunKubectl(t, ctx.KubectlOptions(t), "delete", "-n", KubeNS, "exportedservices", "default") + logger.Log(t, "deleting mesh custom resource") k8s.RunKubectl(t, ctx.KubectlOptions(t), "delete", "-n", KubeNS, "mesh", "mesh") @@ -356,6 +378,11 @@ func TestControllerNamespaces(t *testing.T) { require.Error(r, err) require.Contains(r, err.Error(), "404 (Config entry not found") + // partition-exports + _, _, err = consulClient.ConfigEntries().Get(api.ExportedServices, "default", defaultOpts) + require.Error(r, err) + require.Contains(r, err.Error(), "404 (Config entry not found") + // mesh _, _, err = consulClient.ConfigEntries().Get(api.MeshConfig, "mesh", defaultOpts) require.Error(r, err) diff --git a/charts/consul/test/acceptance/tests/controller/controller_test.go b/acceptance/tests/controller/controller_test.go similarity index 97% rename from charts/consul/test/acceptance/tests/controller/controller_test.go rename to acceptance/tests/controller/controller_test.go index ba22f3c61c..e6405f3ccf 100644 --- a/charts/consul/test/acceptance/tests/controller/controller_test.go +++ b/acceptance/tests/controller/controller_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" @@ -58,12 +58,12 @@ func TestController(t *testing.T) { // Retry the kubectl apply because we've seen sporadic // "connection refused" errors where the mutating webhook // endpoint fails initially. - out, err := k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "apply", "-f", "../fixtures/crds") + out, err := k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "apply", "-k", "../fixtures/bases/crds-oss") require.NoError(r, err, out) helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { // Ignore errors here because if the test ran as expected // the custom resources will have been deleted. - k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "delete", "-f", "../fixtures/crds") + k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "delete", "-k", "../fixtures/bases/crds-oss") }) }) diff --git a/charts/consul/test/acceptance/tests/controller/main_test.go b/acceptance/tests/controller/main_test.go similarity index 64% rename from charts/consul/test/acceptance/tests/controller/main_test.go rename to acceptance/tests/controller/main_test.go index ebdd540513..115627fb85 100644 --- a/charts/consul/test/acceptance/tests/controller/main_test.go +++ b/acceptance/tests/controller/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testSuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testSuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testSuite.Suite diff --git a/charts/consul/test/acceptance/tests/example/example_test.go b/acceptance/tests/example/example_test.go similarity index 89% rename from charts/consul/test/acceptance/tests/example/example_test.go rename to acceptance/tests/example/example_test.go index 8f23ceab8a..4be7c3db7d 100644 --- a/charts/consul/test/acceptance/tests/example/example_test.go +++ b/acceptance/tests/example/example_test.go @@ -5,9 +5,9 @@ import ( "context" "testing" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/charts/consul/test/acceptance/tests/example/main_test.go b/acceptance/tests/example/main_test.go similarity index 89% rename from charts/consul/test/acceptance/tests/example/main_test.go rename to acceptance/tests/example/main_test.go index 8c6b31da89..323f421d32 100644 --- a/charts/consul/test/acceptance/tests/example/main_test.go +++ b/acceptance/tests/example/main_test.go @@ -4,7 +4,7 @@ package example import ( "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/ingressgateway.yaml b/acceptance/tests/fixtures/bases/crds-oss/ingressgateway.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/ingressgateway.yaml rename to acceptance/tests/fixtures/bases/crds-oss/ingressgateway.yaml diff --git a/acceptance/tests/fixtures/bases/crds-oss/kustomization.yaml b/acceptance/tests/fixtures/bases/crds-oss/kustomization.yaml new file mode 100644 index 0000000000..040b64f155 --- /dev/null +++ b/acceptance/tests/fixtures/bases/crds-oss/kustomization.yaml @@ -0,0 +1,10 @@ +resources: + - ingressgateway.yaml + - mesh.yaml + - proxydefaults.yaml + - servicedefaults.yaml + - serviceintentions.yaml + - serviceresolver.yaml + - servicerouter.yaml + - servicesplitter.yaml + - terminatinggateway.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/mesh.yaml b/acceptance/tests/fixtures/bases/crds-oss/mesh.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/mesh.yaml rename to acceptance/tests/fixtures/bases/crds-oss/mesh.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/proxydefaults.yaml b/acceptance/tests/fixtures/bases/crds-oss/proxydefaults.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/proxydefaults.yaml rename to acceptance/tests/fixtures/bases/crds-oss/proxydefaults.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/servicedefaults.yaml b/acceptance/tests/fixtures/bases/crds-oss/servicedefaults.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/servicedefaults.yaml rename to acceptance/tests/fixtures/bases/crds-oss/servicedefaults.yaml diff --git a/acceptance/tests/fixtures/bases/crds-oss/serviceexports.yaml b/acceptance/tests/fixtures/bases/crds-oss/serviceexports.yaml new file mode 100644 index 0000000000..8ae095cd8e --- /dev/null +++ b/acceptance/tests/fixtures/bases/crds-oss/serviceexports.yaml @@ -0,0 +1,10 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceExports +metadata: + name: exports +spec: + services: + - name: frontend + namespace: frontend + consumers: + - partition: other \ No newline at end of file diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/serviceintentions.yaml b/acceptance/tests/fixtures/bases/crds-oss/serviceintentions.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/serviceintentions.yaml rename to acceptance/tests/fixtures/bases/crds-oss/serviceintentions.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/serviceresolver.yaml b/acceptance/tests/fixtures/bases/crds-oss/serviceresolver.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/serviceresolver.yaml rename to acceptance/tests/fixtures/bases/crds-oss/serviceresolver.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/servicerouter.yaml b/acceptance/tests/fixtures/bases/crds-oss/servicerouter.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/servicerouter.yaml rename to acceptance/tests/fixtures/bases/crds-oss/servicerouter.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/servicesplitter.yaml b/acceptance/tests/fixtures/bases/crds-oss/servicesplitter.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/servicesplitter.yaml rename to acceptance/tests/fixtures/bases/crds-oss/servicesplitter.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/crds/terminatinggateway.yaml b/acceptance/tests/fixtures/bases/crds-oss/terminatinggateway.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/crds/terminatinggateway.yaml rename to acceptance/tests/fixtures/bases/crds-oss/terminatinggateway.yaml diff --git a/acceptance/tests/fixtures/bases/exportedservices-default/exportedservices-default.yaml b/acceptance/tests/fixtures/bases/exportedservices-default/exportedservices-default.yaml new file mode 100644 index 0000000000..a260602109 --- /dev/null +++ b/acceptance/tests/fixtures/bases/exportedservices-default/exportedservices-default.yaml @@ -0,0 +1,6 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: default +spec: + services: [] diff --git a/acceptance/tests/fixtures/bases/exportedservices-default/kustomization.yaml b/acceptance/tests/fixtures/bases/exportedservices-default/kustomization.yaml new file mode 100644 index 0000000000..e540a4def1 --- /dev/null +++ b/acceptance/tests/fixtures/bases/exportedservices-default/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - exportedservices-default.yaml diff --git a/acceptance/tests/fixtures/bases/exportedservices-secondary/exportedservices-secondary.yaml b/acceptance/tests/fixtures/bases/exportedservices-secondary/exportedservices-secondary.yaml new file mode 100644 index 0000000000..a514ed50d9 --- /dev/null +++ b/acceptance/tests/fixtures/bases/exportedservices-secondary/exportedservices-secondary.yaml @@ -0,0 +1,6 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: secondary +spec: + services: [] diff --git a/acceptance/tests/fixtures/bases/exportedservices-secondary/kustomization.yaml b/acceptance/tests/fixtures/bases/exportedservices-secondary/kustomization.yaml new file mode 100644 index 0000000000..10af8e20c5 --- /dev/null +++ b/acceptance/tests/fixtures/bases/exportedservices-secondary/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - exportedservices-secondary.yaml diff --git a/acceptance/tests/fixtures/bases/intention/intention.yaml b/acceptance/tests/fixtures/bases/intention/intention.yaml new file mode 100644 index 0000000000..c7bf26dac2 --- /dev/null +++ b/acceptance/tests/fixtures/bases/intention/intention.yaml @@ -0,0 +1,10 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceIntentions +metadata: + name: client-to-server +spec: + destination: + name: static-server + sources: + - name: static-client + action: allow diff --git a/acceptance/tests/fixtures/bases/intention/kustomization.yaml b/acceptance/tests/fixtures/bases/intention/kustomization.yaml new file mode 100644 index 0000000000..8d15c05511 --- /dev/null +++ b/acceptance/tests/fixtures/bases/intention/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - intention.yaml \ No newline at end of file diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/mesh-gateway/kustomization.yaml b/acceptance/tests/fixtures/bases/mesh-gateway/kustomization.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/mesh-gateway/kustomization.yaml rename to acceptance/tests/fixtures/bases/mesh-gateway/kustomization.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/mesh-gateway/proxydefaults.yaml b/acceptance/tests/fixtures/bases/mesh-gateway/proxydefaults.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/mesh-gateway/proxydefaults.yaml rename to acceptance/tests/fixtures/bases/mesh-gateway/proxydefaults.yaml diff --git a/acceptance/tests/fixtures/bases/multiport-app/anyuid-scc-rolebinding.yaml b/acceptance/tests/fixtures/bases/multiport-app/anyuid-scc-rolebinding.yaml new file mode 100644 index 0000000000..f80bd41d81 --- /dev/null +++ b/acceptance/tests/fixtures/bases/multiport-app/anyuid-scc-rolebinding.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: multiport-openshift-anyuid +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:openshift:scc:anyuid +subjects: + - kind: ServiceAccount + name: multiport +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: multiport-admin-openshift-anyuid +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:openshift:scc:anyuid +subjects: + - kind: ServiceAccount + name: multiport-admin diff --git a/acceptance/tests/fixtures/bases/multiport-app/deployment.yaml b/acceptance/tests/fixtures/bases/multiport-app/deployment.yaml new file mode 100644 index 0000000000..a99d415d80 --- /dev/null +++ b/acceptance/tests/fixtures/bases/multiport-app/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: multiport +spec: + replicas: 1 + selector: + matchLabels: + app: multiport + template: + metadata: + name: multiport + labels: + app: multiport + annotations: + "consul.hashicorp.com/connect-inject": "true" + 'consul.hashicorp.com/connect-service': 'multiport,multiport-admin' + 'consul.hashicorp.com/connect-service-upstreams': 'static-server:3234' + 'consul.hashicorp.com/connect-service-port': '8080,9090' + 'consul.hashicorp.com/transparent-proxy': 'false' + 'consul.hashicorp.com/enable-metrics': 'false' + 'consul.hashicorp.com/enable-metrics-merging': 'false' + spec: + containers: + - name: multiport + image: docker.mirror.hashicorp.services/hashicorp/http-echo:alpine + args: + - -text="hello world" + - -listen=:8080 + ports: + - containerPort: 8080 + name: http + livenessProbe: + httpGet: + port: 8080 + initialDelaySeconds: 1 + failureThreshold: 1 + periodSeconds: 1 + startupProbe: + httpGet: + port: 8080 + initialDelaySeconds: 1 + failureThreshold: 30 + periodSeconds: 1 + readinessProbe: + exec: + command: ['sh', '-c', 'test ! -f /tmp/unhealthy-multiport'] + initialDelaySeconds: 1 + failureThreshold: 1 + periodSeconds: 1 + - name: multiport-admin + image: docker.mirror.hashicorp.services/hashicorp/http-echo:alpine + args: + - -text="hello world from 9090 admin" + - -listen=:9090 + ports: + - containerPort: 9090 + name: http + livenessProbe: + httpGet: + port: 9090 + initialDelaySeconds: 1 + failureThreshold: 1 + periodSeconds: 1 + startupProbe: + httpGet: + port: 9090 + initialDelaySeconds: 1 + failureThreshold: 30 + periodSeconds: 1 + readinessProbe: + exec: + command: ['sh', '-c', 'test ! -f /tmp/unhealthy-multiport-admin'] + initialDelaySeconds: 1 + failureThreshold: 1 + periodSeconds: 1 + serviceAccountName: multiport + terminationGracePeriodSeconds: 0 # so deletion is quick diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-client/kustomization.yaml b/acceptance/tests/fixtures/bases/multiport-app/kustomization.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-client/kustomization.yaml rename to acceptance/tests/fixtures/bases/multiport-app/kustomization.yaml diff --git a/acceptance/tests/fixtures/bases/multiport-app/privileged-scc-rolebinding.yaml b/acceptance/tests/fixtures/bases/multiport-app/privileged-scc-rolebinding.yaml new file mode 100644 index 0000000000..f909785b36 --- /dev/null +++ b/acceptance/tests/fixtures/bases/multiport-app/privileged-scc-rolebinding.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: multiport-openshift-privileged +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:openshift:scc:privileged +subjects: + - kind: ServiceAccount + name: multiport +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: multiport-admin-openshift-privileged +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:openshift:scc:privileged +subjects: + - kind: ServiceAccount + name: multiport-admin diff --git a/acceptance/tests/fixtures/bases/multiport-app/psp-rolebinding.yaml b/acceptance/tests/fixtures/bases/multiport-app/psp-rolebinding.yaml new file mode 100644 index 0000000000..fce63f0076 --- /dev/null +++ b/acceptance/tests/fixtures/bases/multiport-app/psp-rolebinding.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: multiport +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: test-psp +subjects: + - kind: ServiceAccount + name: multiport +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: multiport-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: test-psp +subjects: + - kind: ServiceAccount + name: multiport-admin diff --git a/acceptance/tests/fixtures/bases/multiport-app/service.yaml b/acceptance/tests/fixtures/bases/multiport-app/service.yaml new file mode 100644 index 0000000000..d18da258a3 --- /dev/null +++ b/acceptance/tests/fixtures/bases/multiport-app/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: multiport +spec: + selector: + app: multiport + ports: + - name: http + port: 80 + targetPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: multiport-admin +spec: + selector: + app: multiport + ports: + - protocol: TCP + port: 80 + targetPort: 9090 diff --git a/acceptance/tests/fixtures/bases/multiport-app/serviceaccount.yaml b/acceptance/tests/fixtures/bases/multiport-app/serviceaccount.yaml new file mode 100644 index 0000000000..2293c2e173 --- /dev/null +++ b/acceptance/tests/fixtures/bases/multiport-app/serviceaccount.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: multiport +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: multiport-admin diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-client/anyuid-scc-rolebinding.yaml b/acceptance/tests/fixtures/bases/static-client/anyuid-scc-rolebinding.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-client/anyuid-scc-rolebinding.yaml rename to acceptance/tests/fixtures/bases/static-client/anyuid-scc-rolebinding.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-client/deployment.yaml b/acceptance/tests/fixtures/bases/static-client/deployment.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-client/deployment.yaml rename to acceptance/tests/fixtures/bases/static-client/deployment.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-server/kustomization.yaml b/acceptance/tests/fixtures/bases/static-client/kustomization.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-server/kustomization.yaml rename to acceptance/tests/fixtures/bases/static-client/kustomization.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-client/privileged-scc-rolebinding.yaml b/acceptance/tests/fixtures/bases/static-client/privileged-scc-rolebinding.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-client/privileged-scc-rolebinding.yaml rename to acceptance/tests/fixtures/bases/static-client/privileged-scc-rolebinding.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-client/psp-rolebinding.yaml b/acceptance/tests/fixtures/bases/static-client/psp-rolebinding.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-client/psp-rolebinding.yaml rename to acceptance/tests/fixtures/bases/static-client/psp-rolebinding.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-client/service.yaml b/acceptance/tests/fixtures/bases/static-client/service.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-client/service.yaml rename to acceptance/tests/fixtures/bases/static-client/service.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-client/serviceaccount.yaml b/acceptance/tests/fixtures/bases/static-client/serviceaccount.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-client/serviceaccount.yaml rename to acceptance/tests/fixtures/bases/static-client/serviceaccount.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-metrics-app/deployment.yaml b/acceptance/tests/fixtures/bases/static-metrics-app/deployment.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-metrics-app/deployment.yaml rename to acceptance/tests/fixtures/bases/static-metrics-app/deployment.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-metrics-app/kustomization.yaml b/acceptance/tests/fixtures/bases/static-metrics-app/kustomization.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-metrics-app/kustomization.yaml rename to acceptance/tests/fixtures/bases/static-metrics-app/kustomization.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-metrics-app/service.yaml b/acceptance/tests/fixtures/bases/static-metrics-app/service.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-metrics-app/service.yaml rename to acceptance/tests/fixtures/bases/static-metrics-app/service.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-server/anyuid-scc-rolebinding.yaml b/acceptance/tests/fixtures/bases/static-server/anyuid-scc-rolebinding.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-server/anyuid-scc-rolebinding.yaml rename to acceptance/tests/fixtures/bases/static-server/anyuid-scc-rolebinding.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-server/deployment.yaml b/acceptance/tests/fixtures/bases/static-server/deployment.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-server/deployment.yaml rename to acceptance/tests/fixtures/bases/static-server/deployment.yaml diff --git a/acceptance/tests/fixtures/bases/static-server/kustomization.yaml b/acceptance/tests/fixtures/bases/static-server/kustomization.yaml new file mode 100644 index 0000000000..b9d8e11f73 --- /dev/null +++ b/acceptance/tests/fixtures/bases/static-server/kustomization.yaml @@ -0,0 +1,7 @@ +resources: + - deployment.yaml + - service.yaml + - serviceaccount.yaml + - psp-rolebinding.yaml + - anyuid-scc-rolebinding.yaml + - privileged-scc-rolebinding.yaml \ No newline at end of file diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-server/privileged-scc-rolebinding.yaml b/acceptance/tests/fixtures/bases/static-server/privileged-scc-rolebinding.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-server/privileged-scc-rolebinding.yaml rename to acceptance/tests/fixtures/bases/static-server/privileged-scc-rolebinding.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-server/psp-rolebinding.yaml b/acceptance/tests/fixtures/bases/static-server/psp-rolebinding.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-server/psp-rolebinding.yaml rename to acceptance/tests/fixtures/bases/static-server/psp-rolebinding.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-server/service.yaml b/acceptance/tests/fixtures/bases/static-server/service.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-server/service.yaml rename to acceptance/tests/fixtures/bases/static-server/service.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/bases/static-server/serviceaccount.yaml b/acceptance/tests/fixtures/bases/static-server/serviceaccount.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/bases/static-server/serviceaccount.yaml rename to acceptance/tests/fixtures/bases/static-server/serviceaccount.yaml diff --git a/acceptance/tests/fixtures/cases/crd-partitions/default-partition-default/kustomization.yaml b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-default/kustomization.yaml new file mode 100644 index 0000000000..499fdc5bc1 --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-default/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/exportedservices-default + +patchesStrategicMerge: +- patch.yaml diff --git a/acceptance/tests/fixtures/cases/crd-partitions/default-partition-default/patch.yaml b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-default/patch.yaml new file mode 100644 index 0000000000..c98ecb6f48 --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-default/patch.yaml @@ -0,0 +1,14 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: default +spec: + services: + - name: mesh-gateway + namespace: default + consumers: + - partition: secondary + - name: static-server + namespace: default + consumers: + - partition: secondary diff --git a/acceptance/tests/fixtures/cases/crd-partitions/default-partition-ns1/kustomization.yaml b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-ns1/kustomization.yaml new file mode 100644 index 0000000000..499fdc5bc1 --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-ns1/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/exportedservices-default + +patchesStrategicMerge: +- patch.yaml diff --git a/acceptance/tests/fixtures/cases/crd-partitions/default-partition-ns1/patch.yaml b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-ns1/patch.yaml new file mode 100644 index 0000000000..f826174aec --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/default-partition-ns1/patch.yaml @@ -0,0 +1,14 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: default +spec: + services: + - name: mesh-gateway + namespace: default + consumers: + - partition: secondary + - name: static-server + namespace: ns1 + consumers: + - partition: secondary diff --git a/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-default/kustomization.yaml b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-default/kustomization.yaml new file mode 100644 index 0000000000..5a9c8412aa --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-default/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/exportedservices-secondary + +patchesStrategicMerge: +- patch.yaml diff --git a/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-default/patch.yaml b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-default/patch.yaml new file mode 100644 index 0000000000..d2fc1ab914 --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-default/patch.yaml @@ -0,0 +1,14 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: secondary +spec: + services: + - name: mesh-gateway + namespace: default + consumers: + - partition: default + - name: static-server + namespace: default + consumers: + - partition: default diff --git a/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-ns1/kustomization.yaml b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-ns1/kustomization.yaml new file mode 100644 index 0000000000..5a9c8412aa --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-ns1/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/exportedservices-secondary + +patchesStrategicMerge: +- patch.yaml diff --git a/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-ns1/patch.yaml b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-ns1/patch.yaml new file mode 100644 index 0000000000..4165f2d21a --- /dev/null +++ b/acceptance/tests/fixtures/cases/crd-partitions/secondary-partition-ns1/patch.yaml @@ -0,0 +1,14 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: secondary +spec: + services: + - name: mesh-gateway + namespace: default + consumers: + - partition: default + - name: static-server + namespace: ns1 + consumers: + - partition: default diff --git a/acceptance/tests/fixtures/cases/crds-ent/exportedservices.yaml b/acceptance/tests/fixtures/cases/crds-ent/exportedservices.yaml new file mode 100644 index 0000000000..4703d23493 --- /dev/null +++ b/acceptance/tests/fixtures/cases/crds-ent/exportedservices.yaml @@ -0,0 +1,10 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ExportedServices +metadata: + name: default +spec: + services: + - name: frontend + namespace: frontend + consumers: + - partition: other \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/crds-ent/kustomization.yaml b/acceptance/tests/fixtures/cases/crds-ent/kustomization.yaml new file mode 100644 index 0000000000..cdc3e60cb1 --- /dev/null +++ b/acceptance/tests/fixtures/cases/crds-ent/kustomization.yaml @@ -0,0 +1,3 @@ +resources: + - ../../bases/crds-oss + - exportedservices.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-inject/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-inject-multiport/kustomization.yaml similarity index 86% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-inject/kustomization.yaml rename to acceptance/tests/fixtures/cases/static-client-inject-multiport/kustomization.yaml index 974fbd4fe1..9834f91903 100644 --- a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-inject/kustomization.yaml +++ b/acceptance/tests/fixtures/cases/static-client-inject-multiport/kustomization.yaml @@ -1,4 +1,4 @@ -bases: +resources: - ../../bases/static-client patchesStrategicMerge: diff --git a/acceptance/tests/fixtures/cases/static-client-inject-multiport/patch.yaml b/acceptance/tests/fixtures/cases/static-client-inject-multiport/patch.yaml new file mode 100644 index 0000000000..c38ce8e448 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-inject-multiport/patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-client +spec: + template: + metadata: + annotations: + "consul.hashicorp.com/connect-inject": "true" + "consul.hashicorp.com/connect-service-upstreams": "multiport:1234, multiport-admin:2234" \ No newline at end of file diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-multi-dc/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-inject/kustomization.yaml similarity index 86% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-multi-dc/kustomization.yaml rename to acceptance/tests/fixtures/cases/static-client-inject/kustomization.yaml index 974fbd4fe1..9834f91903 100644 --- a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-multi-dc/kustomization.yaml +++ b/acceptance/tests/fixtures/cases/static-client-inject/kustomization.yaml @@ -1,4 +1,4 @@ -bases: +resources: - ../../bases/static-client patchesStrategicMerge: diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-inject/patch.yaml b/acceptance/tests/fixtures/cases/static-client-inject/patch.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-inject/patch.yaml rename to acceptance/tests/fixtures/cases/static-client-inject/patch.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-tproxy/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-multi-dc/kustomization.yaml similarity index 86% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-tproxy/kustomization.yaml rename to acceptance/tests/fixtures/cases/static-client-multi-dc/kustomization.yaml index 974fbd4fe1..9834f91903 100644 --- a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-tproxy/kustomization.yaml +++ b/acceptance/tests/fixtures/cases/static-client-multi-dc/kustomization.yaml @@ -1,4 +1,4 @@ -bases: +resources: - ../../bases/static-client patchesStrategicMerge: diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-multi-dc/patch.yaml b/acceptance/tests/fixtures/cases/static-client-multi-dc/patch.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-multi-dc/patch.yaml rename to acceptance/tests/fixtures/cases/static-client-multi-dc/patch.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-namespaces/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-namespaces/kustomization.yaml similarity index 86% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-namespaces/kustomization.yaml rename to acceptance/tests/fixtures/cases/static-client-namespaces/kustomization.yaml index 4ddbaa9915..4c5d895a98 100644 --- a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-namespaces/kustomization.yaml +++ b/acceptance/tests/fixtures/cases/static-client-namespaces/kustomization.yaml @@ -1,4 +1,4 @@ -bases: +resources: - ../../bases/static-client patchesStrategicMerge: diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-namespaces/patch.yaml b/acceptance/tests/fixtures/cases/static-client-namespaces/patch.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-namespaces/patch.yaml rename to acceptance/tests/fixtures/cases/static-client-namespaces/patch.yaml diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-default-partition/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-default-partition/kustomization.yaml new file mode 100644 index 0000000000..7191edfb80 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-default-partition/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/static-client + +patchesStrategicMerge: + - patch.yaml \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-default-partition/patch.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-default-partition/patch.yaml new file mode 100644 index 0000000000..43507364f8 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-default-partition/patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-client +spec: + template: + metadata: + annotations: + "consul.hashicorp.com/connect-inject": "true" + "consul.hashicorp.com/connect-service-upstreams": "static-server.default.default:1234" \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-partition/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-partition/kustomization.yaml new file mode 100644 index 0000000000..7191edfb80 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-partition/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/static-client + +patchesStrategicMerge: + - patch.yaml \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-partition/patch.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-partition/patch.yaml new file mode 100644 index 0000000000..42857c3d2b --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/default-ns-partition/patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-client +spec: + template: + metadata: + annotations: + "consul.hashicorp.com/connect-inject": "true" + "consul.hashicorp.com/connect-service-upstreams": "static-server.default.secondary:1234" \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/ns-default-partition/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/ns-default-partition/kustomization.yaml new file mode 100644 index 0000000000..7191edfb80 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/ns-default-partition/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/static-client + +patchesStrategicMerge: + - patch.yaml \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/ns-default-partition/patch.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/ns-default-partition/patch.yaml new file mode 100644 index 0000000000..2fa8ec7e27 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/ns-default-partition/patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-client +spec: + template: + metadata: + annotations: + "consul.hashicorp.com/connect-inject": "true" + "consul.hashicorp.com/connect-service-upstreams": "static-server.ns1.default:1234" \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/ns-partition/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/ns-partition/kustomization.yaml new file mode 100644 index 0000000000..7191edfb80 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/ns-partition/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../../bases/static-client + +patchesStrategicMerge: + - patch.yaml \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-partitions/ns-partition/patch.yaml b/acceptance/tests/fixtures/cases/static-client-partitions/ns-partition/patch.yaml new file mode 100644 index 0000000000..f0ceb634bd --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-partitions/ns-partition/patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-client +spec: + template: + metadata: + annotations: + "consul.hashicorp.com/connect-inject": "true" + "consul.hashicorp.com/connect-service-upstreams": "static-server.ns1.secondary:1234" \ No newline at end of file diff --git a/acceptance/tests/fixtures/cases/static-client-tproxy/kustomization.yaml b/acceptance/tests/fixtures/cases/static-client-tproxy/kustomization.yaml new file mode 100644 index 0000000000..9834f91903 --- /dev/null +++ b/acceptance/tests/fixtures/cases/static-client-tproxy/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - ../../bases/static-client + +patchesStrategicMerge: + - patch.yaml \ No newline at end of file diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-client-tproxy/patch.yaml b/acceptance/tests/fixtures/cases/static-client-tproxy/patch.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-client-tproxy/patch.yaml rename to acceptance/tests/fixtures/cases/static-client-tproxy/patch.yaml diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-server-inject/kustomization.yaml b/acceptance/tests/fixtures/cases/static-server-inject/kustomization.yaml similarity index 86% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-server-inject/kustomization.yaml rename to acceptance/tests/fixtures/cases/static-server-inject/kustomization.yaml index a8978db882..bac892baa0 100644 --- a/charts/consul/test/acceptance/tests/fixtures/cases/static-server-inject/kustomization.yaml +++ b/acceptance/tests/fixtures/cases/static-server-inject/kustomization.yaml @@ -1,4 +1,4 @@ -bases: +resources: - ../../bases/static-server patchesStrategicMerge: diff --git a/charts/consul/test/acceptance/tests/fixtures/cases/static-server-inject/patch.yaml b/acceptance/tests/fixtures/cases/static-server-inject/patch.yaml similarity index 100% rename from charts/consul/test/acceptance/tests/fixtures/cases/static-server-inject/patch.yaml rename to acceptance/tests/fixtures/cases/static-server-inject/patch.yaml diff --git a/charts/consul/test/acceptance/tests/ingress-gateway/ingress_gateway_namespaces_test.go b/acceptance/tests/ingress-gateway/ingress_gateway_namespaces_test.go similarity index 85% rename from charts/consul/test/acceptance/tests/ingress-gateway/ingress_gateway_namespaces_test.go rename to acceptance/tests/ingress-gateway/ingress_gateway_namespaces_test.go index 8a3610a812..f47f98d70f 100644 --- a/charts/consul/test/acceptance/tests/ingress-gateway/ingress_gateway_namespaces_test.go +++ b/acceptance/tests/ingress-gateway/ingress_gateway_namespaces_test.go @@ -6,10 +6,10 @@ import ( "testing" terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" ) @@ -128,16 +128,21 @@ func TestIngressGatewaySingleNamespace(t *testing.T) { // via the bounce pod. It should fail to connect with the // static-server pod because of intentions. logger.Log(t, "testing intentions prevent ingress") - k8s.CheckStaticServerConnectionFailing(t, nsK8SOptions, "-H", "Host: static-server.ingress.consul", ingressGatewayService) + k8s.CheckStaticServerConnectionFailing(t, nsK8SOptions, staticClientName, "-H", "Host: static-server.ingress.consul", ingressGatewayService) // Now we create the allow intention. logger.Log(t, "creating ingress-gateway => static-server intention") - _, err = consulClient.Connect().IntentionUpsert(&api.Intention{ - SourceName: "ingress-gateway", - SourceNS: testNamespace, - DestinationName: "static-server", - DestinationNS: testNamespace, - Action: api.IntentionActionAllow, + _, _, err = consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: "static-server", + Namespace: testNamespace, + Sources: []*api.SourceIntention{ + { + Name: "ingress-gateway", + Namespace: testNamespace, + Action: api.IntentionActionAllow, + }, + }, }, nil) require.NoError(t, err) } @@ -145,7 +150,7 @@ func TestIngressGatewaySingleNamespace(t *testing.T) { // Test that we can make a call to the ingress gateway // via the static-client pod. It should route to the static-server pod. logger.Log(t, "trying calls to ingress gateway") - k8s.CheckStaticServerConnectionSuccessful(t, nsK8SOptions, "-H", "Host: static-server.ingress.consul", ingressGatewayService) + k8s.CheckStaticServerConnectionSuccessful(t, nsK8SOptions, staticClientName, "-H", "Host: static-server.ingress.consul", ingressGatewayService) }) } } @@ -248,16 +253,21 @@ func TestIngressGatewayNamespaceMirroring(t *testing.T) { // via the bounce pod. It should fail to connect with the // static-server pod because of intentions. logger.Log(t, "testing intentions prevent ingress") - k8s.CheckStaticServerConnectionFailing(t, nsK8SOptions, "-H", "Host: static-server.ingress.consul", ingressGatewayService) + k8s.CheckStaticServerConnectionFailing(t, nsK8SOptions, staticClientName, "-H", "Host: static-server.ingress.consul", ingressGatewayService) // Now we create the allow intention. logger.Log(t, "creating ingress-gateway => static-server intention") - _, err = consulClient.Connect().IntentionUpsert(&api.Intention{ - SourceName: "ingress-gateway", - SourceNS: "default", - DestinationName: "static-server", - DestinationNS: testNamespace, - Action: api.IntentionActionAllow, + _, _, err = consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: "static-server", + Namespace: testNamespace, + Sources: []*api.SourceIntention{ + { + Name: "ingress-gateway", + Namespace: "default", + Action: api.IntentionActionAllow, + }, + }, }, nil) require.NoError(t, err) } @@ -265,7 +275,7 @@ func TestIngressGatewayNamespaceMirroring(t *testing.T) { // Test that we can make a call to the ingress gateway // via the static-client pod. It should route to the static-server pod. logger.Log(t, "trying calls to ingress gateway") - k8s.CheckStaticServerConnectionSuccessful(t, nsK8SOptions, "-H", "Host: static-server.ingress.consul", ingressGatewayService) + k8s.CheckStaticServerConnectionSuccessful(t, nsK8SOptions, staticClientName, "-H", "Host: static-server.ingress.consul", ingressGatewayService) }) } } diff --git a/charts/consul/test/acceptance/tests/ingress-gateway/ingress_gateway_test.go b/acceptance/tests/ingress-gateway/ingress_gateway_test.go similarity index 76% rename from charts/consul/test/acceptance/tests/ingress-gateway/ingress_gateway_test.go rename to acceptance/tests/ingress-gateway/ingress_gateway_test.go index e360f0efd3..359b917a73 100644 --- a/charts/consul/test/acceptance/tests/ingress-gateway/ingress_gateway_test.go +++ b/acceptance/tests/ingress-gateway/ingress_gateway_test.go @@ -5,14 +5,16 @@ import ( "strconv" "testing" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" ) +const staticClientName = "static-client" + // Test that ingress gateways work in a default installation and a secure installation. func TestIngressGateway(t *testing.T) { cases := []struct { @@ -92,14 +94,19 @@ func TestIngressGateway(t *testing.T) { // via the bounce pod. It should fail to connect with the // static-server pod because of intentions. logger.Log(t, "testing intentions prevent ingress") - k8s.CheckStaticServerConnectionFailing(t, k8sOptions, "-H", "Host: static-server.ingress.consul", fmt.Sprintf("http://%s-consul-ingress-gateway:8080/", releaseName)) + k8s.CheckStaticServerConnectionFailing(t, k8sOptions, staticClientName, "-H", "Host: static-server.ingress.consul", fmt.Sprintf("http://%s-consul-ingress-gateway:8080/", releaseName)) // Now we create the allow intention. logger.Log(t, "creating ingress-gateway => static-server intention") - _, err = consulClient.Connect().IntentionUpsert(&api.Intention{ - SourceName: "ingress-gateway", - DestinationName: "static-server", - Action: api.IntentionActionAllow, + _, _, err = consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: "static-server", + Sources: []*api.SourceIntention{ + { + Name: "ingress-gateway", + Action: api.IntentionActionAllow, + }, + }, }, nil) require.NoError(t, err) } @@ -107,7 +114,7 @@ func TestIngressGateway(t *testing.T) { // Test that we can make a call to the ingress gateway // via the static-client pod. It should route to the static-server pod. logger.Log(t, "trying calls to ingress gateway") - k8s.CheckStaticServerConnectionSuccessful(t, k8sOptions, "-H", "Host: static-server.ingress.consul", fmt.Sprintf("http://%s-consul-ingress-gateway:8080/", releaseName)) + k8s.CheckStaticServerConnectionSuccessful(t, k8sOptions, staticClientName, "-H", "Host: static-server.ingress.consul", fmt.Sprintf("http://%s-consul-ingress-gateway:8080/", releaseName)) }) } } diff --git a/charts/consul/test/acceptance/tests/ingress-gateway/main_test.go b/acceptance/tests/ingress-gateway/main_test.go similarity index 64% rename from charts/consul/test/acceptance/tests/ingress-gateway/main_test.go rename to acceptance/tests/ingress-gateway/main_test.go index 2a208fbf0e..bd927f96cb 100644 --- a/charts/consul/test/acceptance/tests/ingress-gateway/main_test.go +++ b/acceptance/tests/ingress-gateway/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/mesh-gateway/main_test.go b/acceptance/tests/mesh-gateway/main_test.go similarity index 78% rename from charts/consul/test/acceptance/tests/mesh-gateway/main_test.go rename to acceptance/tests/mesh-gateway/main_test.go index d29d17a1d0..fb8935441e 100644 --- a/charts/consul/test/acceptance/tests/mesh-gateway/main_test.go +++ b/acceptance/tests/mesh-gateway/main_test.go @@ -5,7 +5,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/mesh-gateway/mesh_gateway_test.go b/acceptance/tests/mesh-gateway/mesh_gateway_test.go similarity index 76% rename from charts/consul/test/acceptance/tests/mesh-gateway/mesh_gateway_test.go rename to acceptance/tests/mesh-gateway/mesh_gateway_test.go index 521cc1573a..230f5b01f4 100644 --- a/charts/consul/test/acceptance/tests/mesh-gateway/mesh_gateway_test.go +++ b/acceptance/tests/mesh-gateway/mesh_gateway_test.go @@ -4,15 +4,13 @@ import ( "context" "fmt" "testing" - "time" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/environment" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,7 +18,7 @@ import ( const staticClientName = "static-client" // Test that Connect and wan federation over mesh gateways work in a default installation -// i.e. without ACLs because TLS is required for WAN federation over mesh gateways +// i.e. without ACLs because TLS is required for WAN federation over mesh gateways. func TestMeshGatewayDefault(t *testing.T) { env := suite.Environment() cfg := suite.Config() @@ -96,12 +94,19 @@ func TestMeshGatewayDefault(t *testing.T) { secondaryConsulCluster := consul.NewHelmCluster(t, secondaryHelmValues, secondaryContext, cfg, releaseName) secondaryConsulCluster.Create(t) + if cfg.UseKind { + // This is a temporary workaround that seems to fix mesh gateway tests on kind 1.22.x. + // TODO (ishustava): we need to investigate this further and remove once we've found the issue. + k8s.RunKubectl(t, primaryContext.KubectlOptions(t), "rollout", "restart", fmt.Sprintf("sts/%s-consul-server", releaseName)) + k8s.RunKubectl(t, primaryContext.KubectlOptions(t), "rollout", "status", fmt.Sprintf("sts/%s-consul-server", releaseName)) + } + primaryClient := primaryConsulCluster.SetupConsulClient(t, false) secondaryClient := secondaryConsulCluster.SetupConsulClient(t, false) // Verify federation between servers logger.Log(t, "verifying federation was successful") - verifyFederation(t, primaryClient, secondaryClient, releaseName, false) + helpers.VerifyFederation(t, primaryClient, secondaryClient, releaseName, false) // Create a ProxyDefaults resource to configure services to use the mesh // gateways. @@ -120,7 +125,7 @@ func TestMeshGatewayDefault(t *testing.T) { k8s.DeployKustomize(t, primaryContext.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-multi-dc") logger.Log(t, "checking that connection is successful") - k8s.CheckStaticServerConnectionSuccessful(t, primaryContext.KubectlOptions(t), "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, primaryContext.KubectlOptions(t), staticClientName, "http://localhost:1234") } // Test that Connect and wan federation over mesh gateways work in a secure installation, @@ -225,12 +230,19 @@ func TestMeshGatewaySecure(t *testing.T) { secondaryConsulCluster := consul.NewHelmCluster(t, secondaryHelmValues, secondaryContext, cfg, releaseName) secondaryConsulCluster.Create(t) + if cfg.UseKind { + // This is a temporary workaround that seems to fix mesh gateway tests on kind 1.22.x. + // TODO (ishustava): we need to investigate this further and remove once we've found the issue. + k8s.RunKubectl(t, primaryContext.KubectlOptions(t), "rollout", "restart", fmt.Sprintf("sts/%s-consul-server", releaseName)) + k8s.RunKubectl(t, primaryContext.KubectlOptions(t), "rollout", "status", fmt.Sprintf("sts/%s-consul-server", releaseName)) + } + primaryClient := primaryConsulCluster.SetupConsulClient(t, true) secondaryClient := secondaryConsulCluster.SetupConsulClient(t, true) // Verify federation between servers logger.Log(t, "verifying federation was successful") - verifyFederation(t, primaryClient, secondaryClient, releaseName, true) + helpers.VerifyFederation(t, primaryClient, secondaryClient, releaseName, true) // Create a ProxyDefaults resource to configure services to use the mesh // gateways. @@ -249,50 +261,20 @@ func TestMeshGatewaySecure(t *testing.T) { k8s.DeployKustomize(t, primaryContext.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-multi-dc") logger.Log(t, "creating intention") - _, err = primaryClient.Connect().IntentionUpsert(&api.Intention{ - SourceName: staticClientName, - DestinationName: "static-server", - Action: api.IntentionActionAllow, + _, _, err = primaryClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: "static-server", + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Action: api.IntentionActionAllow, + }, + }, }, nil) require.NoError(t, err) logger.Log(t, "checking that connection is successful") - k8s.CheckStaticServerConnectionSuccessful(t, primaryContext.KubectlOptions(t), "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, primaryContext.KubectlOptions(t), staticClientName, "http://localhost:1234") }) } } - -// verifyFederation checks that the WAN federation between servers is successful -// by first checking members are alive from the perspective of both servers. -// If secure is true, it will also check that the ACL replication is running on the secondary server. -func verifyFederation(t *testing.T, primaryClient, secondaryClient *api.Client, releaseName string, secure bool) { - retrier := &retry.Timer{Timeout: 5 * time.Minute, Wait: 1 * time.Second} - start := time.Now() - - // Check that server in dc1 is healthy from the perspective of the server in dc2, and vice versa. - // We're calling the Consul health API, as opposed to checking serf membership status, - // because we need to make sure that the federated servers can make API calls and forward requests - // from one server to another. From running tests in CI for a while and using serf membership status before, - // we've noticed that the status could be "alive" as soon as the server in the secondary cluster joins the primary - // and then switch to "failed". This would require us to check that the status is "alive" is showing consistently for - // some amount of time, which could be quite flakey. Calling the API in another datacenter allows us to check that - // each server can forward calls to another, which is what we need for connect. - retry.RunWith(retrier, t, func(r *retry.R) { - secondaryServerHealth, _, err := primaryClient.Health().Node(fmt.Sprintf("%s-consul-server-0", releaseName), &api.QueryOptions{Datacenter: "dc2"}) - require.NoError(r, err) - require.Equal(r, secondaryServerHealth.AggregatedStatus(), api.HealthPassing) - - primaryServerHealth, _, err := secondaryClient.Health().Node(fmt.Sprintf("%s-consul-server-0", releaseName), &api.QueryOptions{Datacenter: "dc1"}) - require.NoError(r, err) - require.Equal(r, primaryServerHealth.AggregatedStatus(), api.HealthPassing) - - if secure { - replicationStatus, _, err := secondaryClient.ACL().Replication(nil) - require.NoError(r, err) - require.True(r, replicationStatus.Enabled) - require.True(r, replicationStatus.Running) - } - }) - - logger.Logf(t, "Took %s to verify federation", time.Since(start)) -} diff --git a/charts/consul/test/acceptance/tests/metrics/main_test.go b/acceptance/tests/metrics/main_test.go similarity index 63% rename from charts/consul/test/acceptance/tests/metrics/main_test.go rename to acceptance/tests/metrics/main_test.go index b4e970d990..8717c7c4b5 100644 --- a/charts/consul/test/acceptance/tests/metrics/main_test.go +++ b/acceptance/tests/metrics/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/metrics/metrics_test.go b/acceptance/tests/metrics/metrics_test.go similarity index 79% rename from charts/consul/test/acceptance/tests/metrics/metrics_test.go rename to acceptance/tests/metrics/metrics_test.go index d4b3469d1d..0ad3ce22c4 100644 --- a/charts/consul/test/acceptance/tests/metrics/metrics_test.go +++ b/acceptance/tests/metrics/metrics_test.go @@ -3,13 +3,15 @@ package metrics import ( "context" "fmt" + "github.com/hashicorp/consul/sdk/testutil/retry" "testing" + "time" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/environment" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -75,13 +77,13 @@ func TestComponentMetrics(t *testing.T) { require.Contains(t, metricsOutput, `consul_acl_ResolveToken{quantile="0.5"}`) // Ingress Gateway Metrics - assertGatewayMetricsEnabled(t, ctx, ns, "ingress-gateway", `envoy_cluster_assignment_stale{local_cluster="ingress-gateway",consul_source_service="ingress-gateway",consul_source_namespace="default",consul_source_datacenter="dc1",envoy_cluster_name="local_agent"} 0`) + assertGatewayMetricsEnabled(t, ctx, ns, "ingress-gateway", `envoy_cluster_assignment_stale{local_cluster="ingress-gateway",consul_source_service="ingress-gateway"`) // Terminating Gateway Metrics - assertGatewayMetricsEnabled(t, ctx, ns, "terminating-gateway", `envoy_cluster_assignment_stale{local_cluster="terminating-gateway",consul_source_service="terminating-gateway",consul_source_namespace="default",consul_source_datacenter="dc1",envoy_cluster_name="local_agent"} 0`) + assertGatewayMetricsEnabled(t, ctx, ns, "terminating-gateway", `envoy_cluster_assignment_stale{local_cluster="terminating-gateway",consul_source_service="terminating-gateway"`) // Mesh Gateway Metrics - assertGatewayMetricsEnabled(t, ctx, ns, "mesh-gateway", `envoy_cluster_assignment_stale{local_cluster="mesh-gateway",consul_source_service="mesh-gateway",consul_source_namespace="default",consul_source_datacenter="dc1",envoy_cluster_name="local_agent"} 0`) + assertGatewayMetricsEnabled(t, ctx, ns, "mesh-gateway", `envoy_cluster_assignment_stale{local_cluster="mesh-gateway",consul_source_service="mesh-gateway"`) } // Test that merged service and envoy metrics are accessible from the @@ -121,12 +123,17 @@ func TestAppMetrics(t *testing.T) { require.NoError(t, err) require.Len(t, podList.Items, 1) podIP := podList.Items[0].Status.PodIP - metricsOutput, err := k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "exec", "deploy/"+staticClientName, "--", "curl", "--silent", "--show-error", fmt.Sprintf("http://%s:20200/metrics", podIP)) - require.NoError(t, err) - // This assertion represents the metrics from the envoy sidecar. - require.Contains(t, metricsOutput, `envoy_cluster_assignment_stale{local_cluster="server",consul_source_service="server",consul_source_namespace="default",consul_source_datacenter="dc1",envoy_cluster_name="local_agent"} 0`) - // This assertion represents the metrics from the application. - require.Contains(t, metricsOutput, `service_started_total 1`) + + // Retry because sometimes the merged metrics server takes a couple hundred milliseconds + // to start. + retry.RunWith(&retry.Counter{Count: 3, Wait: 1 * time.Second}, t, func(r *retry.R) { + metricsOutput, err := k8s.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(t), "exec", "deploy/"+staticClientName, "--", "curl", "--silent", "--show-error", fmt.Sprintf("http://%s:20200/metrics", podIP)) + require.NoError(r, err) + // This assertion represents the metrics from the envoy sidecar. + require.Contains(r, metricsOutput, `envoy_cluster_assignment_stale{local_cluster="server",consul_source_service="server"`) + // This assertion represents the metrics from the application. + require.Contains(r, metricsOutput, `service_started_total 1`) + }) } func assertGatewayMetricsEnabled(t *testing.T, ctx environment.TestContext, ns, label, metricsAssertion string) { diff --git a/acceptance/tests/partitions/main_test.go b/acceptance/tests/partitions/main_test.go new file mode 100644 index 0000000000..b2758a572c --- /dev/null +++ b/acceptance/tests/partitions/main_test.go @@ -0,0 +1,22 @@ +package partitions + +import ( + "fmt" + "os" + "testing" + + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" +) + +var suite testsuite.Suite + +func TestMain(m *testing.M) { + suite = testsuite.NewSuite(m) + + if suite.Config().EnableMultiCluster { + os.Exit(suite.Run()) + } else { + fmt.Println("Skipping partitions tests because -enable-multi-cluster is not set") + os.Exit(0) + } +} diff --git a/acceptance/tests/partitions/partitions_test.go b/acceptance/tests/partitions/partitions_test.go new file mode 100644 index 0000000000..d3c732f071 --- /dev/null +++ b/acceptance/tests/partitions/partitions_test.go @@ -0,0 +1,641 @@ +package partitions + +import ( + "context" + "fmt" + "strconv" + "testing" + + terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const staticClientName = "static-client" +const staticServerName = "static-server" +const staticServerNamespace = "ns1" +const staticClientNamespace = "ns2" + +// Test that Connect works in a default and ACLsAndAutoEncryptEnabled installations for X-Partition and in-partition networking. +func TestPartitions(t *testing.T) { + env := suite.Environment() + cfg := suite.Config() + + if !cfg.EnableEnterprise { + t.Skipf("skipping this test because -enable-enterprise is not set") + } + + const defaultPartition = "default" + const secondaryPartition = "secondary" + const defaultNamespace = "default" + cases := []struct { + name string + destinationNamespace string + mirrorK8S bool + ACLsAndAutoEncryptEnabled bool + }{ + { + "default destination namespace", + defaultNamespace, + false, + false, + }, + { + "default destination namespace; ACLs and auto-encrypt enabled", + defaultNamespace, + false, + true, + }, + { + "single destination namespace", + staticServerNamespace, + false, + false, + }, + { + "single destination namespace; ACLs and auto-encrypt enabled", + staticServerNamespace, + false, + true, + }, + { + "mirror k8s namespaces", + staticServerNamespace, + true, + false, + }, + { + "mirror k8s namespaces; ACLs and auto-encrypt enabled", + staticServerNamespace, + true, + true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + serverClusterContext := env.DefaultContext(t) + clientClusterContext := env.Context(t, environment.SecondaryContextName) + + ctx := context.Background() + + commonHelmValues := map[string]string{ + "global.adminPartitions.enabled": "true", + + "global.enableConsulNamespaces": "true", + + "global.tls.enabled": "true", + "global.tls.httpsOnly": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled), + "global.tls.enableAutoEncrypt": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled), + + "global.acls.manageSystemACLs": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled), + + "connectInject.enabled": "true", + // When mirroringK8S is set, this setting is ignored. + "connectInject.consulNamespaces.consulDestinationNamespace": c.destinationNamespace, + "connectInject.consulNamespaces.mirroringK8S": strconv.FormatBool(c.mirrorK8S), + + "meshGateway.enabled": "true", + "meshGateway.replicas": "1", + + "controller.enabled": "true", + + "dns.enabled": "true", + "dns.enableRedirection": strconv.FormatBool(cfg.EnableTransparentProxy), + } + + serverHelmValues := map[string]string{ + "server.exposeGossipAndRPCPorts": "true", + } + + // On Kind, there are no load balancers but since all clusters + // share the same node network (docker bridge), we can use + // a NodePort service so that we can access node(s) in a different Kind cluster. + if cfg.UseKind { + serverHelmValues["global.adminPartitions.service.type"] = "NodePort" + serverHelmValues["global.adminPartitions.service.nodePort.https"] = "30000" + serverHelmValues["meshGateway.service.type"] = "NodePort" + serverHelmValues["meshGateway.service.nodePort"] = "30100" + } + + releaseName := helpers.RandomName() + + helpers.MergeMaps(serverHelmValues, commonHelmValues) + + // Install the consul cluster with servers in the default kubernetes context. + serverConsulCluster := consul.NewHelmCluster(t, serverHelmValues, serverClusterContext, cfg, releaseName) + serverConsulCluster.Create(t) + + // Get the TLS CA certificate and key secret from the server cluster and apply it to the client cluster. + caCertSecretName := fmt.Sprintf("%s-consul-ca-cert", releaseName) + caKeySecretName := fmt.Sprintf("%s-consul-ca-key", releaseName) + + logger.Logf(t, "retrieving ca cert secret %s from the server cluster and applying to the client cluster", caCertSecretName) + moveSecret(t, serverClusterContext, clientClusterContext, caCertSecretName) + + if !c.ACLsAndAutoEncryptEnabled { + // When auto-encrypt is disabled, we need both + // the CA cert and CA key to be available in the clients cluster to generate client certificates and keys. + logger.Logf(t, "retrieving ca key secret %s from the server cluster and applying to the client cluster", caKeySecretName) + moveSecret(t, serverClusterContext, clientClusterContext, caKeySecretName) + } + + partitionToken := fmt.Sprintf("%s-consul-partitions-acl-token", releaseName) + if c.ACLsAndAutoEncryptEnabled { + logger.Logf(t, "retrieving partition token secret %s from the server cluster and applying to the client cluster", partitionToken) + moveSecret(t, serverClusterContext, clientClusterContext, partitionToken) + } + + partitionServiceName := fmt.Sprintf("%s-consul-partition", releaseName) + partitionSvcAddress := k8s.ServiceHost(t, cfg, serverClusterContext, partitionServiceName) + + k8sAuthMethodHost := k8s.KubernetesAPIServerHost(t, cfg, clientClusterContext) + + // Create client cluster. + clientHelmValues := map[string]string{ + "global.enabled": "false", + + "global.adminPartitions.name": secondaryPartition, + + "global.tls.caCert.secretName": caCertSecretName, + "global.tls.caCert.secretKey": "tls.crt", + + "externalServers.enabled": "true", + "externalServers.hosts[0]": partitionSvcAddress, + "externalServers.tlsServerName": "server.dc1.consul", + + "client.enabled": "true", + "client.exposeGossipPorts": "true", + "client.join[0]": partitionSvcAddress, + } + + if c.ACLsAndAutoEncryptEnabled { + // Setup partition token and auth method host if ACLs enabled. + clientHelmValues["global.acls.bootstrapToken.secretName"] = partitionToken + clientHelmValues["global.acls.bootstrapToken.secretKey"] = "token" + clientHelmValues["externalServers.k8sAuthMethodHost"] = k8sAuthMethodHost + } else { + // Provide CA key when auto-encrypt is disabled. + clientHelmValues["global.tls.caKey.secretName"] = caKeySecretName + clientHelmValues["global.tls.caKey.secretKey"] = "tls.key" + } + + if cfg.UseKind { + clientHelmValues["externalServers.httpsPort"] = "30000" + clientHelmValues["meshGateway.service.type"] = "NodePort" + clientHelmValues["meshGateway.service.nodePort"] = "30100" + } + + helpers.MergeMaps(clientHelmValues, commonHelmValues) + + // Install the consul cluster without servers in the client cluster kubernetes context. + clientConsulCluster := consul.NewHelmCluster(t, clientHelmValues, clientClusterContext, cfg, releaseName) + clientConsulCluster.Create(t) + + // Ensure consul clients are created. + agentPodList, err := clientClusterContext.KubernetesClient(t).CoreV1().Pods(clientClusterContext.KubectlOptions(t).Namespace).List(ctx, metav1.ListOptions{LabelSelector: "app=consul,component=client"}) + require.NoError(t, err) + require.NotEmpty(t, agentPodList.Items) + + output, err := k8s.RunKubectlAndGetOutputE(t, clientClusterContext.KubectlOptions(t), "logs", agentPodList.Items[0].Name, "-n", clientClusterContext.KubectlOptions(t).Namespace) + require.NoError(t, err) + require.Contains(t, output, "Partition: 'secondary'") + + serverClusterStaticServerOpts := &terratestk8s.KubectlOptions{ + ContextName: serverClusterContext.KubectlOptions(t).ContextName, + ConfigPath: serverClusterContext.KubectlOptions(t).ConfigPath, + Namespace: staticServerNamespace, + } + serverClusterStaticClientOpts := &terratestk8s.KubectlOptions{ + ContextName: serverClusterContext.KubectlOptions(t).ContextName, + ConfigPath: serverClusterContext.KubectlOptions(t).ConfigPath, + Namespace: staticClientNamespace, + } + clientClusterStaticServerOpts := &terratestk8s.KubectlOptions{ + ContextName: clientClusterContext.KubectlOptions(t).ContextName, + ConfigPath: clientClusterContext.KubectlOptions(t).ConfigPath, + Namespace: staticServerNamespace, + } + clientClusterStaticClientOpts := &terratestk8s.KubectlOptions{ + ContextName: clientClusterContext.KubectlOptions(t).ContextName, + ConfigPath: clientClusterContext.KubectlOptions(t).ConfigPath, + Namespace: staticClientNamespace, + } + + logger.Logf(t, "creating namespaces %s and %s in servers cluster", staticServerNamespace, staticClientNamespace) + k8s.RunKubectl(t, serverClusterContext.KubectlOptions(t), "create", "ns", staticServerNamespace) + k8s.RunKubectl(t, serverClusterContext.KubectlOptions(t), "create", "ns", staticClientNamespace) + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.RunKubectl(t, serverClusterContext.KubectlOptions(t), "delete", "ns", staticServerNamespace, staticClientNamespace) + }) + + logger.Logf(t, "creating namespaces %s and %s in clients cluster", staticServerNamespace, staticClientNamespace) + k8s.RunKubectl(t, clientClusterContext.KubectlOptions(t), "create", "ns", staticServerNamespace) + k8s.RunKubectl(t, clientClusterContext.KubectlOptions(t), "create", "ns", staticClientNamespace) + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.RunKubectl(t, clientClusterContext.KubectlOptions(t), "delete", "ns", staticServerNamespace, staticClientNamespace) + }) + + consulClient := serverConsulCluster.SetupConsulClient(t, c.ACLsAndAutoEncryptEnabled) + + serverQueryServerOpts := &api.QueryOptions{Namespace: staticServerNamespace, Partition: defaultPartition} + clientQueryServerOpts := &api.QueryOptions{Namespace: staticClientNamespace, Partition: defaultPartition} + + serverQueryClientOpts := &api.QueryOptions{Namespace: staticServerNamespace, Partition: secondaryPartition} + clientQueryClientOpts := &api.QueryOptions{Namespace: staticClientNamespace, Partition: secondaryPartition} + + if !c.mirrorK8S { + serverQueryServerOpts = &api.QueryOptions{Namespace: c.destinationNamespace, Partition: defaultPartition} + clientQueryServerOpts = &api.QueryOptions{Namespace: c.destinationNamespace, Partition: defaultPartition} + serverQueryClientOpts = &api.QueryOptions{Namespace: c.destinationNamespace, Partition: secondaryPartition} + clientQueryClientOpts = &api.QueryOptions{Namespace: c.destinationNamespace, Partition: secondaryPartition} + } + + // Check that the ACL token is deleted. + if c.ACLsAndAutoEncryptEnabled { + // We need to register the cleanup function before we create the deployments + // because golang will execute them in reverse order i.e. the last registered + // cleanup function will be executed first. + t.Cleanup(func() { + if c.ACLsAndAutoEncryptEnabled { + retry.Run(t, func(r *retry.R) { + tokens, _, err := consulClient.ACL().TokenList(serverQueryServerOpts) + require.NoError(r, err) + for _, token := range tokens { + require.NotContains(r, token.Description, staticServerName) + } + + tokens, _, err = consulClient.ACL().TokenList(clientQueryServerOpts) + require.NoError(r, err) + for _, token := range tokens { + require.NotContains(r, token.Description, staticClientName) + } + tokens, _, err = consulClient.ACL().TokenList(serverQueryClientOpts) + require.NoError(r, err) + for _, token := range tokens { + require.NotContains(r, token.Description, staticServerName) + } + + tokens, _, err = consulClient.ACL().TokenList(clientQueryClientOpts) + require.NoError(r, err) + for _, token := range tokens { + require.NotContains(r, token.Description, staticClientName) + } + }) + } + }) + } + + // Create a ProxyDefaults resource to configure services to use the mesh + // gateways. + logger.Log(t, "creating proxy-defaults config") + kustomizeDir := "../fixtures/bases/mesh-gateway" + + k8s.KubectlApplyK(t, serverClusterContext.KubectlOptions(t), kustomizeDir) + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.KubectlDeleteK(t, serverClusterContext.KubectlOptions(t), kustomizeDir) + }) + + k8s.KubectlApplyK(t, clientClusterContext.KubectlOptions(t), kustomizeDir) + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.KubectlDeleteK(t, clientClusterContext.KubectlOptions(t), kustomizeDir) + }) + // This section of the tests runs the in-partition networking tests. + t.Run("in-partition", func(t *testing.T) { + logger.Log(t, "test in-partition networking") + logger.Log(t, "creating static-server and static-client deployments in server cluster") + k8s.DeployKustomize(t, serverClusterStaticServerOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + if cfg.EnableTransparentProxy { + k8s.DeployKustomize(t, serverClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") + } else { + if c.destinationNamespace == defaultNamespace { + k8s.DeployKustomize(t, serverClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") + } else { + k8s.DeployKustomize(t, serverClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-namespaces") + } + } + logger.Log(t, "creating static-server and static-client deployments in client cluster") + k8s.DeployKustomize(t, clientClusterStaticServerOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + if cfg.EnableTransparentProxy { + k8s.DeployKustomize(t, clientClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") + } else { + if c.destinationNamespace == defaultNamespace { + k8s.DeployKustomize(t, clientClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") + } else { + k8s.DeployKustomize(t, clientClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-namespaces") + } + } + // Check that both static-server and static-client have been injected and now have 2 containers in server cluster. + for _, labelSelector := range []string{"app=static-server", "app=static-client"} { + podList, err := serverClusterContext.KubernetesClient(t).CoreV1().Pods(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{ + LabelSelector: labelSelector, + }) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Len(t, podList.Items[0].Spec.Containers, 2) + } + + // Check that both static-server and static-client have been injected and now have 2 containers in client cluster. + for _, labelSelector := range []string{"app=static-server", "app=static-client"} { + podList, err := clientClusterContext.KubernetesClient(t).CoreV1().Pods(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{ + LabelSelector: labelSelector, + }) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Len(t, podList.Items[0].Spec.Containers, 2) + } + + // Make sure that services are registered in the correct namespace. + // If mirroring is enabled, we expect services to be registered in the + // Consul namespace with the same name as their source + // Kubernetes namespace. + // If a single destination namespace is set, we expect all services + // to be registered in that destination Consul namespace. + // Server cluster. + services, _, err := consulClient.Catalog().Service(staticServerName, "", serverQueryServerOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + services, _, err = consulClient.Catalog().Service(staticClientName, "", clientQueryServerOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + // Client cluster. + services, _, err = consulClient.Catalog().Service(staticServerName, "", serverQueryClientOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + services, _, err = consulClient.Catalog().Service(staticClientName, "", clientQueryClientOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + if c.ACLsAndAutoEncryptEnabled { + logger.Log(t, "checking that the connection is not successful because there's no intention") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionFailing(t, serverClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + k8s.CheckStaticServerConnectionFailing(t, clientClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + } else { + k8s.CheckStaticServerConnectionFailing(t, serverClusterStaticClientOpts, staticClientName, "http://localhost:1234") + k8s.CheckStaticServerConnectionFailing(t, clientClusterStaticClientOpts, staticClientName, "http://localhost:1234") + } + + intention := &api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: staticServerName, + Namespace: staticServerNamespace, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Namespace: staticClientNamespace, + Action: api.IntentionActionAllow, + }, + }, + } + + // Set the destination namespace to be the same + // unless mirrorK8S is true. + if !c.mirrorK8S { + intention.Namespace = c.destinationNamespace + intention.Sources[0].Namespace = c.destinationNamespace + } + + logger.Log(t, "creating intention") + _, _, err := consulClient.ConfigEntries().Set(intention, &api.WriteOptions{Partition: defaultPartition}) + require.NoError(t, err) + _, _, err = consulClient.ConfigEntries().Set(intention, &api.WriteOptions{Partition: secondaryPartition}) + require.NoError(t, err) + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + _, err := consulClient.ConfigEntries().Delete(api.ServiceIntentions, staticServerName, &api.WriteOptions{Partition: defaultPartition}) + require.NoError(t, err) + _, err = consulClient.ConfigEntries().Delete(api.ServiceIntentions, staticServerName, &api.WriteOptions{Partition: secondaryPartition}) + require.NoError(t, err) + }) + } + + logger.Log(t, "checking that connection is successful") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionSuccessful(t, serverClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + k8s.CheckStaticServerConnectionSuccessful(t, clientClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + } else { + k8s.CheckStaticServerConnectionSuccessful(t, serverClusterStaticClientOpts, staticClientName, "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, clientClusterStaticClientOpts, staticClientName, "http://localhost:1234") + } + + // Test that kubernetes readiness status is synced to Consul. + // Create the file so that the readiness probe of the static-server pod fails. + logger.Log(t, "testing k8s -> consul health checks sync by making the static-server unhealthy") + k8s.RunKubectl(t, serverClusterStaticServerOpts, "exec", "deploy/"+staticServerName, "--", "touch", "/tmp/unhealthy") + k8s.RunKubectl(t, clientClusterStaticServerOpts, "exec", "deploy/"+staticServerName, "--", "touch", "/tmp/unhealthy") + + // The readiness probe should take a moment to be reflected in Consul, CheckStaticServerConnection will retry + // until Consul marks the service instance unavailable for mesh traffic, causing the connection to fail. + // We are expecting a "connection reset by peer" error because in a case of health checks, + // there will be no healthy proxy host to connect to. That's why we can't assert that we receive an empty reply + // from server, which is the case when a connection is unsuccessful due to intentions in other tests. + logger.Log(t, "checking that connection is unsuccessful") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, serverClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, clientClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.%s", staticServerNamespace)) + } else { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, serverClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "", "http://localhost:1234") + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, clientClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "", "http://localhost:1234") + } + }) + // This section of the tests runs the cross-partition networking tests. + t.Run("cross-partition", func(t *testing.T) { + logger.Log(t, "test cross-partition networking") + logger.Log(t, "creating static-server and static-client deployments in server cluster") + k8s.DeployKustomize(t, serverClusterStaticServerOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + if cfg.EnableTransparentProxy { + k8s.DeployKustomize(t, serverClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") + } else { + if c.destinationNamespace == defaultNamespace { + k8s.DeployKustomize(t, serverClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-partitions/default-ns-partition") + } else { + k8s.DeployKustomize(t, serverClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-partitions/ns-partition") + } + } + logger.Log(t, "creating static-server and static-client deployments in client cluster") + k8s.DeployKustomize(t, clientClusterStaticServerOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + if cfg.EnableTransparentProxy { + k8s.DeployKustomize(t, clientClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") + } else { + if c.destinationNamespace == defaultNamespace { + k8s.DeployKustomize(t, clientClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-partitions/default-ns-default-partition") + } else { + k8s.DeployKustomize(t, clientClusterStaticClientOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-partitions/ns-default-partition") + } + } + // Check that both static-server and static-client have been injected and now have 2 containers in server cluster. + for _, labelSelector := range []string{"app=static-server", "app=static-client"} { + podList, err := serverClusterContext.KubernetesClient(t).CoreV1().Pods(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{ + LabelSelector: labelSelector, + }) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Len(t, podList.Items[0].Spec.Containers, 2) + } + + // Check that both static-server and static-client have been injected and now have 2 containers in client cluster. + for _, labelSelector := range []string{"app=static-server", "app=static-client"} { + podList, err := clientClusterContext.KubernetesClient(t).CoreV1().Pods(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{ + LabelSelector: labelSelector, + }) + require.NoError(t, err) + require.Len(t, podList.Items, 1) + require.Len(t, podList.Items[0].Spec.Containers, 2) + } + + // Make sure that services are registered in the correct namespace. + // If mirroring is enabled, we expect services to be registered in the + // Consul namespace with the same name as their source + // Kubernetes namespace. + // If a single destination namespace is set, we expect all services + // to be registered in that destination Consul namespace. + // Server cluster. + // We are going to test that static-clients deployed in each partition can + // access the static-servers running in another partition. + // ie default -> secondary and secondary -> default. + services, _, err := consulClient.Catalog().Service(staticServerName, "", serverQueryServerOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + services, _, err = consulClient.Catalog().Service(staticClientName, "", clientQueryServerOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + // Client cluster. + services, _, err = consulClient.Catalog().Service(staticServerName, "", serverQueryClientOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + services, _, err = consulClient.Catalog().Service(staticClientName, "", clientQueryClientOpts) + require.NoError(t, err) + require.Len(t, services, 1) + + logger.Log(t, "creating exported services") + if c.destinationNamespace == defaultNamespace { + k8s.KubectlApplyK(t, serverClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/default-partition-default") + k8s.KubectlApplyK(t, clientClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/secondary-partition-default") + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.KubectlDeleteK(t, serverClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/default-partition-default") + k8s.KubectlDeleteK(t, clientClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/secondary-partition-default") + }) + } else { + k8s.KubectlApplyK(t, serverClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/default-partition-ns1") + k8s.KubectlApplyK(t, clientClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/secondary-partition-ns1") + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.KubectlDeleteK(t, serverClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/default-partition-ns1") + k8s.KubectlDeleteK(t, clientClusterContext.KubectlOptions(t), "../fixtures/cases/crd-partitions/secondary-partition-ns1") + }) + } + + if c.ACLsAndAutoEncryptEnabled { + logger.Log(t, "checking that the connection is not successful because there's no intention") + if cfg.EnableTransparentProxy { + if !c.mirrorK8S { + k8s.CheckStaticServerConnectionFailing(t, serverClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", c.destinationNamespace, secondaryPartition)) + k8s.CheckStaticServerConnectionFailing(t, clientClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", c.destinationNamespace, defaultPartition)) + } else { + k8s.CheckStaticServerConnectionFailing(t, serverClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", staticServerNamespace, secondaryPartition)) + k8s.CheckStaticServerConnectionFailing(t, clientClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", staticServerNamespace, defaultPartition)) + } + } else { + k8s.CheckStaticServerConnectionFailing(t, serverClusterStaticClientOpts, staticClientName, "http://localhost:1234") + k8s.CheckStaticServerConnectionFailing(t, clientClusterStaticClientOpts, staticClientName, "http://localhost:1234") + } + + intention := &api.ServiceIntentionsConfigEntry{ + Name: staticServerName, + Kind: api.ServiceIntentions, + Namespace: staticServerNamespace, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Namespace: staticClientNamespace, + Action: api.IntentionActionAllow, + }, + }, + } + + // Set the destination namespace to be the same + // unless mirrorK8S is true. + if !c.mirrorK8S { + intention.Namespace = c.destinationNamespace + intention.Sources[0].Namespace = c.destinationNamespace + } + + logger.Log(t, "creating intentions in each partition") + intention.Sources[0].Partition = secondaryPartition + _, _, err := consulClient.ConfigEntries().Set(intention, &api.WriteOptions{Partition: defaultPartition}) + require.NoError(t, err) + intention.Sources[0].Partition = defaultPartition + _, _, err = consulClient.ConfigEntries().Set(intention, &api.WriteOptions{Partition: secondaryPartition}) + require.NoError(t, err) + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + _, err := consulClient.ConfigEntries().Delete(api.ServiceIntentions, staticServerName, &api.WriteOptions{Partition: defaultPartition}) + require.NoError(t, err) + _, err = consulClient.ConfigEntries().Delete(api.ServiceIntentions, staticServerName, &api.WriteOptions{Partition: secondaryPartition}) + require.NoError(t, err) + }) + } + + logger.Log(t, "checking that connection is successful") + if cfg.EnableTransparentProxy { + if !c.mirrorK8S { + k8s.CheckStaticServerConnectionSuccessful(t, serverClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", c.destinationNamespace, secondaryPartition)) + k8s.CheckStaticServerConnectionSuccessful(t, clientClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", c.destinationNamespace, defaultPartition)) + } else { + k8s.CheckStaticServerConnectionSuccessful(t, serverClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", staticServerNamespace, secondaryPartition)) + k8s.CheckStaticServerConnectionSuccessful(t, clientClusterStaticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", staticServerNamespace, defaultPartition)) + } + } else { + k8s.CheckStaticServerConnectionSuccessful(t, serverClusterStaticClientOpts, staticClientName, "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, clientClusterStaticClientOpts, staticClientName, "http://localhost:1234") + } + + // Test that kubernetes readiness status is synced to Consul. + // Create the file so that the readiness probe of the static-server pod fails. + logger.Log(t, "testing k8s -> consul health checks sync by making the static-server unhealthy") + k8s.RunKubectl(t, serverClusterStaticServerOpts, "exec", "deploy/"+staticServerName, "--", "touch", "/tmp/unhealthy") + k8s.RunKubectl(t, clientClusterStaticServerOpts, "exec", "deploy/"+staticServerName, "--", "touch", "/tmp/unhealthy") + + // The readiness probe should take a moment to be reflected in Consul, CheckStaticServerConnection will retry + // until Consul marks the service instance unavailable for mesh traffic, causing the connection to fail. + // We are expecting a "connection reset by peer" error because in a case of health checks, + // there will be no healthy proxy host to connect to. That's why we can't assert that we receive an empty reply + // from server, which is the case when a connection is unsuccessful due to intentions in other tests. + logger.Log(t, "checking that connection is unsuccessful") + if cfg.EnableTransparentProxy { + if !c.mirrorK8S { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, serverClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", c.destinationNamespace, secondaryPartition)) + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, clientClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", c.destinationNamespace, defaultPartition)) + } else { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, serverClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", staticServerNamespace, secondaryPartition)) + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, clientClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.virtual.%s.ns.%s.ap.dc1.dc.consul", staticServerNamespace, defaultPartition)) + } + } else { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, serverClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "", "http://localhost:1234") + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, clientClusterStaticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "", "http://localhost:1234") + } + }) + }) + } +} + +func moveSecret(t *testing.T, sourceContext, destContext environment.TestContext, secretName string) { + t.Helper() + + secret, err := sourceContext.KubernetesClient(t).CoreV1().Secrets(sourceContext.KubectlOptions(t).Namespace).Get(context.Background(), secretName, metav1.GetOptions{}) + secret.ResourceVersion = "" + require.NoError(t, err) + _, err = destContext.KubernetesClient(t).CoreV1().Secrets(destContext.KubectlOptions(t).Namespace).Create(context.Background(), secret, metav1.CreateOptions{}) + require.NoError(t, err) +} diff --git a/charts/consul/test/acceptance/tests/sync/main_test.go b/acceptance/tests/sync/main_test.go similarity index 63% rename from charts/consul/test/acceptance/tests/sync/main_test.go rename to acceptance/tests/sync/main_test.go index 75326d15f7..80c394e561 100644 --- a/charts/consul/test/acceptance/tests/sync/main_test.go +++ b/acceptance/tests/sync/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/sync/sync_catalog_namespaces_test.go b/acceptance/tests/sync/sync_catalog_namespaces_test.go similarity index 92% rename from charts/consul/test/acceptance/tests/sync/sync_catalog_namespaces_test.go rename to acceptance/tests/sync/sync_catalog_namespaces_test.go index 40e316412b..2352fedc6a 100644 --- a/charts/consul/test/acceptance/tests/sync/sync_catalog_namespaces_test.go +++ b/acceptance/tests/sync/sync_catalog_namespaces_test.go @@ -6,10 +6,10 @@ import ( "time" terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" diff --git a/charts/consul/test/acceptance/tests/sync/sync_catalog_test.go b/acceptance/tests/sync/sync_catalog_test.go similarity index 88% rename from charts/consul/test/acceptance/tests/sync/sync_catalog_test.go rename to acceptance/tests/sync/sync_catalog_test.go index a8d45596df..dcfa361487 100644 --- a/charts/consul/test/acceptance/tests/sync/sync_catalog_test.go +++ b/acceptance/tests/sync/sync_catalog_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/stretchr/testify/require" ) diff --git a/charts/consul/test/acceptance/tests/terminating-gateway/main_test.go b/acceptance/tests/terminating-gateway/main_test.go similarity index 65% rename from charts/consul/test/acceptance/tests/terminating-gateway/main_test.go rename to acceptance/tests/terminating-gateway/main_test.go index 341d70dc7e..477e125f94 100644 --- a/charts/consul/test/acceptance/tests/terminating-gateway/main_test.go +++ b/acceptance/tests/terminating-gateway/main_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - testsuite "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/suite" + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" ) var suite testsuite.Suite diff --git a/charts/consul/test/acceptance/tests/terminating-gateway/terminating_gateway_namespaces_test.go b/acceptance/tests/terminating-gateway/terminating_gateway_namespaces_test.go similarity index 94% rename from charts/consul/test/acceptance/tests/terminating-gateway/terminating_gateway_namespaces_test.go rename to acceptance/tests/terminating-gateway/terminating_gateway_namespaces_test.go index b6dfff9ce7..76510b9a76 100644 --- a/charts/consul/test/acceptance/tests/terminating-gateway/terminating_gateway_namespaces_test.go +++ b/acceptance/tests/terminating-gateway/terminating_gateway_namespaces_test.go @@ -6,10 +6,10 @@ import ( "testing" terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" ) @@ -121,7 +121,7 @@ func TestTerminatingGatewaySingleNamespace(t *testing.T) { // Test that we can make a call to the terminating gateway. logger.Log(t, "trying calls to terminating gateway") - k8s.CheckStaticServerConnectionSuccessful(t, nsK8SOptions, "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, nsK8SOptions, staticClientName, "http://localhost:1234") }) } } @@ -229,7 +229,7 @@ func TestTerminatingGatewayNamespaceMirroring(t *testing.T) { // Test that we can make a call to the terminating gateway logger.Log(t, "trying calls to terminating gateway") - k8s.CheckStaticServerConnectionSuccessful(t, ns2K8SOptions, "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, ns2K8SOptions, staticClientName, "http://localhost:1234") }) } } diff --git a/charts/consul/test/acceptance/tests/terminating-gateway/terminating_gateway_test.go b/acceptance/tests/terminating-gateway/terminating_gateway_test.go similarity index 89% rename from charts/consul/test/acceptance/tests/terminating-gateway/terminating_gateway_test.go rename to acceptance/tests/terminating-gateway/terminating_gateway_test.go index c1ec1e748c..cb362d4445 100644 --- a/charts/consul/test/acceptance/tests/terminating-gateway/terminating_gateway_test.go +++ b/acceptance/tests/terminating-gateway/terminating_gateway_test.go @@ -7,10 +7,10 @@ import ( "testing" terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" ) @@ -93,7 +93,7 @@ func TestTerminatingGateway(t *testing.T) { // Test that we can make a call to the terminating gateway. logger.Log(t, "trying calls to terminating gateway") - k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://localhost:1234") + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") }) } } @@ -191,15 +191,20 @@ func assertNoConnectionAndAddIntention(t *testing.T, consulClient *api.Client, k t.Helper() logger.Log(t, "testing intentions prevent connections through the terminating gateway") - k8s.CheckStaticServerConnectionFailing(t, k8sOptions, "http://localhost:1234") + k8s.CheckStaticServerConnectionFailing(t, k8sOptions, staticClientName, "http://localhost:1234") logger.Log(t, "creating static-client => static-server intention") - _, err := consulClient.Connect().IntentionUpsert(&api.Intention{ - SourceName: staticClientName, - SourceNS: sourceNS, - DestinationName: staticServerName, - DestinationNS: destinationNS, - Action: api.IntentionActionAllow, + _, _, err := consulClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: staticServerName, + Namespace: destinationNS, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Namespace: sourceNS, + Action: api.IntentionActionAllow, + }, + }, }, nil) require.NoError(t, err) } diff --git a/acceptance/tests/vault/helpers.go b/acceptance/tests/vault/helpers.go new file mode 100644 index 0000000000..e24f24b718 --- /dev/null +++ b/acceptance/tests/vault/helpers.go @@ -0,0 +1,259 @@ +package vault + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "testing" + + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/go-uuid" + vapi "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/require" +) + +const ( + gossipPolicy = ` +path "consul/data/secret/gossip" { + capabilities = ["read"] +}` + replicationTokenPolicy = ` +path "consul/data/secret/replication" { + capabilities = ["read", "update"] +}` + + enterpriseLicensePolicy = ` +path "consul/data/secret/enterpriselicense" { + capabilities = ["read"] +}` + + // connectCAPolicy allows Consul to bootstrap all certificates for the service mesh in Vault. + // Adapted from https://www.consul.io/docs/connect/ca/vault#consul-managed-pki-paths. + connectCAPolicyTemplate = ` +path "/sys/mounts" { + capabilities = [ "read" ] +} + +path "/sys/mounts/connect_root" { + capabilities = [ "create", "read", "update", "delete", "list" ] +} + +path "/sys/mounts/%s/connect_inter" { + capabilities = [ "create", "read", "update", "delete", "list" ] +} + +path "/connect_root/*" { + capabilities = [ "create", "read", "update", "delete", "list" ] +} + +path "/%s/connect_inter/*" { + capabilities = [ "create", "read", "update", "delete", "list" ] +} +` + caPolicy = ` +path "pki/cert/ca" { + capabilities = ["read"] +}` +) + +// generateGossipSecret generates a random 32 byte secret returned as a base64 encoded string. +func generateGossipSecret() (string, error) { + // This code was copied from Consul's Keygen command: + // https://github.com/hashicorp/consul/blob/d652cc86e3d0322102c2b5e9026c6a60f36c17a5/command/keygen/keygen.go + + key := make([]byte, 32) + n, err := rand.Reader.Read(key) + if err != nil { + return "", fmt.Errorf("error reading random data: %s", err) + } + if n != 32 { + return "", fmt.Errorf("couldn't read enough entropy") + } + + return base64.StdEncoding.EncodeToString(key), nil +} + +// configureGossipVaultSecret generates a gossip encryption key, +// stores it in vault as a secret and configures a policy to access it. +func configureGossipVaultSecret(t *testing.T, vaultClient *vapi.Client) string { + // Create the Vault Policy for the gossip key. + logger.Log(t, "Creating gossip policy") + err := vaultClient.Sys().PutPolicy("consul-gossip", gossipPolicy) + require.NoError(t, err) + + // Generate the gossip secret. + gossipKey, err := generateGossipSecret() + require.NoError(t, err) + + // Create the gossip secret. + logger.Log(t, "Creating the gossip secret") + params := map[string]interface{}{ + "data": map[string]interface{}{ + "gossip": gossipKey, + }, + } + _, err = vaultClient.Logical().Write("consul/data/secret/gossip", params) + require.NoError(t, err) + + return gossipKey +} + +// configureEnterpriseLicenseVaultSecret stores it in vault as a secret and configures a policy to access it. +func configureEnterpriseLicenseVaultSecret(t *testing.T, vaultClient *vapi.Client, cfg *config.TestConfig) { + // Create the enterprise license secret. + logger.Log(t, "Creating the Enterprise License secret") + params := map[string]interface{}{ + "data": map[string]interface{}{ + "enterpriselicense": cfg.EnterpriseLicense, + }, + } + _, err := vaultClient.Logical().Write("consul/data/secret/enterpriselicense", params) + require.NoError(t, err) + + // Create the Vault Policy for the consul-enterpriselicense. + err = vaultClient.Sys().PutPolicy("consul-enterpriselicense", enterpriseLicensePolicy) + require.NoError(t, err) +} + +// configureKubernetesAuthRoles configures roles for the Kubernetes auth method +// that will be used by the test Helm chart installation. +func configureKubernetesAuthRoles(t *testing.T, vaultClient *vapi.Client, consulReleaseName, ns, authPath, datacenter string, cfg *config.TestConfig) { + consulClientServiceAccountName := fmt.Sprintf("%s-consul-client", consulReleaseName) + consulServerServiceAccountName := fmt.Sprintf("%s-consul-server", consulReleaseName) + sharedPolicies := "consul-gossip" + if cfg.EnableEnterprise { + sharedPolicies += ",consul-enterpriselicense" + } + + // Create the Auth Roles for consul-server and consul-client. + // Auth roles bind policies to Kubernetes service accounts, which + // then enables the Vault agent init container to call 'vault login' + // with the Kubernetes auth method to obtain a Vault token. + // Please see https://www.vaultproject.io/docs/auth/kubernetes#configuration + // for more details. + logger.Log(t, "Creating the consul-server and consul-client roles") + params := map[string]interface{}{ + "bound_service_account_names": consulClientServiceAccountName, + "bound_service_account_namespaces": ns, + "policies": sharedPolicies, + "ttl": "24h", + } + _, err := vaultClient.Logical().Write(fmt.Sprintf("auth/%s/role/consul-client", authPath), params) + require.NoError(t, err) + + params = map[string]interface{}{ + "bound_service_account_names": consulServerServiceAccountName, + "bound_service_account_namespaces": ns, + "policies": fmt.Sprintf(sharedPolicies+",connect-ca-%s,consul-server-%s,consul-replication-token", datacenter, datacenter), + "ttl": "24h", + } + _, err = vaultClient.Logical().Write(fmt.Sprintf("auth/%s/role/consul-server", authPath), params) + require.NoError(t, err) + + // Create the CA role that all components will use to fetch the Server CA certs. + params = map[string]interface{}{ + "bound_service_account_names": "*", + "bound_service_account_namespaces": ns, + "policies": "consul-ca", + "ttl": "24h", + } + _, err = vaultClient.Logical().Write(fmt.Sprintf("auth/%s/role/consul-ca", authPath), params) + require.NoError(t, err) +} + +// configurePKICA generates a CA in Vault. +func configurePKICA(t *testing.T, vaultClient *vapi.Client) { + // Create root CA to issue Consul server certificates and the `consul-server` PKI role. + // See https://learn.hashicorp.com/tutorials/consul/vault-pki-consul-secure-tls. + // Generate the root CA. + params := map[string]interface{}{ + "common_name": "Consul CA", + "ttl": "24h", + } + _, err := vaultClient.Logical().Write("pki/root/generate/internal", params) + require.NoError(t, err) + + err = vaultClient.Sys().PutPolicy("consul-ca", caPolicy) + require.NoError(t, err) +} + +// configurePKICertificates configures roles so that Consul server TLS certificates +// can be issued by Vault. +func configurePKICertificates(t *testing.T, vaultClient *vapi.Client, consulReleaseName, ns, datacenter string) string { + // Create the Vault PKI Role. + consulServerDNSName := consulReleaseName + "-consul-server" + allowedDomains := fmt.Sprintf("%s.consul,%s,%s.%s,%s.%s.svc", datacenter, consulServerDNSName, consulServerDNSName, ns, consulServerDNSName, ns) + params := map[string]interface{}{ + "allowed_domains": allowedDomains, + "allow_bare_domains": "true", + "allow_localhost": "true", + "allow_subdomains": "true", + "generate_lease": "true", + "max_ttl": "1h", + } + + pkiRoleName := fmt.Sprintf("consul-server-%s", datacenter) + + _, err := vaultClient.Logical().Write(fmt.Sprintf("pki/roles/%s", pkiRoleName), params) + require.NoError(t, err) + + certificateIssuePath := fmt.Sprintf("pki/issue/%s", pkiRoleName) + serverTLSPolicy := fmt.Sprintf(` +path %q { + capabilities = ["create", "update"] +}`, certificateIssuePath) + + // Create the server policy. + err = vaultClient.Sys().PutPolicy(pkiRoleName, serverTLSPolicy) + require.NoError(t, err) + + return certificateIssuePath +} + +// configureReplicationTokenVaultSecret generates a replication token secret ID, +// stores it in vault as a secret and configures a policy to access it. +func configureReplicationTokenVaultSecret(t *testing.T, vaultClient *vapi.Client, consulReleaseName, ns string, authMethodPaths ...string) string { + // Create the Vault Policy for the replication token. + logger.Log(t, "Creating replication token policy") + err := vaultClient.Sys().PutPolicy("consul-replication-token", replicationTokenPolicy) + require.NoError(t, err) + + // Generate the token secret. + token, err := uuid.GenerateUUID() + require.NoError(t, err) + + // Create the replication token secret. + logger.Log(t, "Creating the replication token secret") + params := map[string]interface{}{ + "data": map[string]interface{}{ + "replication": token, + }, + } + _, err = vaultClient.Logical().Write("consul/data/secret/replication", params) + require.NoError(t, err) + + logger.Log(t, "Creating kubernetes auth role for the server-acl-init job") + serverACLInitSAName := fmt.Sprintf("%s-consul-server-acl-init", consulReleaseName) + params = map[string]interface{}{ + "bound_service_account_names": serverACLInitSAName, + "bound_service_account_namespaces": ns, + "policies": "consul-replication-token", + "ttl": "24h", + } + + for _, authMethodPath := range authMethodPaths { + _, err := vaultClient.Logical().Write(fmt.Sprintf("auth/%s/role/server-acl-init", authMethodPath), params) + require.NoError(t, err) + } + + return token +} + +// createConnectCAPolicy creates the Vault Policy for the connect-ca in a given datacenter. +func createConnectCAPolicy(t *testing.T, vaultClient *vapi.Client, datacenter string) { + err := vaultClient.Sys().PutPolicy( + fmt.Sprintf("connect-ca-%s", datacenter), + fmt.Sprintf(connectCAPolicyTemplate, datacenter, datacenter)) + require.NoError(t, err) +} diff --git a/acceptance/tests/vault/main_test.go b/acceptance/tests/vault/main_test.go new file mode 100644 index 0000000000..1d3a5a5842 --- /dev/null +++ b/acceptance/tests/vault/main_test.go @@ -0,0 +1,15 @@ +package vault + +import ( + "os" + "testing" + + testsuite "github.com/hashicorp/consul-k8s/acceptance/framework/suite" +) + +var suite testsuite.Suite + +func TestMain(m *testing.M) { + suite = testsuite.NewSuite(m) + os.Exit(suite.Run()) +} diff --git a/acceptance/tests/vault/vault_test.go b/acceptance/tests/vault/vault_test.go new file mode 100644 index 0000000000..dcf151f231 --- /dev/null +++ b/acceptance/tests/vault/vault_test.go @@ -0,0 +1,156 @@ +package vault + +import ( + "context" + "fmt" + "testing" + + terratestLogger "github.com/gruntwork-io/terratest/modules/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/vault" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const staticClientName = "static-client" + +// TestVault installs Vault, bootstraps it with secrets, policies, and Kube Auth Method. +// It then configures Consul to use vault as the backend and checks that it works. +func TestVault(t *testing.T) { + cfg := suite.Config() + ctx := suite.Environment().DefaultContext(t) + ns := ctx.KubectlOptions(t).Namespace + + consulReleaseName := helpers.RandomName() + vaultReleaseName := helpers.RandomName() + + vaultCluster := vault.NewVaultCluster(t, ctx, cfg, vaultReleaseName, nil) + vaultCluster.Create(t, ctx) + // Vault is now installed in the cluster. + + // Now fetch the Vault client so we can create the policies and secrets. + vaultClient := vaultCluster.VaultClient(t) + + gossipKey := configureGossipVaultSecret(t, vaultClient) + + createConnectCAPolicy(t, vaultClient, "dc1") + if cfg.EnableEnterprise { + configureEnterpriseLicenseVaultSecret(t, vaultClient, cfg) + } + + configureKubernetesAuthRoles(t, vaultClient, consulReleaseName, ns, "kubernetes", "dc1", cfg) + + configurePKICA(t, vaultClient) + certPath := configurePKICertificates(t, vaultClient, consulReleaseName, ns, "dc1") + + vaultCASecret := vault.CASecretName(vaultReleaseName) + + consulHelmValues := map[string]string{ + "server.extraVolumes[0].type": "secret", + "server.extraVolumes[0].name": vaultCASecret, + "server.extraVolumes[0].load": "false", + + "connectInject.enabled": "true", + "connectInject.replicas": "1", + "controller.enabled": "true", + + "global.secretsBackend.vault.enabled": "true", + "global.secretsBackend.vault.consulServerRole": "consul-server", + "global.secretsBackend.vault.consulClientRole": "consul-client", + "global.secretsBackend.vault.consulCARole": "consul-ca", + + "global.secretsBackend.vault.ca.secretName": vaultCASecret, + "global.secretsBackend.vault.ca.secretKey": "tls.crt", + + "global.secretsBackend.vault.connectCA.address": vaultCluster.Address(), + "global.secretsBackend.vault.connectCA.rootPKIPath": "connect_root", + "global.secretsBackend.vault.connectCA.intermediatePKIPath": "dc1/connect_inter", + + "global.acls.manageSystemACLs": "true", + "global.tls.enabled": "true", + "global.gossipEncryption.secretName": "consul/data/secret/gossip", + "global.gossipEncryption.secretKey": "gossip", + + "ingressGateways.enabled": "true", + "ingressGateways.defaults.replicas": "1", + "terminatingGateways.enabled": "true", + "terminatingGateways.defaults.replicas": "1", + + "server.serverCert.secretName": certPath, + "global.tls.caCert.secretName": "pki/cert/ca", + "global.tls.enableAutoEncrypt": "true", + + // For sync catalog, it is sufficient to check that the deployment is running and ready + // because we only care that get-auto-encrypt-client-ca init container was able + // to talk to the Consul server using the CA from Vault. For this reason, + // we don't need any services to be synced in either direction. + "syncCatalog.enabled": "true", + "syncCatalog.toConsul": "false", + "syncCatalog.toK8S": "false", + } + + if cfg.EnableEnterprise { + consulHelmValues["global.enterpriseLicense.secretName"] = "consul/data/secret/enterpriselicense" + consulHelmValues["global.enterpriseLicense.secretKey"] = "enterpriselicense" + } + + logger.Log(t, "Installing Consul") + consulCluster := consul.NewHelmCluster(t, consulHelmValues, ctx, cfg, consulReleaseName) + consulCluster.Create(t) + + // Validate that the gossip encryption key is set correctly. + logger.Log(t, "Validating the gossip key has been set correctly.") + consulClient := consulCluster.SetupConsulClient(t, true) + keys, err := consulClient.Operator().KeyringList(nil) + require.NoError(t, err) + // There are two identical keys for LAN and WAN since there is only 1 dc. + require.Len(t, keys, 2) + require.Equal(t, 1, keys[0].PrimaryKeys[gossipKey]) + + // Confirm that the Vault Connect CA has been bootstrapped correctly. + caConfig, _, err := consulClient.Connect().CAGetConfig(nil) + require.NoError(t, err) + require.Equal(t, caConfig.Provider, "vault") + + // Validate that consul sever is running correctly and the consul members command works + tokenSecret, err := ctx.KubernetesClient(t).CoreV1().Secrets(ns).Get(context.Background(), fmt.Sprintf("%s-consul-bootstrap-acl-token", consulReleaseName), metav1.GetOptions{}) + require.NoError(t, err) + token := string(tokenSecret.Data["token"]) + + logger.Log(t, "Confirming that we can run Consul commands when exec'ing into server container") + membersOutput, err := k8s.RunKubectlAndGetOutputWithLoggerE(t, ctx.KubectlOptions(t), terratestLogger.Discard, "exec", fmt.Sprintf("%s-consul-server-0", consulReleaseName), "-c", "consul", "--", "sh", "-c", fmt.Sprintf("CONSUL_HTTP_TOKEN=%s consul members", token)) + logger.Logf(t, "Members: \n%s", membersOutput) + require.NoError(t, err) + require.Contains(t, membersOutput, fmt.Sprintf("%s-consul-server-0", consulReleaseName)) + + if cfg.EnableEnterprise { + // Validate that the enterprise license is set correctly. + logger.Log(t, "Validating the enterprise license has been set correctly.") + license, licenseErr := consulClient.Operator().LicenseGet(nil) + require.NoError(t, licenseErr) + require.True(t, license.Valid) + } + + // Deploy two services and check that they can talk to each other. + logger.Log(t, "creating static-server and static-client deployments") + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + if cfg.EnableTransparentProxy { + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") + } else { + k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") + } + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.KubectlDeleteK(t, ctx.KubectlOptions(t), "../fixtures/bases/intention") + }) + k8s.KubectlApplyK(t, ctx.KubectlOptions(t), "../fixtures/bases/intention") + + logger.Log(t, "checking that connection is successful") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://static-server") + } else { + k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), staticClientName, "http://localhost:1234") + } +} diff --git a/acceptance/tests/vault/vault_wan_fed_test.go b/acceptance/tests/vault/vault_wan_fed_test.go new file mode 100644 index 0000000000..e8ac60cf73 --- /dev/null +++ b/acceptance/tests/vault/vault_wan_fed_test.go @@ -0,0 +1,321 @@ +package vault + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/consul-k8s/acceptance/framework/config" + "github.com/hashicorp/consul-k8s/acceptance/framework/consul" + "github.com/hashicorp/consul-k8s/acceptance/framework/environment" + "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" + "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" + "github.com/hashicorp/consul-k8s/acceptance/framework/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/vault" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Test that WAN federation via Mesh gateways works with Vault +// as the secrets backend, testing all possible credentials that can be used for WAN federation. +// This test deploys a Vault cluster with a server in the primary k8s cluster and exposes it to the +// secondary cluster via a Kubernetes service. We then only need to deploy Vault agent injector +// in the secondary that will treat the Vault server in the primary as an external server. +func TestVault_WANFederationViaGateways(t *testing.T) { + cfg := suite.Config() + if !cfg.EnableMultiCluster { + t.Skipf("skipping this test because -enable-multi-cluster is not set") + } + primaryCtx := suite.Environment().DefaultContext(t) + secondaryCtx := suite.Environment().Context(t, environment.SecondaryContextName) + + ns := primaryCtx.KubectlOptions(t).Namespace + + vaultReleaseName := helpers.RandomName() + consulReleaseName := helpers.RandomName() + + // In the primary cluster, we will expose Vault server as a Load balancer + // or a NodePort service so that the secondary can connect to it. + primaryVaultHelmValues := map[string]string{ + "server.service.type": "LoadBalancer", + } + if cfg.UseKind { + primaryVaultHelmValues["server.service.type"] = "NodePort" + primaryVaultHelmValues["server.service.nodePort"] = "31000" + } + + primaryVaultCluster := vault.NewVaultCluster(t, primaryCtx, cfg, vaultReleaseName, primaryVaultHelmValues) + primaryVaultCluster.Create(t, primaryCtx) + + externalVaultAddress := vaultAddress(t, cfg, primaryCtx, vaultReleaseName) + + // In the secondary cluster, we will only deploy the agent injector and provide + // it with the primary's Vault address. We also want to configure the injector with + // a different k8s auth method path since the secondary cluster will need its own auth method. + secondaryVaultHelmValues := map[string]string{ + "server.enabled": "false", + "injector.externalVaultAddr": externalVaultAddress, + "injector.authPath": "auth/kubernetes-dc2", + } + + secondaryVaultCluster := vault.NewVaultCluster(t, secondaryCtx, cfg, vaultReleaseName, secondaryVaultHelmValues) + secondaryVaultCluster.Create(t, secondaryCtx) + + vaultClient := primaryVaultCluster.VaultClient(t) + + configureGossipVaultSecret(t, vaultClient) + + if cfg.EnableEnterprise { + configureEnterpriseLicenseVaultSecret(t, vaultClient, cfg) + } + + configureKubernetesAuthRoles(t, vaultClient, consulReleaseName, ns, "kubernetes", "dc1", cfg) + + // Configure Vault Kubernetes auth method for the secondary datacenter. + { + // Create auth method service account and ClusterRoleBinding. The Vault server + // in the primary cluster will use this service account token to talk to the secondary + // Kubernetes cluster. + // This ClusterRoleBinding is adapted from the Vault server's role: + // https://github.com/hashicorp/vault-helm/blob/b0528fce49c529f2c37953ea3a14f30ed651e0d6/templates/server-clusterrolebinding.yaml + + // Use a single name for all RBAC objects. + authMethodRBACName := fmt.Sprintf("%s-vault-auth-method", vaultReleaseName) + _, err := secondaryCtx.KubernetesClient(t).RbacV1().ClusterRoleBindings().Create(context.Background(), &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: authMethodRBACName, + }, + Subjects: []rbacv1.Subject{{Kind: rbacv1.ServiceAccountKind, Name: authMethodRBACName, Namespace: ns}}, + RoleRef: rbacv1.RoleRef{APIGroup: "rbac.authorization.k8s.io", Name: "system:auth-delegator", Kind: "ClusterRole"}, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + // Create service account for the auth method in the secondary cluster. + _, err = secondaryCtx.KubernetesClient(t).CoreV1().ServiceAccounts(ns).Create(context.Background(), &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: authMethodRBACName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + t.Cleanup(func() { + secondaryCtx.KubernetesClient(t).RbacV1().ClusterRoleBindings().Delete(context.Background(), authMethodRBACName, metav1.DeleteOptions{}) + secondaryCtx.KubernetesClient(t).CoreV1().ServiceAccounts(ns).Delete(context.Background(), authMethodRBACName, metav1.DeleteOptions{}) + }) + + // Figure out the host for the Kubernetes API. This needs to be reachable from the Vault server + // in the primary cluster. + k8sAuthMethodHost := k8s.KubernetesAPIServerHost(t, cfg, secondaryCtx) + + // Now, configure the auth method in Vault. + secondaryVaultCluster.ConfigureAuthMethod(t, vaultClient, "kubernetes-dc2", k8sAuthMethodHost, authMethodRBACName, ns) + } + + configureKubernetesAuthRoles(t, vaultClient, consulReleaseName, ns, "kubernetes-dc2", "dc2", cfg) + + // Generate a CA and create PKI roles for the primary and secondary Consul servers. + configurePKICA(t, vaultClient) + primaryCertPath := configurePKICertificates(t, vaultClient, consulReleaseName, ns, "dc1") + secondaryCertPath := configurePKICertificates(t, vaultClient, consulReleaseName, ns, "dc2") + + replicationToken := configureReplicationTokenVaultSecret(t, vaultClient, consulReleaseName, ns, "kubernetes", "kubernetes-dc2") + + // Create the Vault Policy for the Connect CA in both datacenters. + createConnectCAPolicy(t, vaultClient, "dc1") + createConnectCAPolicy(t, vaultClient, "dc2") + + // Move Vault CA secret from primary to secondary so that we can mount it to pods in the + // secondary cluster. + vaultCASecretName := vault.CASecretName(vaultReleaseName) + logger.Logf(t, "retrieving Vault CA secret %s from the primary cluster and applying to the secondary", vaultCASecretName) + vaultCASecret, err := primaryCtx.KubernetesClient(t).CoreV1().Secrets(primaryCtx.KubectlOptions(t).Namespace).Get(context.Background(), vaultCASecretName, metav1.GetOptions{}) + vaultCASecret.ResourceVersion = "" + require.NoError(t, err) + _, err = secondaryCtx.KubernetesClient(t).CoreV1().Secrets(secondaryCtx.KubectlOptions(t).Namespace).Create(context.Background(), vaultCASecret, metav1.CreateOptions{}) + require.NoError(t, err) + t.Cleanup(func() { + secondaryCtx.KubernetesClient(t).CoreV1().Secrets(ns).Delete(context.Background(), vaultCASecretName, metav1.DeleteOptions{}) + }) + + primaryConsulHelmValues := map[string]string{ + "global.datacenter": "dc1", + + "global.federation.enabled": "true", + + // TLS config. + "global.tls.enabled": "true", + "global.tls.enableAutoEncrypt": "true", + "global.tls.caCert.secretName": "pki/cert/ca", + "server.serverCert.secretName": primaryCertPath, + + // Gossip config. + "global.gossipEncryption.secretName": "consul/data/secret/gossip", + "global.gossipEncryption.secretKey": "gossip", + + // ACL config. + "global.acls.manageSystemACLs": "true", + "global.acls.createReplicationToken": "true", + "global.acls.replicationToken.secretName": "consul/data/secret/replication", + "global.acls.replicationToken.secretKey": "replication", + + // Mesh config. + "connectInject.enabled": "true", + "controller.enabled": "true", + "meshGateway.enabled": "true", + "meshGateway.replicas": "1", + + // Server config. + "server.extraVolumes[0].type": "secret", + "server.extraVolumes[0].name": vaultCASecretName, + "server.extraVolumes[0].load": "false", + + // Vault config. + "global.secretsBackend.vault.enabled": "true", + "global.secretsBackend.vault.consulServerRole": "consul-server", + "global.secretsBackend.vault.consulClientRole": "consul-client", + "global.secretsBackend.vault.consulCARole": "consul-ca", + "global.secretsBackend.vault.manageSystemACLsRole": "server-acl-init", + "global.secretsBackend.vault.ca.secretName": vaultCASecretName, + "global.secretsBackend.vault.ca.secretKey": "tls.crt", + "global.secretsBackend.vault.connectCA.address": primaryVaultCluster.Address(), + "global.secretsBackend.vault.connectCA.rootPKIPath": "connect_root", + "global.secretsBackend.vault.connectCA.intermediatePKIPath": "dc1/connect_inter", + } + + if cfg.EnableEnterprise { + primaryConsulHelmValues["global.enterpriseLicense.secretName"] = "consul/data/secret/enterpriselicense" + primaryConsulHelmValues["global.enterpriseLicense.secretKey"] = "enterpriselicense" + } + + if cfg.UseKind { + primaryConsulHelmValues["meshGateway.service.type"] = "NodePort" + primaryConsulHelmValues["meshGateway.service.nodePort"] = "30000" + } + + primaryConsulCluster := consul.NewHelmCluster(t, primaryConsulHelmValues, primaryCtx, cfg, consulReleaseName) + primaryConsulCluster.Create(t) + + // Get the address of the mesh gateway. + primaryMeshGWAddress := meshGatewayAddress(t, cfg, primaryCtx, consulReleaseName) + secondaryConsulHelmValues := map[string]string{ + "global.datacenter": "dc2", + + "global.federation.enabled": "true", + "global.federation.primaryDatacenter": "dc1", + "global.federation.primaryGateways[0]": primaryMeshGWAddress, + + // TLS config. + "global.tls.enabled": "true", + "global.tls.enableAutoEncrypt": "true", + "global.tls.caCert.secretName": "pki/cert/ca", + "server.serverCert.secretName": secondaryCertPath, + + // Gossip config. + "global.gossipEncryption.secretName": "consul/data/secret/gossip", + "global.gossipEncryption.secretKey": "gossip", + + // ACL config. + "global.acls.manageSystemACLs": "true", + "global.acls.replicationToken.secretName": "consul/data/secret/replication", + "global.acls.replicationToken.secretKey": "replication", + + // Mesh config. + "connectInject.enabled": "true", + "meshGateway.enabled": "true", + "meshGateway.replicas": "1", + + // Server config. + "server.extraVolumes[0].type": "secret", + "server.extraVolumes[0].name": vaultCASecretName, + "server.extraVolumes[0].load": "false", + + // Vault config. + "global.secretsBackend.vault.enabled": "true", + "global.secretsBackend.vault.consulServerRole": "consul-server", + "global.secretsBackend.vault.consulClientRole": "consul-client", + "global.secretsBackend.vault.consulCARole": "consul-ca", + "global.secretsBackend.vault.manageSystemACLsRole": "server-acl-init", + "global.secretsBackend.vault.ca.secretName": vaultCASecretName, + "global.secretsBackend.vault.ca.secretKey": "tls.crt", + "global.secretsBackend.vault.agentAnnotations": fmt.Sprintf("vault.hashicorp.com/tls-server-name: %s-vault", vaultReleaseName), + "global.secretsBackend.vault.connectCA.address": externalVaultAddress, + "global.secretsBackend.vault.connectCA.authMethodPath": "kubernetes-dc2", + "global.secretsBackend.vault.connectCA.rootPKIPath": "connect_root", + "global.secretsBackend.vault.connectCA.intermediatePKIPath": "dc2/connect_inter", + "global.secretsBackend.vault.connectCA.additionalConfig": fmt.Sprintf(`"{"connect": [{"ca_config": [{"tls_server_name": "%s-vault"}]}]}"`, vaultReleaseName), + } + + if cfg.EnableEnterprise { + secondaryConsulHelmValues["global.enterpriseLicense.secretName"] = "consul/data/secret/enterpriselicense" + secondaryConsulHelmValues["global.enterpriseLicense.secretKey"] = "enterpriselicense" + } + + if cfg.UseKind { + secondaryConsulHelmValues["meshGateway.service.type"] = "NodePort" + secondaryConsulHelmValues["meshGateway.service.nodePort"] = "30000" + } + + // Install the secondary consul cluster in the secondary kubernetes context. + secondaryConsulCluster := consul.NewHelmCluster(t, secondaryConsulHelmValues, secondaryCtx, cfg, consulReleaseName) + secondaryConsulCluster.Create(t) + + // Verify federation between servers. + logger.Log(t, "verifying federation was successful") + primaryClient := primaryConsulCluster.SetupConsulClient(t, true) + secondaryConsulCluster.ACLToken = replicationToken + secondaryClient := secondaryConsulCluster.SetupConsulClient(t, true) + helpers.VerifyFederation(t, primaryClient, secondaryClient, consulReleaseName, true) + + // Create a ProxyDefaults resource to configure services to use the mesh + // gateways. + logger.Log(t, "creating proxy-defaults config") + kustomizeDir := "../fixtures/bases/mesh-gateway" + k8s.KubectlApplyK(t, primaryCtx.KubectlOptions(t), kustomizeDir) + helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { + k8s.KubectlDeleteK(t, primaryCtx.KubectlOptions(t), kustomizeDir) + }) + + // Check that we can connect services over the mesh gateways. + logger.Log(t, "creating static-server in dc2") + k8s.DeployKustomize(t, secondaryCtx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") + + logger.Log(t, "creating static-client in dc1") + k8s.DeployKustomize(t, primaryCtx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-multi-dc") + + logger.Log(t, "creating intention") + _, _, err = primaryClient.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ + Kind: api.ServiceIntentions, + Name: "static-server", + Sources: []*api.SourceIntention{ + { + Name: "static-client", + Action: api.IntentionActionAllow, + }, + }, + }, nil) + require.NoError(t, err) + + logger.Log(t, "checking that connection is successful") + k8s.CheckStaticServerConnectionSuccessful(t, primaryCtx.KubectlOptions(t), staticClientName, "http://localhost:1234") +} + +// vaultAddress returns Vault's server URL depending on test configuration. +func vaultAddress(t *testing.T, cfg *config.TestConfig, ctx environment.TestContext, vaultReleaseName string) string { + vaultHost := k8s.ServiceHost(t, cfg, ctx, fmt.Sprintf("%s-vault", vaultReleaseName)) + if cfg.UseKind { + return fmt.Sprintf("https://%s:31000", vaultHost) + } + return fmt.Sprintf("https://%s:8200", vaultHost) +} + +// meshGatewayAddress returns a full address of the mesh gateway depending on configuration. +func meshGatewayAddress(t *testing.T, cfg *config.TestConfig, ctx environment.TestContext, consulReleaseName string) string { + primaryMeshGWHost := k8s.ServiceHost(t, cfg, ctx, fmt.Sprintf("%s-consul-mesh-gateway", consulReleaseName)) + if cfg.UseKind { + return fmt.Sprintf("%s:%d", primaryMeshGWHost, 30000) + } else { + return fmt.Sprintf("%s:%d", primaryMeshGWHost, 443) + } +} diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000000..c8346c9217 --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/charts/consul/.circleci/config.yml b/charts/consul/.circleci/config.yml deleted file mode 100644 index b3fe72881b..0000000000 --- a/charts/consul/.circleci/config.yml +++ /dev/null @@ -1,749 +0,0 @@ -# Originally from consul-helm -version: 2.1 -orbs: - slack: circleci/slack@3.4.2 -executors: - go: - docker: - - image: docker.mirror.hashicorp.services/circleci/golang:1.16 - environment: - - TEST_RESULTS: /tmp/test-results - -commands: - install-prereqs: - steps: - - run: - name: Install gotestsum, kind, kubectl, and helm - command: | - wget https://golang.org/dl/go1.16.5.linux-amd64.tar.gz - sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.16.5.linux-amd64.tar.gz - rm go1.16.5.linux-amd64.tar.gz - echo 'export PATH=$PATH:/usr/local/go/bin' >> $BASH_ENV - - wget https://github.com/gotestyourself/gotestsum/releases/download/v1.6.4/gotestsum_1.6.4_linux_amd64.tar.gz - sudo tar -C /usr/local/bin -xzf gotestsum_1.6.4_linux_amd64.tar.gz - rm gotestsum_1.6.4_linux_amd64.tar.gz - - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.0/kind-linux-amd64 - chmod +x ./kind - sudo mv ./kind /usr/local/bin/kind - - curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" - chmod +x ./kubectl - sudo mv ./kubectl /usr/local/bin/kubectl - - curl https://baltocdn.com/helm/signing.asc | sudo apt-key add - - sudo apt-get install apt-transport-https --yes - echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list - sudo apt-get update - sudo apt-get install helm - create-kind-clusters: - parameters: - version: - type: string - steps: - - run: - name: Create kind clusters - command: | - kind create cluster --name dc1 --image kindest/node:<< parameters.version >> - kind create cluster --name dc2 --image kindest/node:<< parameters.version >> - run-acceptance-tests: - parameters: - failfast: - type: boolean - default: false - additional-flags: - type: string - consul-k8s-image: - type: string - default: "docker.mirror.hashicorp.services/hashicorpdev/consul-k8s:latest" - steps: - - when: - condition: << parameters.failfast >> - steps: - - run: - name: Run acceptance tests - working_directory: test/acceptance/tests - no_output_timeout: 2h - command: | - # Enterprise tests can't run on fork PRs because they require - # a secret. - if [ -z "$CIRCLE_PR_NUMBER" ]; then - ENABLE_ENTERPRISE=true - fi - - # We have to run the tests for each package separately so that we can - # exit early if any test fails (-failfast only works within a single - # package). - exit_code=0 - pkgs=$(go list ./... | circleci tests split --split-by=timings --timings-type=classname) - echo "Running $(echo $pkgs | wc -w) packages:" - echo $pkgs - for pkg in $pkgs - do - if ! gotestsum --no-summary=all --jsonfile=jsonfile-${pkg////-} -- $pkg -p 1 -timeout 2h -failfast \ - << parameters.additional-flags >> \ - ${ENABLE_ENTERPRISE:+-enable-enterprise} \ - -enable-multi-cluster \ - -debug-directory="$TEST_RESULTS/debug" \ - -consul-k8s-image=<< parameters.consul-k8s-image >> - then - echo "Tests in ${pkg} failed, aborting early" - exit_code=1 - break - fi - done - gotestsum --raw-command --junitfile "$TEST_RESULTS/gotestsum-report.xml" -- cat jsonfile* - exit $exit_code - - - unless: - condition: << parameters.failfast >> - steps: - - run: - name: Run acceptance tests - working_directory: test/acceptance/tests - no_output_timeout: 2h - command: | - # Enterprise tests can't run on fork PRs because they require - # a secret. - if [ -z "$CIRCLE_PR_NUMBER" ]; then - ENABLE_ENTERPRISE=true - fi - - pkgs=$(go list ./... | circleci tests split --split-by=timings --timings-type=classname) - echo "Running $pkgs" - gotestsum --junitfile "$TEST_RESULTS/gotestsum-report.xml" -- $pkgs -p 1 -timeout 2h -failfast \ - << parameters.additional-flags >> \ - -enable-multi-cluster \ - ${ENABLE_ENTERPRISE:+-enable-enterprise} \ - -debug-directory="$TEST_RESULTS/debug" \ - -consul-k8s-image=<< parameters.consul-k8s-image >> -jobs: - unit-helm: - docker: - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 - - steps: - - checkout - - - run: - name: Run Unit Tests - command: bats --jobs 4 ./test/unit - - go-fmt-and-vet-acceptance: - executor: go - steps: - - checkout - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} - - - run: - name: go mod download - working_directory: test/acceptance - command: go mod download - - # Save go module cache if the go.mod file has changed - - save_cache: - key: consul-helm-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} - paths: - - "/go/pkg/mod" - - # check go fmt output because it does not report non-zero when there are fmt changes - - run: - name: check go fmt - working_directory: test/acceptance - command: | - files=$(go fmt ./...) - if [ -n "$files" ]; then - echo "The following file(s) do not conform to go fmt:" - echo "$files" - exit 1 - fi - - - run: - name: go vet - working_directory: test/acceptance - command: go vet ./... - - unit-acceptance-framework: - executor: go - steps: - - checkout - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} - - - run: mkdir -p $TEST_RESULTS - - - run: - name: Run tests - working_directory: test/acceptance/framework - command: | - gotestsum --junitfile $TEST_RESULTS/gotestsum-report.xml ./... -- -p 4 - - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - acceptance: - environment: - - TEST_RESULTS: /tmp/test-results - machine: - image: ubuntu-2004:202010-01 - resource_class: xlarge - parallelism: 6 - steps: - - checkout - - install-prereqs - - create-kind-clusters: - version: "v1.20.7" - - restore_cache: - keys: - - consul-helm-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - - run: - name: go mod download - working_directory: test/acceptance - command: go mod download - - save_cache: - key: consul-helm-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - paths: - - ~/.go_workspace/pkg/mod - - run: mkdir -p $TEST_RESULTS - - run-acceptance-tests: - failfast: true - additional-flags: -use-kind -kubecontext="kind-dc1" -secondary-kubecontext="kind-dc2" - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - acceptance-tproxy: - environment: - - TEST_RESULTS: /tmp/test-results - machine: - image: ubuntu-2004:202010-01 - resource_class: xlarge - parallelism: 6 - steps: - - checkout - - install-prereqs - - create-kind-clusters: - version: "v1.20.7" - - restore_cache: - keys: - - consul-helm-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - - run: - name: go mod download - working_directory: test/acceptance - command: go mod download - - save_cache: - key: consul-helm-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - paths: - - ~/.go_workspace/pkg/mod - - run: mkdir -p $TEST_RESULTS - - run-acceptance-tests: - failfast: true - additional-flags: -use-kind -kubecontext="kind-dc1" -secondary-kubecontext="kind-dc2" -enable-transparent-proxy - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - acceptance-gke-1-17: - environment: - - TEST_RESULTS: /tmp/test-results - docker: - # This image is built from test/docker/Test.dockerfile - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 - - steps: - - run: - name: Exit if forked PR - command: | - if [ -n "$CIRCLE_PR_NUMBER" ]; then - echo "Skipping acceptance tests for forked PRs; marking step successful." - circleci step halt - fi - - - checkout - - - run: - name: terraform init & apply - working_directory: test/terraform/gke - command: | - terraform init - echo "${GOOGLE_CREDENTIALS}" | gcloud auth activate-service-account --key-file=- - - # On GKE, we're setting the build number instead of build URL because label values - # cannot contain '/'. - terraform apply \ - -var project=${CLOUDSDK_CORE_PROJECT} \ - -var init_cli=true \ - -var cluster_count=2 \ - -var labels="{\"build_number\": \"$CIRCLE_BUILD_NUM\"}" \ - -auto-approve - - primary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[0]) - secondary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[1]) - - echo "export primary_kubeconfig=$primary_kubeconfig" >> $BASH_ENV - echo "export secondary_kubeconfig=$secondary_kubeconfig" >> $BASH_ENV - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} - - - run: mkdir -p $TEST_RESULTS - - - run-acceptance-tests: - additional-flags: -kubeconfig="$primary_kubeconfig" -secondary-kubeconfig="$secondary_kubeconfig" -enable-pod-security-policies -enable-transparent-proxy - - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - - run: - name: terraform destroy - working_directory: test/terraform/gke - command: | - terraform destroy -var project=${CLOUDSDK_CORE_PROJECT} -auto-approve - when: always - - - slack/status: - fail_only: true - failure_message: "GKE acceptance tests failed. Check the logs at: ${CIRCLE_BUILD_URL}" - - acceptance-aks-1-19: - environment: - - TEST_RESULTS: /tmp/test-results - docker: - # This image is built from test/docker/Test.dockerfile - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 - - steps: - - checkout - - - run: - name: terraform init & apply - working_directory: test/terraform/aks - command: | - terraform init - - terraform apply \ - -var client_id="$ARM_CLIENT_ID" \ - -var client_secret="$ARM_CLIENT_SECRET" \ - -var cluster_count=2 \ - -var tags="{\"build_url\": \"$CIRCLE_BUILD_URL\"}" \ - -auto-approve - - primary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[0]) - secondary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[1]) - - echo "export primary_kubeconfig=$primary_kubeconfig" >> $BASH_ENV - echo "export secondary_kubeconfig=$secondary_kubeconfig" >> $BASH_ENV - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} - - - run: mkdir -p $TEST_RESULTS - - - run-acceptance-tests: - additional-flags: -kubeconfig="$primary_kubeconfig" -secondary-kubeconfig="$secondary_kubeconfig" -enable-transparent-proxy - - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - - run: - name: terraform destroy - working_directory: test/terraform/aks - command: | - terraform destroy -auto-approve - when: always - - - slack/status: - fail_only: true - failure_message: "AKS acceptance tests failed. Check the logs at: ${CIRCLE_BUILD_URL}" - - acceptance-eks-1-18: - environment: - - TEST_RESULTS: /tmp/test-results - docker: - # This image is built from test/docker/Test.dockerfile - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 - - steps: - - checkout - - - run: - name: configure aws - command: | - aws configure --profile helm_user set aws_access_key_id "$AWS_ACCESS_KEY_ID" - aws configure --profile helm_user set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" - aws configure set role_arn "$AWS_ROLE_ARN" - aws configure set source_profile helm_user - - echo "unset AWS_ACCESS_KEY_ID" >> $BASH_ENV - echo "unset AWS_SECRET_ACCESS_KEY" >> $BASH_ENV - - - run: - name: terraform init & apply - working_directory: test/terraform/eks - command: | - terraform init - - terraform apply -var cluster_count=2 -var tags="{\"build_url\": \"$CIRCLE_BUILD_URL\"}" -auto-approve - - primary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[0]) - secondary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[1]) - - echo "export primary_kubeconfig=$primary_kubeconfig" >> $BASH_ENV - echo "export secondary_kubeconfig=$secondary_kubeconfig" >> $BASH_ENV - - # Change file permissions of the kubecofig files to avoid warnings by helm. - # TODO: remove when https://github.com/terraform-aws-modules/terraform-aws-eks/pull/1114 is merged. - chmod 600 "$primary_kubeconfig" - chmod 600 "$secondary_kubeconfig" - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} - - - run: mkdir -p $TEST_RESULTS - - - run-acceptance-tests: - additional-flags: -kubeconfig="$primary_kubeconfig" -secondary-kubeconfig="$secondary_kubeconfig" -enable-transparent-proxy - - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - - run: - name: terraform destroy - working_directory: test/terraform/eks - command: | - terraform destroy -auto-approve - when: always - - - slack/status: - fail_only: true - failure_message: "EKS acceptance tests failed. Check the logs at: ${CIRCLE_BUILD_URL}" - - acceptance-openshift: - environment: - TEST_RESULTS: /tmp/test-results - parallelism: 3 - docker: - # This image is built from test/docker/Test.dockerfile - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 - - steps: - - checkout - - run: - name: terraform init & apply - working_directory: test/terraform/openshift - command: | - terraform init - - az login --service-principal -u "$ARM_CLIENT_ID" -p "$ARM_CLIENT_SECRET" --tenant "$ARM_TENANT_ID" > /dev/null - terraform apply \ - -var cluster_count=2 \ - -var tags="{\"build_url\": \"$CIRCLE_BUILD_URL\"}" \ - -auto-approve - - primary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[0]) - secondary_kubeconfig=$(terraform output -json | jq -r .kubeconfigs.value[1]) - - echo "export primary_kubeconfig=$primary_kubeconfig" >> $BASH_ENV - echo "export secondary_kubeconfig=$secondary_kubeconfig" >> $BASH_ENV - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} - - - run: mkdir -p $TEST_RESULTS - - - run-acceptance-tests: - additional-flags: -kubeconfig="$primary_kubeconfig" -secondary-kubeconfig="$secondary_kubeconfig" -enable-openshift -enable-transparent-proxy - - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - run: - name: terraform destroy - working_directory: test/terraform/openshift - command: | - terraform destroy -auto-approve - when: always - - slack/status: - fail_only: true - failure_message: "OpenShift acceptance tests failed. Check the logs at: ${CIRCLE_BUILD_URL}" - - acceptance-kind-1-21: - environment: - - TEST_RESULTS: /tmp/test-results - machine: - image: ubuntu-2004:202010-01 - resource_class: xlarge - steps: - - checkout - - install-prereqs - - create-kind-clusters: - version: "v1.21.1" - - restore_cache: - keys: - - consul-helm-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - - run: - name: go mod download - working_directory: test/acceptance - command: go mod download - - save_cache: - key: consul-helm-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - paths: - - ~/.go_workspace/pkg/mod - - run: mkdir -p $TEST_RESULTS - - run-acceptance-tests: - additional-flags: -use-kind -kubecontext="kind-dc1" -secondary-kubecontext="kind-dc2" -enable-transparent-proxy - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - slack/status: - fail_only: true - failure_message: "Acceptance tests against Kind with Kubernetes v1.21 failed. Check the logs at: ${CIRCLE_BUILD_URL}" - - go-fmt-and-vet-helm-gen: - executor: go - steps: - - checkout - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-helm-gen-modcache-v1-{{ checksum "hack/helm-reference-gen/go.mod" }} - - - run: - name: go mod download - working_directory: hack/helm-reference-gen - command: go mod download - - # Save go module cache if the go.mod file has changed - - save_cache: - key: consul-helm-helm-gen-modcache-v1-{{ checksum "hack/helm-reference-gen/go.mod" }} - paths: - - "/go/pkg/mod" - - # check go fmt output because it does not report non-zero when there are fmt changes - - run: - name: check go fmt - working_directory: hack/helm-reference-gen - command: | - files=$(go fmt ./...) - if [ -n "$files" ]; then - echo "The following file(s) do not conform to go fmt:" - echo "$files" - exit 1 - fi - - - run: - name: go vet - working_directory: hack/helm-reference-gen - command: go vet ./... - - unit-helm-gen: - executor: go - steps: - - checkout - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-helm-gen-modcache-v1-{{ checksum "hack/helm-reference-gen/go.mod" }} - - - run: mkdir -p $TEST_RESULTS - - - run: - name: Run tests - working_directory: hack/helm-reference-gen - command: | - gotestsum --junitfile $TEST_RESULTS/gotestsum-report.xml ./... -- -p 4 - - - store_test_results: - path: /tmp/test-results - - store_artifacts: - path: /tmp/test-results - - test-helm-gen: - executor: go - steps: - - checkout - - # Restore go module cache if there is one - - restore_cache: - keys: - - consul-helm-helm-gen-modcache-v1-{{ checksum "hack/helm-reference-gen/go.mod" }} - - - run: mkdir -p $TEST_RESULTS - - - run: - name: Run tests - working_directory: hack/helm-reference-gen - command: | - go run ./... -validate - - update-helm-charts-index: - docker: - - image: docker.mirror.hashicorp.services/circleci/golang:latest - steps: - - checkout - - run: - name: verify Chart version matches tag version - command: | - GO111MODULE=on go get github.com/mikefarah/yq/v2 - git_tag=$(echo "${CIRCLE_TAG#v}") - chart_tag=$(yq r Chart.yaml version) - if [ "${git_tag}" != "${chart_tag}" ]; then - echo "chart version (${chart_tag}) did not match git version (${git_tag})" - exit 1 - fi - - run: - name: update helm-charts index - command: | - curl --show-error --silent --fail --user "${CIRCLE_TOKEN}:" \ - -X POST \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -d "{\"branch\": \"master\",\"parameters\":{\"SOURCE_REPO\": \"${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}\",\"SOURCE_TAG\": \"${CIRCLE_TAG}\"}}" \ - "${CIRCLE_ENDPOINT}/${CIRCLE_PROJECT}/pipeline" - - slack/status: - fail_only: true - failure_message: "Failed to trigger an update to the helm charts index. Check the logs at: ${CIRCLE_BUILD_URL}" - - cleanup-azure-resources: - docker: - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 - steps: - - run: - name: cleanup leftover resources - command: | - az login --service-principal -u "$ARM_CLIENT_ID" -p "$ARM_CLIENT_SECRET" --tenant "$ARM_TENANT_ID" > /dev/null - resource_groups=$(az group list -o json | jq -r '.[] | select(.name | test("^consul-k8s-\\d+$")) | .name') - for group in $resource_groups; do - echo "Deleting $group resource group" - az group delete -n $group --yes - done - - - slack/status: - fail_only: true - failure_message: "AKS cleanup failed" - - cleanup-gcp-resources: - docker: - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.10.0 - steps: - - run: - name: cleanup leftover resources - command: | - echo "${GOOGLE_CREDENTIALS}" | gcloud auth activate-service-account --key-file=- - clusters=$(gcloud container clusters list --zone us-central1-a --project ${CLOUDSDK_CORE_PROJECT} --format json | jq -r '.[] | select(.name | test("^consul-k8s-\\d+$")) | .name') - for cluster in $clusters; do - echo "Deleting $cluster GKE cluster" - gcloud container clusters delete $cluster --zone us-central1-a --project ${CLOUDSDK_CORE_PROJECT} --quiet - done - - - slack/status: - fail_only: true - failure_message: "GKE cleanup failed" - - cleanup-eks-resources: - docker: - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-helm-test:0.9.0 - steps: - - checkout - - run: - name: cleanup eks resources - command: | - # Assume the role and set environment variables. - aws sts assume-role --role-arn "$AWS_ROLE_ARN" --role-session-name "consul-helm-$CIRCLE_BUILD_NUM" --duration-seconds 10800 > assume-role.json - export AWS_ACCESS_KEY_ID="$(jq -r .Credentials.AccessKeyId assume-role.json)" - export AWS_SECRET_ACCESS_KEY="$(jq -r .Credentials.SecretAccessKey assume-role.json)" - export AWS_SESSION_TOKEN="$(jq -r .Credentials.SessionToken assume-role.json)" - - cd hack/aws-acceptance-test-cleanup - go run ./... -auto-approve - - - slack/status: - fail_only: true - failure_message: "EKS cleanup failed" - -workflows: - version: 2 - test: - jobs: - - go-fmt-and-vet-acceptance - - go-fmt-and-vet-helm-gen - - unit-acceptance-framework: - requires: - - go-fmt-and-vet-acceptance - - unit-helm-gen: - requires: - - go-fmt-and-vet-helm-gen - - test-helm-gen - - unit-helm - - acceptance: - requires: - - unit-helm - - unit-acceptance-framework - - acceptance-tproxy: - requires: - - unit-helm - - unit-acceptance-framework - nightly-acceptance-tests: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - jobs: - - cleanup-gcp-resources - - cleanup-azure-resources - - cleanup-eks-resources -# - acceptance-openshift: <-- Disabled until we can make them less flakey. -# requires: -# - cleanup-azure-resources - - acceptance-gke-1-17: - requires: - - cleanup-gcp-resources - - acceptance-eks-1-18: - requires: - - cleanup-eks-resources - - acceptance-aks-1-19: - requires: - - cleanup-azure-resources - - acceptance-kind-1-21 - update-helm-charts-index: - jobs: - - update-helm-charts-index: - context: helm-charts-trigger-consul - filters: - tags: - only: /^v.*/ - branches: - ignore: /.*/ diff --git a/charts/consul/Chart.yaml b/charts/consul/Chart.yaml index 0fe255fa76..ae895af58b 100644 --- a/charts/consul/Chart.yaml +++ b/charts/consul/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 name: consul -version: 0.33.0 -appVersion: 1.10.0 -kubeVersion: ">=1.17.0-0" +version: 0.41.1 +appVersion: 1.11.3 +kubeVersion: ">=1.19.0-0" description: Official HashiCorp Consul Chart home: https://www.consul.io icon: https://raw.githubusercontent.com/hashicorp/consul-k8s/main/assets/icon.png @@ -13,11 +13,11 @@ annotations: artifacthub.io/prerelease: false artifacthub.io/images: | - name: consul - image: hashicorp/consul:1.10.0 + image: hashicorp/consul:1.11.3 - name: consul-k8s-control-plane - image: hashicorp/consul-k8s-control-plane:0.33.0 + image: hashicorp/consul-k8s-control-plane:0.41.1 - name: envoy - image: envoyproxy/envoy-alpine:v1.18.4 + image: envoyproxy/envoy-alpine:v1.20.2 artifacthub.io/license: MPL-2.0 artifacthub.io/links: | - name: Documentation diff --git a/charts/consul/Makefile b/charts/consul/Makefile deleted file mode 100644 index 43543821d8..0000000000 --- a/charts/consul/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -TEST_IMAGE?=consul-helm-test - -test-docker: - @docker build --rm -t '$(TEST_IMAGE)' -f $(CURDIR)/test/docker/Test.dockerfile $(CURDIR) - -# Generate Helm reference docs from values.yaml and update Consul website. -# Usage: make gen-docs consul= -gen-docs: - @cd hack/helm-reference-gen; go run ./... $(consul) - -.PHONY: test-docker diff --git a/charts/consul/README.md b/charts/consul/README.md index 0689880348..d5e0eef355 100644 --- a/charts/consul/README.md +++ b/charts/consul/README.md @@ -26,8 +26,8 @@ by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com). non-Kubernetes nodes to easily discover and access Kubernetes services. ### Prerequisites - * **Helm 3.0+** (Helm 2 is not supported) - * **Kubernetes 1.17+** - This is the earliest version of Kubernetes tested. + * **Helm 3.2+** (Helm 2 is not supported) + * **Kubernetes 1.18+** - This is the earliest version of Kubernetes tested. It is possible that this chart works with earlier versions but it is untested. @@ -40,15 +40,17 @@ Detailed installation instructions for Consul on Kubernetes are found [here](htt $ helm repo add hashicorp https://helm.releases.hashicorp.com "hashicorp" has been added to your repositories -2. Ensure you have access to the consul chart: +2. Ensure you have access to the Consul Helm chart and you see the latest chart version listed. + If you have previously added the HashiCorp Helm repository, run `helm repo update`. $ helm search repo hashicorp/consul NAME CHART VERSION APP VERSION DESCRIPTION - hashicorp/consul 0.33.0 1.10.0 Official HashiCorp Consul Chart + hashicorp/consul 0.35.0 1.10.3 Official HashiCorp Consul Chart -3. Now you're ready to install Consul! To install Consul with the default configuration using Helm 3 run: +3. Now you're ready to install Consul! To install Consul with the default configuration using Helm 3.2 run the following command below. + This will create a `consul` Kubernetes namespace if not already present, and install Consul on the dedicated namespace. - $ helm install consul hashicorp/consul --set global.name=consul + $ helm install consul hashicorp/consul --set global.name=consul --create-namespace -n consul NAME: consul Please see the many options supported in the `values.yaml` diff --git a/charts/consul/templates/NOTES.txt b/charts/consul/templates/NOTES.txt index f14c2e7b43..135948a375 100644 --- a/charts/consul/templates/NOTES.txt +++ b/charts/consul/templates/NOTES.txt @@ -1,12 +1,6 @@ Thank you for installing HashiCorp Consul! -Now that you have deployed Consul, you should look over the docs on using -Consul with Kubernetes available here: - -https://www.consul.io/docs/platform/k8s/index.html - - Your release is named {{ .Release.Name }}. To learn more about the release, run: @@ -14,6 +8,11 @@ To learn more about the release, run: $ helm status {{ .Release.Name }} $ helm get all {{ .Release.Name }} +Consul on Kubernetes Documentation: +https://www.consul.io/docs/platform/k8s + +Consul on Kubernetes CLI Reference: +https://www.consul.io/docs/k8s/k8s-cli {{- if (and .Values.global.acls.manageSystemACLs (gt (len .Values.server.extraConfig) 3)) }} Warning: Defining server extraConfig potentially disrupts the automatic ACL diff --git a/charts/consul/templates/_helpers.tpl b/charts/consul/templates/_helpers.tpl index 173eb68cc9..b266907b4b 100644 --- a/charts/consul/templates/_helpers.tpl +++ b/charts/consul/templates/_helpers.tpl @@ -15,6 +15,64 @@ as well as the global.name setting. {{- end -}} {{- end -}} +{{- define "consul.vaultSecretTemplate" -}} + | + {{ "{{" }}- with secret "{{ .secretName }}" -{{ "}}" }} + {{ "{{" }}- {{ printf ".Data.data.%s" .secretKey }} -{{ "}}" }} + {{ "{{" }}- end -{{ "}}" }} +{{- end -}} + +{{- define "consul.serverTLSCATemplate" -}} + | + {{ "{{" }}- with secret "{{ .Values.global.tls.caCert.secretName }}" -{{ "}}" }} + {{ "{{" }}- .Data.certificate -{{ "}}" }} + {{ "{{" }}- end -{{ "}}" }} +{{- end -}} + +{{- define "consul.serverTLSCertTemplate" -}} + | + {{ "{{" }}- with secret "{{ .Values.server.serverCert.secretName }}" "{{ printf "common_name=server.%s.%s" .Values.global.datacenter .Values.global.domain }}" + "ttl=1h" "alt_names={{ include "consul.serverTLSAltNames" . }}" "ip_sans=127.0.0.1{{ include "consul.serverAdditionalIPSANs" . }}" -{{ "}}" }} + {{ "{{" }}- .Data.certificate -{{ "}}" }} + {{ "{{" }}- end -{{ "}}" }} +{{- end -}} + +{{- define "consul.serverTLSKeyTemplate" -}} + | + {{ "{{" }}- with secret "{{ .Values.server.serverCert.secretName }}" "{{ printf "common_name=server.%s.%s" .Values.global.datacenter .Values.global.domain }}" + "ttl=1h" "alt_names={{ include "consul.serverTLSAltNames" . }}" "ip_sans=127.0.0.1{{ include "consul.serverAdditionalIPSANs" . }}" -{{ "}}" }} + {{ "{{" }}- .Data.private_key -{{ "}}" }} + {{ "{{" }}- end -{{ "}}" }} +{{- end -}} + +{{- define "consul.serverTLSAltNames" -}} +{{- $name := include "consul.fullname" . -}} +{{- $ns := .Release.Namespace -}} +{{ printf "localhost,%s-server,*.%s-server,*.%s-server.%s,*.%s-server.%s.svc,*.server.%s.%s" $name $name $name $ns $name $ns (.Values.global.datacenter ) (.Values.global.domain) }}{{ include "consul.serverAdditionalDNSSANs" . }} +{{- end -}} + +{{- define "consul.serverAdditionalDNSSANs" -}} +{{- if .Values.global.tls -}}{{- if .Values.global.tls.serverAdditionalDNSSANs -}}{{- range $san := .Values.global.tls.serverAdditionalDNSSANs }},{{ $san }} {{- end -}}{{- end -}}{{- end -}} +{{- end -}} + +{{- define "consul.serverAdditionalIPSANs" -}} +{{- if .Values.global.tls -}}{{- if .Values.global.tls.serverAdditionalIPSANs -}}{{- range $ipsan := .Values.global.tls.serverAdditionalIPSANs }},{{ $ipsan }} {{- end -}}{{- end -}}{{- end -}} +{{- end -}} + +{{- define "consul.vaultReplicationTokenTemplate" -}} +| + {{ "{{" }}- with secret "{{ .Values.global.acls.replicationToken.secretName }}" -{{ "}}" }} + {{ "{{" }}- {{ printf ".Data.data.%s" .Values.global.acls.replicationToken.secretKey }} -{{ "}}" }} + {{ "{{" }}- end -{{ "}}" }} +{{- end -}} + +{{- define "consul.vaultReplicationTokenConfigTemplate" -}} +| + {{ "{{" }}- with secret "{{ .Values.global.acls.replicationToken.secretName }}" -{{ "}}" }} + acl { tokens { agent = "{{ "{{" }}- {{ printf ".Data.data.%s" .Values.global.acls.replicationToken.secretKey }} -{{ "}}" }}", replication = "{{ "{{" }}- {{ printf ".Data.data.%s" .Values.global.acls.replicationToken.secretKey }} -{{ "}}" }}" }} + {{ "{{" }}- end -{{ "}}" }} +{{- end -}} + {{/* Sets up the extra-from-values config file passed to consul and then uses sed to do any necessary substitution for HOST_IP/POD_IP/HOSTNAME. Useful for dogstats telemetry. The output file @@ -28,6 +86,18 @@ is passed to consul as a -config-file param on command line. [ -n "${HOSTNAME}" ] && sed -Ei "s|HOSTNAME|${HOSTNAME?}|g" /consul/extra-config/extra-from-values.json {{- end -}} +{{/* +Sets up a list of recusor flags for Consul agents by iterating over the IPs of every nameserver +in /etc/resolv.conf and concatenating them into a string of arguments that can be passed directly +to the consul agent command. +*/}} +{{- define "consul.recursors" -}} + recursor_flags="" + for ip in $(cat /etc/resolv.conf | grep nameserver | cut -d' ' -f2) + do + recursor_flags="$recursor_flags -recursor=$ip" + done +{{- end -}} {{/* Create chart name and version as used by the chart label. @@ -102,13 +172,19 @@ This template is for an init container. {{- else }} -server-addr={{ template "consul.fullname" . }}-server \ -server-port=8501 \ + {{- if .Values.global.secretsBackend.vault.enabled }} + -ca-file=/vault/secrets/serverca.crt + {{- else }} -ca-file=/consul/tls/ca/tls.crt {{- end }} + {{- end }} volumeMounts: {{- if not (and .Values.externalServers.enabled .Values.externalServers.useSystemRoots) }} + {{- if not .Values.global.secretsBackend.vault.enabled }} - name: consul-ca-cert mountPath: /consul/tls/ca {{- end }} + {{- end }} - name: consul-auto-encrypt-ca-cert mountPath: /consul/tls/client/ca resources: @@ -119,3 +195,21 @@ This template is for an init container. memory: "50Mi" cpu: "50m" {{- end -}} + +{{/* +Fails when a reserved name is passed in. This should be used to test against +Consul namespaces and partition names. +This template accepts an array that contains two elements. The first element +is the name that's being checked and the second is the name of the values.yaml +key that's setting the name. + +Usage: {{ template "consul.reservedNamesFailer" (list .Values.key "key") }} + +*/}} +{{- define "consul.reservedNamesFailer" -}} +{{- $name := index . 0 -}} +{{- $key := index . 1 -}} +{{- if or (eq "system" $name) (eq "universal" $name) (eq "consul" $name) (eq "operator" $name) (eq "root" $name) }} +{{- fail (cat "The name" $name "set for key" $key "is reserved by Consul for future use." ) }} +{{- end }} +{{- end -}} diff --git a/charts/consul/templates/api-gateway-controller-clusterrole.yaml b/charts/consul/templates/api-gateway-controller-clusterrole.yaml new file mode 100644 index 0000000000..9f9acb2e86 --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-clusterrole.yaml @@ -0,0 +1,221 @@ +{{- if .Values.apiGateway.enabled }} +# The ClusterRole to enable the API Gateway controller to access required api endpoints. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +rules: +- apiGroups: + - api-gateway.consul.hashicorp.com + resources: + - gatewayclassconfigs + verbs: + - get + - list + - update + - watch +- apiGroups: + - api-gateway.consul.hashicorp.com + resources: + - gatewayclassconfigs/finalizers + verbs: + - update +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - get + - list + - update + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - list + - update +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - list + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - services + verbs: + - create + - get + - list + - update + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - get + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses/status + verbs: + - get + - patch + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways/status + verbs: + - get + - patch + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/status + verbs: + - get + - patch + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - tcproutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - tcproutes/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - tcproutes/status + verbs: + - get + - patch + - update +{{- end }} diff --git a/charts/consul/templates/api-gateway-controller-clusterrolebinding.yaml b/charts/consul/templates/api-gateway-controller-clusterrolebinding.yaml new file mode 100644 index 0000000000..d083a08129 --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-clusterrolebinding.yaml @@ -0,0 +1,20 @@ +{{- if .Values.apiGateway.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ template "consul.fullname" . }}-api-gateway-controller +subjects: +- kind: ServiceAccount + name: {{ template "consul.fullname" . }}-api-gateway-controller + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/consul/templates/api-gateway-controller-deployment.yaml b/charts/consul/templates/api-gateway-controller-deployment.yaml new file mode 100644 index 0000000000..492d9f3302 --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-deployment.yaml @@ -0,0 +1,155 @@ +{{- if .Values.apiGateway.enabled }} +{{- if not .Values.client.grpc }}{{ fail "client.grpc must be true for api gateway" }}{{ end }} +{{- if not .Values.apiGateway.image}}{{ fail "apiGateway.image must be set to enable api gateway" }}{{ end }} +{{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +spec: + replicas: {{ .Values.apiGateway.controller.replicas }} + selector: + matchLabels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + release: {{ .Release.Name }} + component: api-gateway-controller + template: + metadata: + annotations: + consul.hashicorp.com/connect-inject: "false" + {{- if (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- end }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + release: {{ .Release.Name }} + component: api-gateway-controller + spec: + serviceAccountName: {{ template "consul.fullname" . }}-api-gateway-controller + containers: + - name: api-gateway-controller + image: {{ .Values.apiGateway.image }} + ports: + - containerPort: 9090 + name: sds + protocol: TCP + env: + {{- if .Values.global.tls.enabled }} + - name: CONSUL_CACERT + value: /consul/tls/ca/tls.crt + {{- end }} + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + {{- if .Values.global.acls.manageSystemACLs }} + - name: CONSUL_HTTP_TOKEN + valueFrom: + secretKeyRef: + name: "{{ template "consul.fullname" . }}-api-gateway-controller-acl-token" + key: "token" + {{- end }} + - name: CONSUL_HTTP_ADDR + {{- if .Values.global.tls.enabled }} + value: https://$(HOST_IP):8501 + {{- else }} + value: http://$(HOST_IP):8500 + {{- end }} + command: + - "/bin/sh" + - "-ec" + - | + consul-api-gateway server \ + -sds-server-host {{ template "consul.fullname" . }}-api-gateway-controller.{{ .Release.Namespace }}.svc \ + -k8s-namespace {{ .Release.Namespace }} \ + {{- if .Values.global.enableConsulNamespaces }} + {{- if .Values.apiGateway.consulNamespaces.consulDestinationNamespace }} + -consul-destination-namespace={{ .Values.apiGateway.consulNamespaces.consulDestinationNamespace }} \ + {{- end }} + {{- if .Values.apiGateway.consulNamespaces.mirroringK8S }} + -mirroring-k8s=true \ + {{- if .Values.apiGateway.consulNamespaces.mirroringK8SPrefix }} + -mirroring-k8s-prefix={{ .Values.apiGateway.consulNamespaces.mirroringK8SPrefix }} \ + {{- end }} + {{- end }} + {{- end }} + -log-level {{ default .Values.global.logLevel .Values.apiGateway.logLevel }} \ + volumeMounts: + {{- if .Values.global.tls.enabled }} + {{- if .Values.global.tls.enableAutoEncrypt }} + - name: consul-auto-encrypt-ca-cert + {{- else }} + - name: consul-ca-cert + {{- end }} + mountPath: /consul/tls/ca + readOnly: true + {{- end }} + volumes: + {{- if .Values.global.tls.enabled }} + {{- if not (and .Values.externalServers.enabled .Values.externalServers.useSystemRoots) }} + - name: consul-ca-cert + secret: + {{- if .Values.global.tls.caCert.secretName }} + secretName: {{ .Values.global.tls.caCert.secretName }} + {{- else }} + secretName: {{ template "consul.fullname" . }}-ca-cert + {{- end }} + items: + - key: {{ default "tls.crt" .Values.global.tls.caCert.secretKey }} + path: tls.crt + {{- end }} + {{- if .Values.global.tls.enableAutoEncrypt }} + - name: consul-auto-encrypt-ca-cert + emptyDir: + medium: "Memory" + {{- end }} + {{- end }} + {{- if or (and .Values.global.acls.manageSystemACLs) (and .Values.global.tls.enabled .Values.global.tls.enableAutoEncrypt) }} + initContainers: + {{- if .Values.global.acls.manageSystemACLs }} + - name: api-gateway-controller-acl-init + image: {{ .Values.global.imageK8S }} + command: + - "/bin/sh" + - "-ec" + - | + consul-k8s-control-plane acl-init \ + -secret-name="{{ template "consul.fullname" . }}-api-gateway-controller-acl-token" \ + -k8s-namespace={{ .Release.Namespace }} + resources: + requests: + memory: "25Mi" + cpu: "50m" + limits: + memory: "25Mi" + cpu: "50m" + {{- end }} + {{- if (and .Values.global.tls.enabled .Values.global.tls.enableAutoEncrypt) }} + {{- include "consul.getAutoEncryptClientCA" . | nindent 6 }} + {{- end }} + {{- end }} + {{- if .Values.apiGateway.controller.priorityClassName }} + priorityClassName: {{ .Values.apiGateway.controller.priorityClassName | quote }} + {{- end }} + {{- if .Values.apiGateway.controller.nodeSelector }} + nodeSelector: + {{ tpl .Values.apiGateway.controller.nodeSelector . | indent 8 | trim }} + {{- end }} +{{- end }} diff --git a/charts/consul/templates/api-gateway-controller-service.yaml b/charts/consul/templates/api-gateway-controller-service.yaml new file mode 100644 index 0000000000..aa79ff9fc3 --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-service.yaml @@ -0,0 +1,27 @@ +{{- if .Values.apiGateway.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller + annotations: + {{- if .Values.apiGateway.controller.service.annotations }} + {{ tpl .Values.apiGateway.controller.service.annotations . | nindent 4 | trim }} + {{- end }} +spec: + ports: + - name: sds + port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app: {{ template "consul.name" . }} + release: "{{ .Release.Name }}" + component: api-gateway-controller +{{- end }} diff --git a/charts/consul/templates/connect-inject-authmethod-serviceaccount.yaml b/charts/consul/templates/api-gateway-controller-serviceaccount.yaml similarity index 53% rename from charts/consul/templates/connect-inject-authmethod-serviceaccount.yaml rename to charts/consul/templates/api-gateway-controller-serviceaccount.yaml index 7ba0424be0..98292a8dbe 100644 --- a/charts/consul/templates/connect-inject-authmethod-serviceaccount.yaml +++ b/charts/consul/templates/api-gateway-controller-serviceaccount.yaml @@ -1,15 +1,19 @@ -{{- if or (and (ne (.Values.connectInject.enabled | toString) "-") .Values.connectInject.enabled) (and (eq (.Values.connectInject.enabled | toString) "-") .Values.global.enabled) }} -{{- if .Values.global.acls.manageSystemACLs }} +{{- if .Values.apiGateway.enabled }} apiVersion: v1 kind: ServiceAccount metadata: - name: {{ template "consul.fullname" . }}-connect-injector-authmethod-svc-account + name: {{ template "consul.fullname" . }}-api-gateway-controller namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: api-gateway-controller + {{- if .Values.apiGateway.serviceAccount.annotations }} + annotations: + {{ tpl .Values.apiGateway.serviceAccount.annotations . | nindent 4 | trim }} + {{- end }} {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- range . }} @@ -17,4 +21,3 @@ imagePullSecrets: {{- end }} {{- end }} {{- end }} -{{- end }} diff --git a/charts/consul/templates/api-gateway-gatewayclass.yaml b/charts/consul/templates/api-gateway-gatewayclass.yaml new file mode 100644 index 0000000000..d9ba85e633 --- /dev/null +++ b/charts/consul/templates/api-gateway-gatewayclass.yaml @@ -0,0 +1,18 @@ +{{- if (and .Values.apiGateway.enabled .Values.apiGateway.managedGatewayClass.enabled) }} +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GatewayClass +metadata: + name: consul-api-gateway + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +spec: + controllerName: hashicorp.com/consul-api-gateway-controller + parametersRef: + group: api-gateway.consul.hashicorp.com + kind: GatewayClassConfig + name: consul-api-gateway +{{- end }} diff --git a/charts/consul/templates/api-gateway-gatewayclassconfig.yaml b/charts/consul/templates/api-gateway-gatewayclassconfig.yaml new file mode 100644 index 0000000000..d0e6e453ae --- /dev/null +++ b/charts/consul/templates/api-gateway-gatewayclassconfig.yaml @@ -0,0 +1,46 @@ +{{- if (and .Values.apiGateway.enabled .Values.apiGateway.managedGatewayClass.enabled) }} +apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 +kind: GatewayClassConfig +metadata: + name: consul-api-gateway + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway +spec: + consul: + authentication: + {{- if .Values.global.acls.manageSystemACLs }} + managed: true + method: {{ template "consul.fullname" . }}-k8s-auth-method + {{- end }} + {{- if .Values.global.tls.enabled }} + scheme: https + {{- else }} + scheme: http + {{- end }} + ports: + grpc: 8502 + {{- if .Values.global.tls.enabled }} + http: 8501 + {{- else }} + http: 8500 + {{- end }} + image: + consulAPIGateway: {{ .Values.apiGateway.image }} + envoy: {{ .Values.global.imageEnvoy }} + {{- if .Values.apiGateway.nodeSelector }} + nodeSelector: + {{ tpl .Values.apiGateway.managedGatewayClass.nodeSelector . | indent 4 | trim }} + {{- end }} + {{- if .Values.apiGateway.managedGatewayClass.copyAnnotations.service }} + copyAnnotations: + service: + {{ tpl .Values.apiGateway.managedGatewayClass.copyAnnotations.service.annotations . | nindent 6 | trim }} + {{- end }} + serviceType: {{ .Values.apiGateway.managedGatewayClass.serviceType }} + useHostPorts: {{ .Values.apiGateway.managedGatewayClass.useHostPorts }} + logLevel: {{ default .Values.global.logLevel .Values.apiGateway.managedGatewayClass.logLevel }} +{{- end }} diff --git a/charts/consul/templates/client-config-configmap.yaml b/charts/consul/templates/client-config-configmap.yaml index bfdeacc962..5954c1f86f 100644 --- a/charts/consul/templates/client-config-configmap.yaml +++ b/charts/consul/templates/client-config-configmap.yaml @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client data: extra-from-values.json: |- {{ tpl .Values.client.extraConfig . | trimAll "\"" | indent 4 }} diff --git a/charts/consul/templates/client-daemonset.yaml b/charts/consul/templates/client-daemonset.yaml index ddf17bc624..d5d4a07de0 100644 --- a/charts/consul/templates/client-daemonset.yaml +++ b/charts/consul/templates/client-daemonset.yaml @@ -1,16 +1,27 @@ +{{- if .Values.global.imageK8s }}{{ fail "global.imageK8s is not a valid key, use global.imageK8S (note the capital 'S')" }}{{ end -}} {{- if (or (and (ne (.Values.client.enabled | toString) "-") .Values.client.enabled) (and (eq (.Values.client.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and (and .Values.global.tls.enabled .Values.global.tls.httpsOnly) (and .Values.global.metrics.enabled .Values.global.metrics.enableAgentMetrics))}}{{ fail "global.metrics.enableAgentMetrics cannot be enabled if TLS (HTTPS only) is enabled" }}{{ end -}} +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled $serverEnabled (ne .Values.global.adminPartitions.name "default"))}}{{ fail "global.adminPartitions.name has to be \"default\" in the server cluster" }}{{ end -}} +{{- if (and (not .Values.global.secretsBackend.vault.consulClientRole) .Values.global.secretsBackend.vault.enabled) }}{{ fail "global.secretsBackend.vault.consulClientRole must be provided if global.secretsBackend.vault.enabled=true." }}{{ end -}} +{{- if (and (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) (not .Values.global.tls.caCert.secretName)) }}{{ fail "global.tls.caCert.secretName must be provided if global.tls.enabled=true and global.secretsBackend.vault.enabled=true." }}{{ end -}} +{{- if (and (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) (not .Values.global.tls.enableAutoEncrypt)) }}{{ fail "global.tls.enableAutoEncrypt must be true if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" }}{{ end -}} +{{- if (and (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) (not .Values.global.secretsBackend.vault.consulCARole)) }}{{ fail "global.secretsBackend.vault.consulCARole must be provided if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" }}{{ end -}} +{{- if and .Values.global.federation.enabled .Values.global.adminPartitions.enabled }}{{ fail "If global.federation.enabled is true, global.adminPartitions.enabled must be false because they are mutually exclusive" }}{{ end }} +{{- if (and .Values.global.enterpriseLicense.secretName (not .Values.global.enterpriseLicense.secretKey)) }}{{fail "enterpriseLicense.secretKey and secretName must both be specified." }}{{ end -}} +{{- if (and (not .Values.global.enterpriseLicense.secretName) .Values.global.enterpriseLicense.secretKey) }}{{fail "enterpriseLicense.secretKey and secretName must both be specified." }}{{ end -}} # DaemonSet to run the Consul clients on every node. apiVersion: apps/v1 kind: DaemonSet metadata: - name: {{ template "consul.fullname" . }} + name: {{ template "consul.fullname" . }}-client namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client spec: {{- if .Values.client.updateStrategy }} updateStrategy: @@ -35,6 +46,33 @@ spec: {{- toYaml .Values.client.extraLabels | nindent 8 }} {{- end }} annotations: + {{- if .Values.global.secretsBackend.vault.enabled }} + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": "{{ .Values.global.secretsBackend.vault.consulClientRole }}" + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- if .Values.global.gossipEncryption.secretName }} + {{- with .Values.global.gossipEncryption }} + "vault.hashicorp.com/agent-inject-secret-gossip.txt": {{ .secretName }} + "vault.hashicorp.com/agent-inject-template-gossip.txt": {{ template "consul.vaultSecretTemplate" . }} + {{- end }} + {{- end }} + {{- if .Values.global.tls.enabled }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- if .Values.global.enterpriseLicense.secretName }} + {{- with .Values.global.enterpriseLicense }} + "vault.hashicorp.com/agent-inject-secret-enterpriselicense.txt": "{{ .secretName }}" + "vault.hashicorp.com/agent-inject-template-enterpriselicense.txt": {{ template "consul.vaultSecretTemplate" . }} + {{- end }} + {{- end }} + {{- end }} "consul.hashicorp.com/connect-inject": "false" "consul.hashicorp.com/config-checksum": {{ include (print $.Template.BasePath "/client-config-configmap.yaml") . | sha256sum }} {{- if .Values.client.annotations }} @@ -87,6 +125,7 @@ spec: configMap: name: {{ template "consul.fullname" . }}-client-config {{- if .Values.global.tls.enabled }} + {{- if not .Values.global.secretsBackend.vault.enabled }} - name: consul-ca-cert secret: {{- if .Values.global.tls.caCert.secretName }} @@ -115,6 +154,7 @@ spec: medium: "Memory" {{- end }} {{- end }} + {{- end }} {{- range .Values.client.extraVolumes }} - name: userconfig-{{ .name }} {{ .type }}: @@ -128,10 +168,10 @@ spec: - name: aclconfig emptyDir: {} {{- else }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-license secret: - secretName: {{ .Values.server.enterpriseLicense.secretName }} + secretName: {{ .Values.global.enterpriseLicense.secretName }} {{- end }} {{- end }} containers: @@ -166,16 +206,27 @@ spec: fieldPath: status.podIP - name: CONSUL_DISABLE_PERM_MGMT value: "true" - {{- if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} + {{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }} + {{- if not .Values.global.secretsBackend.vault.enabled }} - name: GOSSIP_KEY valueFrom: secretKeyRef: + {{- if .Values.global.gossipEncryption.autoGenerate }} + name: {{ template "consul.fullname" . }}-gossip-encryption-key + key: key + {{- else if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} name: {{ .Values.global.gossipEncryption.secretName }} key: {{ .Values.global.gossipEncryption.secretKey }} + {{- end }} + {{- end }} {{- end }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload (not .Values.global.acls.manageSystemACLs)) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload (not .Values.global.acls.manageSystemACLs)) }} - name: CONSUL_LICENSE_PATH - value: /consul/license/{{ .Values.server.enterpriseLicense.secretKey }} + {{- if .Values.global.secretsBackend.vault.enabled }} + value: /vault/secrets/enterpriselicense.txt + {{- else }} + value: /consul/license/{{ .Values.global.enterpriseLicense.secretKey }} + {{- end }} {{- end }} {{- if .Values.global.tls.enabled }} - name: CONSUL_HTTP_ADDR @@ -195,6 +246,13 @@ spec: - | CONSUL_FULLNAME="{{template "consul.fullname" . }}" + {{- if and .Values.global.secretsBackend.vault.enabled .Values.global.gossipEncryption.secretName }} + GOSSIP_KEY=`cat /vault/secrets/gossip.txt` + {{- end }} + {{- if (and .Values.dns.enabled .Values.dns.enableRedirection) }} + {{ template "consul.recursors" }} + {{- end }} + {{ template "consul.extraconfig" }} exec /usr/local/bin/docker-entrypoint.sh consul agent \ @@ -207,7 +265,11 @@ spec: {{- end }} -hcl='leave_on_terminate = true' \ {{- if .Values.global.tls.enabled }} + {{- if .Values.global.secretsBackend.vault.enabled }} + -hcl='ca_file = "/vault/secrets/serverca.crt"' \ + {{- else }} -hcl='ca_file = "/consul/tls/ca/tls.crt"' \ + {{- end }} {{- if .Values.global.tls.enableAutoEncrypt }} -hcl='auto_encrypt = {tls = true}' \ -hcl="auto_encrypt = {ip_san = [\"$HOST_IP\",\"$POD_IP\"]}" \ @@ -233,6 +295,9 @@ spec: {{- if (and .Values.global.metrics.enabled .Values.global.metrics.enableAgentMetrics) }} -hcl='telemetry { prometheus_retention_time = "{{ .Values.global.metrics.agentMetricsRetentionTime }}" }' \ {{- end }} + {{- if .Values.global.adminPartitions.enabled }} + -hcl='partition = "{{ .Values.global.adminPartitions.name }}"' \ + {{- end }} -config-dir=/consul/config \ {{- if .Values.global.acls.manageSystemACLs }} -config-dir=/consul/aclconfig \ @@ -247,7 +312,7 @@ spec: {{- end }} -datacenter={{ .Values.global.datacenter }} \ -data-dir=/consul/data \ - {{- if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} + {{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }} -encrypt="${GOSSIP_KEY}" \ {{- end }} {{- if .Values.client.join }} @@ -265,6 +330,9 @@ spec: {{- range $value := .Values.global.recursors }} -recursor={{ quote $value }} \ {{- end }} + {{- if (and .Values.dns.enabled .Values.dns.enableRedirection) }} + $recursor_flags \ + {{- end }} -config-file=/consul/extra-config/extra-from-values.json \ -domain={{ .Values.global.domain }} volumeMounts: @@ -273,6 +341,7 @@ spec: - name: config mountPath: /consul/config {{- if .Values.global.tls.enabled }} + {{- if not .Values.global.secretsBackend.vault.enabled }} - name: consul-ca-cert mountPath: /consul/tls/ca readOnly: true @@ -282,6 +351,7 @@ spec: readOnly: true {{- end }} {{- end }} + {{- end }} {{- range .Values.client.extraVolumes }} - name: userconfig-{{ .name }} readOnly: true @@ -291,7 +361,7 @@ spec: - name: aclconfig mountPath: /consul/aclconfig {{- else }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-license mountPath: /consul/license readOnly: true @@ -357,6 +427,9 @@ spec: securityContext: {{- toYaml .Values.client.containerSecurityContext.client | nindent 12 }} {{- end }} + {{- if .Values.client.extraContainers }} + {{ toYaml .Values.client.extraContainers | nindent 8 }} + {{- end }} {{- if (or .Values.global.acls.manageSystemACLs (and .Values.global.tls.enabled (not .Values.global.tls.enableAutoEncrypt))) }} initContainers: {{- if .Values.global.acls.manageSystemACLs }} @@ -412,6 +485,7 @@ spec: mv {{ .Values.global.datacenter }}-client-{{ .Values.global.domain }}-0.pem tls.crt mv {{ .Values.global.datacenter }}-client-{{ .Values.global.domain }}-0-key.pem tls.key volumeMounts: + {{- if not .Values.global.secretsBackend.vault.enabled }} - name: consul-client-cert mountPath: /consul/tls/client - name: consul-ca-cert @@ -420,6 +494,7 @@ spec: - name: consul-ca-key mountPath: /consul/tls/ca/key readOnly: true + {{- end }} resources: requests: memory: "50Mi" diff --git a/charts/consul/templates/client-podsecuritypolicy.yaml b/charts/consul/templates/client-podsecuritypolicy.yaml index 81ecd8d640..0121bdf586 100644 --- a/charts/consul/templates/client-podsecuritypolicy.yaml +++ b/charts/consul/templates/client-podsecuritypolicy.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client spec: privileged: false # Required to prevent escalations to root. @@ -48,10 +49,14 @@ spec: - min: 8502 max: 8502 {{- end }} - {{- if .Values.client.exposeGossipPorts }} + {{- if (or .Values.client.exposeGossipPorts .Values.client.hostNetwork) }} - min: 8301 max: 8301 {{- end }} + {{- if .Values.client.hostNetwork }} + - min: 8600 + max: 8600 + {{- end }} hostIPC: false hostPID: false runAsUser: diff --git a/charts/consul/templates/client-role.yaml b/charts/consul/templates/client-role.yaml index 8295a5d1f8..7f05b82e6b 100644 --- a/charts/consul/templates/client-role.yaml +++ b/charts/consul/templates/client-role.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client {{- if (or .Values.global.acls.manageSystemACLs .Values.global.enablePodSecurityPolicies .Values.global.openshift.enabled) }} rules: {{- if .Values.global.enablePodSecurityPolicies }} diff --git a/charts/consul/templates/client-rolebinding.yaml b/charts/consul/templates/client-rolebinding.yaml index 25681c6e14..b034c70e55 100644 --- a/charts/consul/templates/client-rolebinding.yaml +++ b/charts/consul/templates/client-rolebinding.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client roleRef: apiGroup: rbac.authorization.k8s.io kind: Role diff --git a/charts/consul/templates/client-securitycontextconstraints.yaml b/charts/consul/templates/client-securitycontextconstraints.yaml index 0328abbd9c..07e7711384 100644 --- a/charts/consul/templates/client-securitycontextconstraints.yaml +++ b/charts/consul/templates/client-securitycontextconstraints.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client annotations: kubernetes.io/description: {{ template "consul.fullname" . }}-client are the security context constraints required to run the consul client. @@ -52,4 +53,4 @@ volumes: {{- if .Values.client.dataDirectoryHostPath }} - hostPath {{- end }} -{{- end}} \ No newline at end of file +{{- end}} diff --git a/charts/consul/templates/client-serviceaccount.yaml b/charts/consul/templates/client-serviceaccount.yaml index 650f1e1333..addd757b84 100644 --- a/charts/consul/templates/client-serviceaccount.yaml +++ b/charts/consul/templates/client-serviceaccount.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client {{- if .Values.client.serviceAccount.annotations }} annotations: {{ tpl .Values.client.serviceAccount.annotations . | nindent 4 | trim }} diff --git a/charts/consul/templates/client-snapshot-agent-deployment.yaml b/charts/consul/templates/client-snapshot-agent-deployment.yaml index 864c2125d1..7b7e953c98 100644 --- a/charts/consul/templates/client-snapshot-agent-deployment.yaml +++ b/charts/consul/templates/client-snapshot-agent-deployment.yaml @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client-snapshot-agent spec: replicas: {{ .Values.client.snapshotAgent.replicas }} selector: @@ -27,6 +28,28 @@ spec: component: client-snapshot-agent annotations: "consul.hashicorp.com/connect-inject": "false" + {{- if .Values.global.secretsBackend.vault.enabled }} + {{- if .Values.global.tls.enabled }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- end }} + {{- if .Values.global.enterpriseLicense.secretName }} + {{- with .Values.global.enterpriseLicense }} + "vault.hashicorp.com/agent-inject-secret-enterpriselicense.txt": "{{ .secretName }}" + "vault.hashicorp.com/agent-inject-template-enterpriselicense.txt": {{ template "consul.vaultSecretTemplate" . }} + {{- end }} + {{- end }} + {{- end }} spec: {{- if .Values.client.tolerations }} tolerations: @@ -37,7 +60,7 @@ spec: {{- if .Values.client.priorityClassName }} priorityClassName: {{ .Values.client.priorityClassName | quote }} {{- end }} - {{- if (or .Values.global.acls.manageSystemACLs .Values.global.tls.enabled (and .Values.client.snapshotAgent.configSecret.secretName .Values.client.snapshotAgent.configSecret.secretKey) (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload)) }} + {{- if (or .Values.global.acls.manageSystemACLs .Values.global.tls.enabled (and .Values.client.snapshotAgent.configSecret.secretName .Values.client.snapshotAgent.configSecret.secretKey) (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload)) }} volumes: {{- if (and .Values.client.snapshotAgent.configSecret.secretName .Values.client.snapshotAgent.configSecret.secretKey) }} - name: snapshot-config @@ -51,10 +74,10 @@ spec: - name: aclconfig emptyDir: {} {{- else }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-license secret: - secretName: {{ .Values.server.enterpriseLicense.secretName }} + secretName: {{ .Values.global.enterpriseLicense.secretName }} {{- end }} {{- end }} {{- if .Values.global.tls.enabled }} @@ -101,9 +124,13 @@ spec: name: "{{ template "consul.fullname" . }}-client-snapshot-agent-acl-token" key: "token" {{- else }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload) }} - name: CONSUL_LICENSE_PATH - value: /consul/license/{{ .Values.server.enterpriseLicense.secretKey }} + {{- if .Values.global.secretsBackend.vault.enabled }} + value: /vault/secrets/enterpriselicense.txt + {{- else }} + value: /consul/license/{{ .Values.global.enterpriseLicense.secretKey }} + {{- end }} {{- end }} {{- end}} command: @@ -122,7 +149,7 @@ spec: {{- if .Values.global.acls.manageSystemACLs }} -config-dir=/consul/aclconfig \ {{- end }} - {{- if (or .Values.global.acls.manageSystemACLs .Values.global.tls.enabled (and .Values.client.snapshotAgent.configSecret.secretName .Values.client.snapshotAgent.configSecret.secretKey) (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload)) }} + {{- if (or .Values.global.acls.manageSystemACLs .Values.global.tls.enabled (and .Values.client.snapshotAgent.configSecret.secretName .Values.client.snapshotAgent.configSecret.secretKey) (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload)) }} volumeMounts: {{- if (and .Values.client.snapshotAgent.configSecret.secretName .Values.client.snapshotAgent.configSecret.secretKey) }} - name: snapshot-config @@ -133,7 +160,7 @@ spec: - name: aclconfig mountPath: /consul/aclconfig {{- else }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey .Values.global.enterpriseLicense.enableLicenseAutoload (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-license mountPath: /consul/license readOnly: true diff --git a/charts/consul/templates/client-snapshot-agent-podsecuritypolicy.yaml b/charts/consul/templates/client-snapshot-agent-podsecuritypolicy.yaml index 7c80b5054b..dd324a3971 100644 --- a/charts/consul/templates/client-snapshot-agent-podsecuritypolicy.yaml +++ b/charts/consul/templates/client-snapshot-agent-podsecuritypolicy.yaml @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client-snapshot-agent spec: privileged: false # Required to prevent escalations to root. diff --git a/charts/consul/templates/client-snapshot-agent-role.yaml b/charts/consul/templates/client-snapshot-agent-role.yaml index 07781430c7..6691750487 100644 --- a/charts/consul/templates/client-snapshot-agent-role.yaml +++ b/charts/consul/templates/client-snapshot-agent-role.yaml @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client-snapshot-agent {{- if (or .Values.global.acls.manageSystemACLs .Values.global.enablePodSecurityPolicies) }} rules: {{- if .Values.global.enablePodSecurityPolicies }} diff --git a/charts/consul/templates/client-snapshot-agent-rolebinding.yaml b/charts/consul/templates/client-snapshot-agent-rolebinding.yaml index bc5d9e72b2..e966c4e2a8 100644 --- a/charts/consul/templates/client-snapshot-agent-rolebinding.yaml +++ b/charts/consul/templates/client-snapshot-agent-rolebinding.yaml @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client-snapshot-agent roleRef: apiGroup: rbac.authorization.k8s.io kind: Role diff --git a/charts/consul/templates/client-snapshot-agent-serviceaccount.yaml b/charts/consul/templates/client-snapshot-agent-serviceaccount.yaml index 2a0ad36217..a485ff0a5c 100644 --- a/charts/consul/templates/client-snapshot-agent-serviceaccount.yaml +++ b/charts/consul/templates/client-snapshot-agent-serviceaccount.yaml @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: client-snapshot-agent {{- if .Values.client.snapshotAgent.serviceAccount.annotations }} annotations: {{ tpl .Values.client.snapshotAgent.serviceAccount.annotations . | nindent 4 | trim }} diff --git a/charts/consul/templates/connect-inject-authmethod-clusterrole.yaml b/charts/consul/templates/connect-inject-authmethod-clusterrole.yaml deleted file mode 100644 index c70f51d7e7..0000000000 --- a/charts/consul/templates/connect-inject-authmethod-clusterrole.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if or (and (ne (.Values.connectInject.enabled | toString) "-") .Values.connectInject.enabled) (and (eq (.Values.connectInject.enabled | toString) "-") .Values.global.enabled) }} -{{- if .Values.global.acls.manageSystemACLs }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ template "consul.fullname" . }}-connect-injector-authmethod-role - labels: - app: {{ template "consul.name" . }} - chart: {{ template "consul.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} -rules: - - apiGroups: [""] - resources: - - serviceaccounts - verbs: - - get -{{- end }} -{{- end }} diff --git a/charts/consul/templates/connect-inject-authmethod-clusterrolebinding.yaml b/charts/consul/templates/connect-inject-authmethod-clusterrolebinding.yaml index bb9e1a0d7e..4f9d7c8083 100644 --- a/charts/consul/templates/connect-inject-authmethod-clusterrolebinding.yaml +++ b/charts/consul/templates/connect-inject-authmethod-clusterrolebinding.yaml @@ -3,37 +3,20 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ template "consul.fullname" . }}-connect-injector-authmethod-authdelegator-role-binding + name: {{ template "consul.fullname" . }}-connect-injector-authdelegator labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: "system:auth-delegator" subjects: - kind: ServiceAccount - name: {{ template "consul.fullname" . }}-connect-injector-authmethod-svc-account - namespace: {{ .Release.Namespace }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ template "consul.fullname" . }}-connect-injector-authmethod-serviceaccount-role-binding - labels: - app: {{ template "consul.name" . }} - chart: {{ template "consul.chart" . }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ template "consul.fullname" . }}-connect-injector-authmethod-role -subjects: - - kind: ServiceAccount - name: {{ template "consul.fullname" . }}-connect-injector-authmethod-svc-account + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} {{- end }} {{- end }} diff --git a/charts/consul/templates/connect-inject-clusterrole.yaml b/charts/consul/templates/connect-inject-clusterrole.yaml index 7ba2942aa8..9d01420363 100644 --- a/charts/consul/templates/connect-inject-clusterrole.yaml +++ b/charts/consul/templates/connect-inject-clusterrole.yaml @@ -3,13 +3,21 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ template "consul.fullname" . }}-connect-injector-webhook + name: {{ template "consul.fullname" . }}-connect-injector labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector rules: +{{- if .Values.global.acls.manageSystemACLs }} +- apiGroups: [""] + resources: + - serviceaccounts + verbs: + - get +{{- end }} - apiGroups: [""] resources: ["pods", "endpoints", "services", "namespaces"] verbs: @@ -29,7 +37,7 @@ rules: - apiGroups: ["policy"] resources: ["podsecuritypolicies"] resourceNames: - - {{ template "consul.fullname" . }}-connect-injector-webhook + - {{ template "consul.fullname" . }}-connect-injector verbs: - use {{- end }} @@ -41,5 +49,10 @@ rules: - {{ template "consul.fullname" . }}-connect-inject-acl-token verbs: - get +- apiGroups: [""] + resources: + - serviceaccounts + verbs: + - get {{- end }} {{- end }} diff --git a/charts/consul/templates/connect-inject-clusterrolebinding.yaml b/charts/consul/templates/connect-inject-clusterrolebinding.yaml index 87ab8bc9df..64bff8269f 100644 --- a/charts/consul/templates/connect-inject-clusterrolebinding.yaml +++ b/charts/consul/templates/connect-inject-clusterrolebinding.yaml @@ -2,18 +2,19 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ template "consul.fullname" . }}-connect-injector-webhook-admin-role-binding + name: {{ template "consul.fullname" . }}-connect-injector labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ template "consul.fullname" . }}-connect-injector-webhook + name: {{ template "consul.fullname" . }}-connect-injector subjects: - kind: ServiceAccount - name: {{ template "consul.fullname" . }}-connect-injector-webhook-svc-account + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} {{- end }} diff --git a/charts/consul/templates/connect-inject-deployment.yaml b/charts/consul/templates/connect-inject-deployment.yaml index 8fa83948b4..cd5ad9ddd2 100644 --- a/charts/consul/templates/connect-inject-deployment.yaml +++ b/charts/consul/templates/connect-inject-deployment.yaml @@ -2,22 +2,25 @@ {{- if not (or (and (ne (.Values.client.enabled | toString) "-") .Values.client.enabled) (and (eq (.Values.client.enabled | toString) "-") .Values.global.enabled)) }}{{ fail "clients must be enabled for connect injection" }}{{ end }} {{- if not .Values.client.grpc }}{{ fail "client.grpc must be true for connect injection" }}{{ end }} {{- if and .Values.connectInject.consulNamespaces.mirroringK8S (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if mirroringK8S=true" }}{{ end }} +{{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} {{- if .Values.connectInject.centralConfig }}{{- if eq (toString .Values.connectInject.centralConfig.enabled) "false" }}{{ fail "connectInject.centralConfig.enabled cannot be set to false; to disable, set enable_central_service_config to false in server.extraConfig and client.extraConfig" }}{{ end -}}{{ end -}} {{- if .Values.connectInject.centralConfig }}{{- if .Values.connectInject.centralConfig.defaultProtocol }}{{ fail "connectInject.centralConfig.defaultProtocol is no longer supported; instead you must migrate to CRDs (see www.consul.io/docs/k8s/crds/upgrade-to-crds)" }}{{ end }}{{ end -}} {{- if .Values.connectInject.centralConfig }}{{ if .Values.connectInject.centralConfig.proxyDefaults }}{{- if ne (trim .Values.connectInject.centralConfig.proxyDefaults) `{}` }}{{ fail "connectInject.centralConfig.proxyDefaults is no longer supported; instead you must migrate to CRDs (see www.consul.io/docs/k8s/crds/upgrade-to-crds)" }}{{ end }}{{ end }}{{ end -}} {{- if .Values.connectInject.imageEnvoy }}{{ fail "connectInject.imageEnvoy must be specified in global.imageEnvoy" }}{{ end }} {{- if .Values.global.lifecycleSidecarContainer }}{{ fail "global.lifecycleSidecarContainer has been renamed to global.consulSidecarContainer. Please set values using global.consulSidecarContainer." }}{{ end }} +{{- template "consul.reservedNamesFailer" (list .Values.connectInject.consulNamespaces.consulDestinationNamespace "connectInject.consulNamespaces.consulDestinationNamespace") }} # The deployment for running the Connect sidecar injector apiVersion: apps/v1 kind: Deployment metadata: - name: {{ template "consul.fullname" . }}-connect-injector-webhook-deployment + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector spec: replicas: {{ .Values.connectInject.replicas }} selector: @@ -35,8 +38,22 @@ spec: component: connect-injector annotations: "consul.hashicorp.com/connect-inject": "false" + {{- if (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- end }} spec: - serviceAccountName: {{ template "consul.fullname" . }}-connect-injector-webhook-svc-account + serviceAccountName: {{ template "consul.fullname" . }}-connect-injector containers: - name: sidecar-injector image: "{{ default .Values.global.imageK8S .Values.connectInject.image }}" @@ -80,8 +97,6 @@ spec: - "/bin/sh" - "-ec" - | - CONSUL_FULLNAME="{{template "consul.fullname" . }}" - consul-k8s-control-plane inject-connect \ -log-level={{ default .Values.global.logLevel .Values.connectInject.logLevel }} \ -log-json={{ .Values.global.logJSON }} \ @@ -105,6 +120,10 @@ spec: {{- else }} -transparent-proxy-default-overwrite-probes=false \ {{- end }} + -resource-prefix={{ template "consul.fullname" . }} \ + {{- if (and .Values.dns.enabled .Values.dns.enableRedirection) }} + -enable-consul-dns=true \ + {{- end }} {{- if .Values.global.openshift.enabled }} -enable-openshift \ {{- end }} @@ -131,6 +150,10 @@ spec: {{- range $value := .Values.connectInject.k8sDenyNamespaces }} -deny-k8s-namespace="{{ $value }}" \ {{- end }} + {{- if .Values.global.adminPartitions.enabled }} + -enable-partitions=true \ + -partition={{ .Values.global.adminPartitions.name }} \ + {{- end }} {{- if .Values.global.enableConsulNamespaces }} -enable-namespaces=true \ {{- if .Values.connectInject.consulNamespaces.consulDestinationNamespace }} @@ -181,18 +204,26 @@ spec: {{- if .Values.global.consulSidecarContainer }} {{- $consulSidecarResources := .Values.global.consulSidecarContainer.resources }} {{- if not (kindIs "invalid" $consulSidecarResources.limits.memory) }} - -consul-sidecar-memory-limit={{ $consulSidecarResources.limits.memory }} \ + -default-consul-sidecar-memory-limit={{ $consulSidecarResources.limits.memory }} \ {{- end }} {{- if not (kindIs "invalid" $consulSidecarResources.requests.memory) }} - -consul-sidecar-memory-request={{ $consulSidecarResources.requests.memory }} \ + -default-consul-sidecar-memory-request={{ $consulSidecarResources.requests.memory }} \ {{- end }} {{- if not (kindIs "invalid" $consulSidecarResources.limits.cpu) }} - -consul-sidecar-cpu-limit={{ $consulSidecarResources.limits.cpu }} \ + -default-consul-sidecar-cpu-limit={{ $consulSidecarResources.limits.cpu }} \ {{- end }} {{- if not (kindIs "invalid" $consulSidecarResources.requests.cpu) }} - -consul-sidecar-cpu-request={{ $consulSidecarResources.requests.cpu }} \ + -default-consul-sidecar-cpu-request={{ $consulSidecarResources.requests.cpu }} \ {{- end }} {{- end }} + startupProbe: + httpGet: + path: /readyz/ready + port: 9445 + scheme: HTTP + failureThreshold: 15 + periodSeconds: 2 + timeoutSeconds: 5 livenessProbe: httpGet: path: /readyz/ready @@ -200,7 +231,6 @@ spec: scheme: HTTP failureThreshold: 2 initialDelaySeconds: 1 - periodSeconds: 2 successThreshold: 1 timeoutSeconds: 5 readinessProbe: @@ -210,7 +240,6 @@ spec: scheme: HTTP failureThreshold: 2 initialDelaySeconds: 2 - periodSeconds: 2 successThreshold: 1 timeoutSeconds: 5 volumeMounts: diff --git a/charts/consul/templates/connect-inject-leader-election-role.yaml b/charts/consul/templates/connect-inject-leader-election-role.yaml index 334dc6bfd9..703aaffaac 100644 --- a/charts/consul/templates/connect-inject-leader-election-role.yaml +++ b/charts/consul/templates/connect-inject-leader-election-role.yaml @@ -9,7 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} - component: controller + component: connect-injector rules: - apiGroups: - "" diff --git a/charts/consul/templates/connect-inject-leader-election-rolebinding.yaml b/charts/consul/templates/connect-inject-leader-election-rolebinding.yaml index be3c351876..9a27d3c868 100644 --- a/charts/consul/templates/connect-inject-leader-election-rolebinding.yaml +++ b/charts/consul/templates/connect-inject-leader-election-rolebinding.yaml @@ -9,13 +9,13 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} - component: controller + component: connect-injector roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ template "consul.fullname" . }}-connect-inject-leader-election subjects: - kind: ServiceAccount - name: {{ template "consul.fullname" . }}-connect-injector-webhook-svc-account + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} {{- end }} diff --git a/charts/consul/templates/connect-inject-mutatingwebhook.yaml b/charts/consul/templates/connect-inject-mutatingwebhookconfiguration.yaml similarity index 93% rename from charts/consul/templates/connect-inject-mutatingwebhook.yaml rename to charts/consul/templates/connect-inject-mutatingwebhookconfiguration.yaml index 0ce6f80dd9..c3164bab24 100644 --- a/charts/consul/templates/connect-inject-mutatingwebhook.yaml +++ b/charts/consul/templates/connect-inject-mutatingwebhookconfiguration.yaml @@ -3,13 +3,14 @@ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: - name: {{ template "consul.fullname" . }}-connect-injector-cfg + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector webhooks: - name: {{ template "consul.fullname" . }}-connect-injector.consul.hashicorp.com # The webhook will fail scheduling all pods that are not part of consul if all replicas of the webhook are unhealthy. @@ -25,7 +26,7 @@ webhooks: - "v1" clientConfig: service: - name: {{ template "consul.fullname" . }}-connect-injector-svc + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} path: "/mutate" rules: diff --git a/charts/consul/templates/connect-inject-podsecuritypolicy.yaml b/charts/consul/templates/connect-inject-podsecuritypolicy.yaml index b4c10361a6..0fafef7c40 100644 --- a/charts/consul/templates/connect-inject-podsecuritypolicy.yaml +++ b/charts/consul/templates/connect-inject-podsecuritypolicy.yaml @@ -2,13 +2,14 @@ apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: - name: {{ template "consul.fullname" . }}-connect-injector-webhook + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector spec: privileged: false # Required to prevent escalations to root. diff --git a/charts/consul/templates/connect-inject-service.yaml b/charts/consul/templates/connect-inject-service.yaml index 8004d8277d..b0284af74d 100644 --- a/charts/consul/templates/connect-inject-service.yaml +++ b/charts/consul/templates/connect-inject-service.yaml @@ -3,13 +3,14 @@ apiVersion: v1 kind: Service metadata: - name: {{ template "consul.fullname" . }}-connect-injector-svc + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector spec: ports: - port: 443 diff --git a/charts/consul/templates/connect-inject-serviceaccount.yaml b/charts/consul/templates/connect-inject-serviceaccount.yaml index ad14895527..250b23d6c3 100644 --- a/charts/consul/templates/connect-inject-serviceaccount.yaml +++ b/charts/consul/templates/connect-inject-serviceaccount.yaml @@ -2,13 +2,14 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ template "consul.fullname" . }}-connect-injector-webhook-svc-account + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: connect-injector {{- if .Values.connectInject.serviceAccount.annotations }} annotations: {{ tpl .Values.connectInject.serviceAccount.annotations . | nindent 4 | trim }} diff --git a/charts/consul/templates/controller-clusterrole.yaml b/charts/consul/templates/controller-clusterrole.yaml index b113366cd2..45fa8d8458 100644 --- a/charts/consul/templates/controller-clusterrole.yaml +++ b/charts/consul/templates/controller-clusterrole.yaml @@ -17,6 +17,7 @@ rules: - serviceresolvers - proxydefaults - meshes + - exportedservices - servicerouters - servicesplitters - serviceintentions @@ -37,6 +38,7 @@ rules: - serviceresolvers/status - proxydefaults/status - meshes/status + - exportedservices/status - servicerouters/status - servicesplitters/status - serviceintentions/status diff --git a/charts/consul/templates/controller-deployment.yaml b/charts/consul/templates/controller-deployment.yaml index 5cee3b4cde..e5ed0d74f5 100644 --- a/charts/consul/templates/controller-deployment.yaml +++ b/charts/consul/templates/controller-deployment.yaml @@ -1,4 +1,5 @@ {{- if .Values.controller.enabled }} +{{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -29,6 +30,20 @@ spec: component: controller annotations: "consul.hashicorp.com/connect-inject": "false" + {{- if (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- end }} spec: {{- if or .Values.global.acls.manageSystemACLs (and .Values.global.tls.enabled .Values.global.tls.enableAutoEncrypt) }} initContainers: @@ -64,6 +79,9 @@ spec: -log-json={{ .Values.global.logJSON }} \ -webhook-tls-cert-dir=/tmp/controller-webhook/certs \ -datacenter={{ .Values.global.datacenter }} \ + {{- if .Values.global.adminPartitions.enabled }} + -partition={{ .Values.global.adminPartitions.name }} \ + {{- end }} -enable-leader-election \ {{- if .Values.global.enableConsulNamespaces }} -enable-namespaces=true \ diff --git a/charts/consul/templates/controller-mutatingwebhookconfiguration.yaml b/charts/consul/templates/controller-mutatingwebhookconfiguration.yaml index e2f33f79e2..bf31ea862f 100644 --- a/charts/consul/templates/controller-mutatingwebhookconfiguration.yaml +++ b/charts/consul/templates/controller-mutatingwebhookconfiguration.yaml @@ -2,7 +2,7 @@ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: - name: {{ template "consul.fullname" . }}-controller-mutating-webhook-configuration + name: {{ template "consul.fullname" . }}-controller namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} @@ -12,7 +12,6 @@ metadata: component: controller webhooks: - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -34,7 +33,6 @@ webhooks: - proxydefaults sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -56,7 +54,6 @@ webhooks: - meshes sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -78,7 +75,6 @@ webhooks: - servicedefaults sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -100,7 +96,6 @@ webhooks: - serviceresolvers sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -122,7 +117,6 @@ webhooks: - servicerouters sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -144,7 +138,6 @@ webhooks: - servicesplitters sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -166,7 +159,6 @@ webhooks: - serviceintentions sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -188,7 +180,6 @@ webhooks: - ingressgateways sideEffects: None - clientConfig: - caBundle: Cg== service: name: {{ template "consul.fullname" . }}-controller-webhook namespace: {{ .Release.Namespace }} @@ -209,4 +200,25 @@ webhooks: resources: - terminatinggateways sideEffects: None +- clientConfig: + service: + name: {{ template "consul.fullname" . }}-controller-webhook + namespace: {{ .Release.Namespace }} + path: /mutate-v1alpha1-exportedservices + failurePolicy: Fail + admissionReviewVersions: + - "v1beta1" + - "v1" + name: mutate-exportedservices.consul.hashicorp.com + rules: + - apiGroups: + - consul.hashicorp.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - exportedservices + sideEffects: None {{- end }} diff --git a/charts/consul/templates/crd-exportedservices.yaml b/charts/consul/templates/crd-exportedservices.yaml new file mode 100644 index 0000000000..9ddb7d3053 --- /dev/null +++ b/charts/consul/templates/crd-exportedservices.yaml @@ -0,0 +1,139 @@ +{{- if .Values.controller.enabled }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.0 + creationTimestamp: null + name: exportedservices.consul.hashicorp.com + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: crd +spec: + group: consul.hashicorp.com + names: + kind: ExportedServices + listKind: ExportedServicesList + plural: exportedservices + shortNames: + - exported-services + singular: exportedservices + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The sync status of the resource with Consul + jsonPath: .status.conditions[?(@.type=="Synced")].status + name: Synced + type: string + - description: The last successful synced time of the resource with Consul + jsonPath: .status.lastSyncedTime + name: Last Synced + type: date + - description: The age of the resource + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExportedServices is the Schema for the exportedservices API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ExportedServicesSpec defines the desired state of ExportedServices + properties: + services: + description: Services is a list of services to be exported and the + list of partitions to expose them to. + items: + description: ExportedService manages the exporting of a service + in the local partition to other partitions. + properties: + consumers: + description: Consumers is a list of downstream consumers of + the service to be exported. + items: + description: ServiceConsumer represents a downstream consumer + of the service to be exported. + properties: + partition: + description: Partition is the admin partition to export + the service to. + type: string + type: object + type: array + name: + description: Name is the name of the service to be exported. + type: string + namespace: + description: Namespace is the namespace to export the service + from. + type: string + type: object + type: array + type: object + status: + properties: + conditions: + description: Conditions indicate the latest available observations + of a resource's current state. + items: + description: 'Conditions define a readiness condition for a Consul + resource. See: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties' + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition. + type: string + required: + - status + - type + type: object + type: array + lastSyncedTime: + description: LastSyncedTime is the last time the resource successfully + synced with Consul. + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +{{- end }} diff --git a/charts/consul/templates/crd-ingressgateways.yaml b/charts/consul/templates/crd-ingressgateways.yaml index eed9f88fc9..4ff360456e 100644 --- a/charts/consul/templates/crd-ingressgateways.yaml +++ b/charts/consul/templates/crd-ingressgateways.yaml @@ -19,6 +19,8 @@ spec: kind: IngressGateway listKind: IngressGatewayList plural: ingressgateways + shortNames: + - ingress-gateway singular: ingressgateway scope: Namespaced versions: @@ -62,6 +64,25 @@ spec: description: IngressListener manages the configuration for a listener on a specific port. properties: + tls: + description: TLS config for this listener. + properties: + enabled: + description: Indicates that TLS should be enabled for this + gateway service. + type: boolean + sds: + description: SDS allows configuring TLS certificate from + an SDS service. + properties: + certResource: + type: string + clusterName: + type: string + type: object + required: + - enabled + type: object port: description: Port declares the port on which the ingress gateway should listen for traffic. @@ -82,6 +103,20 @@ spec: description: IngressService manages configuration for services that are exposed to ingress traffic. properties: + tls: + description: TLS allows specifying some TLS configuration + per listener. + properties: + sds: + description: SDS allows configuring TLS certificate + from an SDS service. + properties: + certResource: + type: string + clusterName: + type: string + type: object + type: object hosts: description: "Hosts is a list of hostnames which should be associated to this service on the defined listener. @@ -109,6 +144,63 @@ spec: description: Namespace is the namespace where the service is located. Namespacing is a Consul Enterprise feature. type: string + partition: + description: Partition is the admin-partition where the + service is located. Partitioning is a Consul Enterprise + feature. + type: string + requestHeaders: + description: Allow HTTP header manipulation to be configured. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object + responseHeaders: + description: HTTPHeaderModifiers is a set of rules for + HTTP header modification that should be performed by + proxies as the request passes through them. It can operate + on either request or response headers depending on the + context in which it is used. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object type: object type: array type: object @@ -120,6 +212,15 @@ spec: description: Indicates that TLS should be enabled for this gateway service. type: boolean + sds: + description: SDS allows configuring TLS certificate from an SDS + service. + properties: + certResource: + type: string + clusterName: + type: string + type: object required: - enabled type: object diff --git a/charts/consul/templates/crd-proxydefaults.yaml b/charts/consul/templates/crd-proxydefaults.yaml index d60160e260..6e21c89480 100644 --- a/charts/consul/templates/crd-proxydefaults.yaml +++ b/charts/consul/templates/crd-proxydefaults.yaml @@ -19,6 +19,8 @@ spec: kind: ProxyDefaults listKind: ProxyDefaultsList plural: proxydefaults + shortNames: + - proxy-defaults singular: proxydefaults scope: Namespaced versions: diff --git a/charts/consul/templates/crd-servicedefaults.yaml b/charts/consul/templates/crd-servicedefaults.yaml index 5f6443be43..ab356836d2 100644 --- a/charts/consul/templates/crd-servicedefaults.yaml +++ b/charts/consul/templates/crd-servicedefaults.yaml @@ -19,6 +19,8 @@ spec: kind: ServiceDefaults listKind: ServiceDefaultsList plural: servicedefaults + shortNames: + - service-defaults singular: servicedefaults scope: Namespaced versions: @@ -206,6 +208,10 @@ spec: description: Namespace is only accepted within a service-defaults config entry. type: string + partition: + description: Partition is only accepted within a service-defaults + config entry. + type: string passiveHealthCheck: description: PassiveHealthCheck configuration determines how upstream proxy instances will be monitored for removal from @@ -294,6 +300,10 @@ spec: description: Namespace is only accepted within a service-defaults config entry. type: string + partition: + description: Partition is only accepted within a service-defaults + config entry. + type: string passiveHealthCheck: description: PassiveHealthCheck configuration determines how upstream proxy instances will be monitored for removal diff --git a/charts/consul/templates/crd-serviceintentions.yaml b/charts/consul/templates/crd-serviceintentions.yaml index d3c8dbe45a..ffcd44ae5c 100644 --- a/charts/consul/templates/crd-serviceintentions.yaml +++ b/charts/consul/templates/crd-serviceintentions.yaml @@ -19,6 +19,8 @@ spec: kind: ServiceIntentions listKind: ServiceIntentionsList plural: serviceintentions + shortNames: + - service-intentions singular: serviceintentions scope: Namespaced versions: @@ -96,6 +98,9 @@ spec: namespace: description: Namespace is the namespace for the Name parameter. type: string + partition: + description: Partition is the Admin Partition for the Name parameter. + type: string permissions: description: Permissions is the list of all additional L7 attributes that extend the intention match criteria. Permission precedence diff --git a/charts/consul/templates/crd-serviceresolvers.yaml b/charts/consul/templates/crd-serviceresolvers.yaml index 998bc0e6e0..1023b9127f 100644 --- a/charts/consul/templates/crd-serviceresolvers.yaml +++ b/charts/consul/templates/crd-serviceresolvers.yaml @@ -19,6 +19,8 @@ spec: kind: ServiceResolver listKind: ServiceResolverList plural: serviceresolvers + shortNames: + - service-resolver singular: serviceresolver scope: Namespaced versions: @@ -186,8 +188,14 @@ spec: from instead of the current one. type: string namespace: - description: Namespace is the namespace to resolve the service - from instead of the current one. + description: Namespace is the Consul namespace to resolve the + service from instead of the current namespace. If empty the + current namespace is assumed. + type: string + partition: + description: Partition is the Consul partition to resolve the + service from instead of the current partition. If empty the + current partition is assumed. type: string service: description: Service is a service to resolve instead of the current diff --git a/charts/consul/templates/crd-servicerouters.yaml b/charts/consul/templates/crd-servicerouters.yaml index 01473e980d..15b15e7f6b 100644 --- a/charts/consul/templates/crd-servicerouters.yaml +++ b/charts/consul/templates/crd-servicerouters.yaml @@ -19,6 +19,8 @@ spec: kind: ServiceRouter listKind: ServiceRouterList plural: servicerouters + shortNames: + - service-router singular: servicerouter scope: Namespaced versions: @@ -76,17 +78,74 @@ spec: the request when a retryable result occurs format: int32 type: integer + partition: + description: Partition is the Consul partition to resolve + the service from instead of the current partition. If + empty the current partition is assumed. + type: string prefixRewrite: description: PrefixRewrite defines how to rewrite the HTTP request path before proxying it to its final destination. This requires that either match.http.pathPrefix or match.http.pathExact be configured on this route. type: string + requestHeaders: + description: Allow HTTP header manipulation to be configured. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object requestTimeout: description: RequestTimeout is the total amount of time permitted for the entire downstream request (and retries) to be processed. type: string + responseHeaders: + description: HTTPHeaderModifiers is a set of rules for HTTP + header modification that should be performed by proxies + as the request passes through them. It can operate on + either request or response headers depending on the context + in which it is used. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object retryOnConnectFailure: description: RetryOnConnectFailure allows for connection failure errors to trigger a retry. diff --git a/charts/consul/templates/crd-servicesplitters.yaml b/charts/consul/templates/crd-servicesplitters.yaml index 17da91b20f..05b5548ce9 100644 --- a/charts/consul/templates/crd-servicesplitters.yaml +++ b/charts/consul/templates/crd-servicesplitters.yaml @@ -19,6 +19,8 @@ spec: kind: ServiceSplitter listKind: ServiceSplitterList plural: servicesplitters + shortNames: + - service-splitter singular: servicesplitter scope: Namespaced versions: @@ -62,10 +64,67 @@ spec: items: properties: namespace: - description: The namespace to resolve the service from instead - of the current namespace. If empty the current namespace is - assumed. + description: Namespace is the Consul namespace to resolve the + service from instead of the current namespace. If empty the + current namespace is assumed. type: string + partition: + description: Partition is the Consul partition to resolve the + service from instead of the current partition. If empty the + current partition is assumed. + type: string + requestHeaders: + description: Allow HTTP header manipulation to be configured. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that should + be appended to the request or response (i.e. allowing + duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that should + be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that should + be added to the request or response, overwriting any existing + header values of the same name. + type: object + type: object + responseHeaders: + description: HTTPHeaderModifiers is a set of rules for HTTP + header modification that should be performed by proxies as + the request passes through them. It can operate on either + request or response headers depending on the context in which + it is used. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that should + be appended to the request or response (i.e. allowing + duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that should + be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that should + be added to the request or response, overwriting any existing + header values of the same name. + type: object + type: object service: description: Service is the service to resolve instead of the default. diff --git a/charts/consul/templates/crd-terminatinggateways.yaml b/charts/consul/templates/crd-terminatinggateways.yaml index 518f03fe70..3db27d2bce 100644 --- a/charts/consul/templates/crd-terminatinggateways.yaml +++ b/charts/consul/templates/crd-terminatinggateways.yaml @@ -19,6 +19,8 @@ spec: kind: TerminatingGateway listKind: TerminatingGatewayList plural: terminatinggateways + shortNames: + - terminating-gateway singular: terminatinggateway scope: Namespaced versions: diff --git a/charts/consul/templates/create-federation-secret-job.yaml b/charts/consul/templates/create-federation-secret-job.yaml index d00fde21cc..096135dd2a 100644 --- a/charts/consul/templates/create-federation-secret-job.yaml +++ b/charts/consul/templates/create-federation-secret-job.yaml @@ -68,6 +68,13 @@ spec: items: - key: {{ .Values.global.gossipEncryption.secretKey }} path: gossip.key + {{- else if .Values.global.gossipEncryption.autoGenerate }} + - name: gossip-encryption-key + secret: + secretName: consul-gossip-encryption-key + items: + - key: key + path: gossip.key {{- end }} {{- if .Values.global.tls.enableAutoEncrypt }} @@ -107,7 +114,7 @@ spec: mountPath: /consul/tls/client/ca readOnly: true {{- end }} - {{- if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} + {{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }} - name: gossip-encryption-key mountPath: /consul/gossip readOnly: true @@ -119,7 +126,7 @@ spec: consul-k8s-control-plane create-federation-secret \ -log-level={{ .Values.global.logLevel }} \ -log-json={{ .Values.global.logJSON }} \ - {{- if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} + {{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }} -gossip-key-file=/consul/gossip/gossip.key \ {{- end }} {{- if .Values.global.acls.createReplicationToken }} diff --git a/charts/consul/templates/create-federation-secret-podsecuritypolicy.yaml b/charts/consul/templates/create-federation-secret-podsecuritypolicy.yaml index ced8e42f00..8217311992 100644 --- a/charts/consul/templates/create-federation-secret-podsecuritypolicy.yaml +++ b/charts/consul/templates/create-federation-secret-podsecuritypolicy.yaml @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: create-federation-secret annotations: "helm.sh/hook": post-install,post-upgrade "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation diff --git a/charts/consul/templates/enterprise-license-job.yaml b/charts/consul/templates/enterprise-license-job.yaml index 2e0dbdfab0..08fda29484 100644 --- a/charts/consul/templates/enterprise-license-job.yaml +++ b/charts/consul/templates/enterprise-license-job.yaml @@ -1,9 +1,10 @@ +{{- if .Values.server.enterpriseLicense }}{{ fail "server.enterpriseLicense has been moved to global.enterpriseLicense" }}{{ end -}} {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} -{{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey (not .Values.server.enterpriseLicense.enableLicenseAutoload)) }} +{{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey (not .Values.global.enterpriseLicense.enableLicenseAutoload)) }} apiVersion: batch/v1 kind: Job metadata: - name: {{ template "consul.fullname" . }}-license + name: {{ template "consul.fullname" . }}-enterprise-license namespace: {{ .Release.Namespace }} labels: app.kubernetes.io/managed-by: {{.Release.Service | quote }} @@ -13,6 +14,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: license annotations: "helm.sh/hook": post-install,post-upgrade "helm.sh/hook-weight": "100" @@ -52,10 +54,14 @@ spec: image: "{{ default .Values.global.image .Values.server.image }}" env: - name: ENTERPRISE_LICENSE + {{- if .Values.global.secretsBackend.vault.enabled }} + value: /vault/secrets/enterpriselicense.txt + {{- else }} valueFrom: secretKeyRef: - name: {{ .Values.server.enterpriseLicense.secretName }} - key: {{ .Values.server.enterpriseLicense.secretKey }} + name: {{ .Values.global.enterpriseLicense.secretName }} + key: {{ .Values.global.enterpriseLicense.secretKey }} + {{- end }} - name: CONSUL_HTTP_ADDR {{- if .Values.global.tls.enabled }} value: https://{{ template "consul.fullname" . }}-server:8501 diff --git a/charts/consul/templates/enterprise-license-podsecuritypolicy.yaml b/charts/consul/templates/enterprise-license-podsecuritypolicy.yaml index 2972a184f3..cf96367473 100644 --- a/charts/consul/templates/enterprise-license-podsecuritypolicy.yaml +++ b/charts/consul/templates/enterprise-license-podsecuritypolicy.yaml @@ -1,5 +1,5 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} -{{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey (not .Values.server.enterpriseLicense.enableLicenseAutoload)) }} +{{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey (not .Values.global.enterpriseLicense.enableLicenseAutoload)) }} {{- if .Values.global.enablePodSecurityPolicies }} apiVersion: policy/v1beta1 kind: PodSecurityPolicy @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: license spec: privileged: false # Allow core volume types. diff --git a/charts/consul/templates/enterprise-license-role.yaml b/charts/consul/templates/enterprise-license-role.yaml index 9824834246..6a1b7fdffa 100644 --- a/charts/consul/templates/enterprise-license-role.yaml +++ b/charts/consul/templates/enterprise-license-role.yaml @@ -1,5 +1,5 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} -{{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey (not .Values.server.enterpriseLicense.enableLicenseAutoload)) }} +{{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey (not .Values.global.enterpriseLicense.enableLicenseAutoload)) }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: license {{- if or .Values.global.acls.manageSystemACLs .Values.global.enablePodSecurityPolicies }} rules: {{- if .Values.global.acls.manageSystemACLs }} diff --git a/charts/consul/templates/enterprise-license-rolebinding.yaml b/charts/consul/templates/enterprise-license-rolebinding.yaml index 8070aecde2..a21118b431 100644 --- a/charts/consul/templates/enterprise-license-rolebinding.yaml +++ b/charts/consul/templates/enterprise-license-rolebinding.yaml @@ -1,5 +1,5 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} -{{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey (not .Values.server.enterpriseLicense.enableLicenseAutoload)) }} +{{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey (not .Values.global.enterpriseLicense.enableLicenseAutoload)) }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: license roleRef: apiGroup: rbac.authorization.k8s.io kind: Role diff --git a/charts/consul/templates/enterprise-license-serviceaccount.yaml b/charts/consul/templates/enterprise-license-serviceaccount.yaml index 70b1c179bb..31c9da841e 100644 --- a/charts/consul/templates/enterprise-license-serviceaccount.yaml +++ b/charts/consul/templates/enterprise-license-serviceaccount.yaml @@ -1,5 +1,5 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} -{{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey (not .Values.server.enterpriseLicense.enableLicenseAutoload)) }} +{{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey (not .Values.global.enterpriseLicense.enableLicenseAutoload)) }} apiVersion: v1 kind: ServiceAccount metadata: @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: license {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- range . }} diff --git a/charts/consul/templates/gossip-encryption-autogenerate-job.yaml b/charts/consul/templates/gossip-encryption-autogenerate-job.yaml new file mode 100644 index 0000000000..e7c2b52209 --- /dev/null +++ b/charts/consul/templates/gossip-encryption-autogenerate-job.yaml @@ -0,0 +1,60 @@ +{{- if .Values.global.gossipEncryption.autoGenerate }} +{{- if (or .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} + {{ fail "If global.gossipEncryption.autoGenerate is true, global.gossipEncryption.secretName and global.gossipEncryption.secretKey must not be set." }} +{{ end }} +# automatically generate encryption key for gossip protocol and save it in Kubernetes secret +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: gossip-encryption-autogenerate + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "1" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +spec: + template: + metadata: + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + release: {{ .Release.Name }} + component: gossip-encryption-autogenerate + annotations: + "consul.hashicorp.com/connect-inject": "false" + spec: + restartPolicy: Never + serviceAccountName: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate + securityContext: + runAsNonRoot: true + runAsGroup: 1000 + runAsUser: 100 + fsGroup: 1000 + containers: + - name: gossip-encryption-autogen + image: "{{ .Values.global.imageK8S }}" + command: + - "/bin/sh" + - "-ec" + - | + consul-k8s-control-plane gossip-encryption-autogenerate \ + -namespace={{ .Release.Namespace }} \ + -secret-name={{ template "consul.fullname" . }}-gossip-encryption-key \ + -secret-key="key" \ + -log-level={{ .Values.global.logLevel }} \ + -log-json={{ .Values.global.logJSON }} + resources: + requests: + memory: "50Mi" + cpu: "50m" + limits: + memory: "50Mi" + cpu: "50m" +{{- end }} diff --git a/charts/consul/templates/gossip-encryption-autogenerate-podsecuritypolicy.yaml b/charts/consul/templates/gossip-encryption-autogenerate-podsecuritypolicy.yaml new file mode 100644 index 0000000000..707ebe57c9 --- /dev/null +++ b/charts/consul/templates/gossip-encryption-autogenerate-podsecuritypolicy.yaml @@ -0,0 +1,40 @@ +{{- if .Values.global.gossipEncryption.autoGenerate }} +--- +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: gossip-encryption-autogenerate + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +spec: + privileged: false + # Required to prevent escalations to root. + allowPrivilegeEscalation: false + # This is redundant with non-root + disallow privilege escalation, + # but we can provide it for defense in depth. + requiredDropCapabilities: + - ALL + # Allow core volume types. + volumes: + - 'secret' + hostNetwork: false + hostIPC: false + hostPID: false + runAsUser: + rule: 'RunAsAny' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'RunAsAny' + fsGroup: + rule: 'RunAsAny' + readOnlyRootFilesystem: false +{{- end }} diff --git a/charts/consul/templates/gossip-encryption-autogenerate-role.yaml b/charts/consul/templates/gossip-encryption-autogenerate-role.yaml new file mode 100644 index 0000000000..8c51c96ffe --- /dev/null +++ b/charts/consul/templates/gossip-encryption-autogenerate-role.yaml @@ -0,0 +1,32 @@ +{{- if .Values.global.gossipEncryption.autoGenerate }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: gossip-encryption-autogenerate + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +rules: +- apiGroups: [""] + resources: + - secrets + verbs: + - create + - get +{{- if .Values.global.enablePodSecurityPolicies }} +- apiGroups: ["policy"] + resources: + - podsecuritypolicies + verbs: + - use + resourceNames: + - {{ template "consul.fullname" . }}-gossip-encryption-autogenerate +{{- end }} +{{- end }} diff --git a/charts/consul/templates/gossip-encryption-autogenerate-rolebinding.yaml b/charts/consul/templates/gossip-encryption-autogenerate-rolebinding.yaml new file mode 100644 index 0000000000..7118475f64 --- /dev/null +++ b/charts/consul/templates/gossip-encryption-autogenerate-rolebinding.yaml @@ -0,0 +1,23 @@ +{{- if .Values.global.gossipEncryption.autoGenerate }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: gossip-encryption-autogenerate + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate +subjects: +- kind: ServiceAccount + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate +{{- end }} diff --git a/charts/consul/templates/gossip-encryption-autogenerate-serviceaccount.yaml b/charts/consul/templates/gossip-encryption-autogenerate-serviceaccount.yaml new file mode 100644 index 0000000000..1fd620237f --- /dev/null +++ b/charts/consul/templates/gossip-encryption-autogenerate-serviceaccount.yaml @@ -0,0 +1,22 @@ +{{- if .Values.global.gossipEncryption.autoGenerate }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "consul.fullname" . }}-gossip-encryption-autogenerate + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: gossip-encryption-autogenerate + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +{{- with .Values.global.imagePullSecrets }} +imagePullSecrets: +{{- range . }} + - name: {{ .name }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/consul/templates/ingress-gateways-deployment.yaml b/charts/consul/templates/ingress-gateways-deployment.yaml index 6d63d361af..d520bd9845 100644 --- a/charts/consul/templates/ingress-gateways-deployment.yaml +++ b/charts/consul/templates/ingress-gateways-deployment.yaml @@ -1,6 +1,7 @@ {{- if .Values.ingressGateways.enabled }} {{- if not .Values.connectInject.enabled }}{{ fail "connectInject.enabled must be true" }}{{ end -}} {{- if not .Values.client.grpc }}{{ fail "client.grpc must be true" }}{{ end -}} +{{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} {{- if not (or (and (ne (.Values.client.enabled | toString) "-") .Values.client.enabled) (and (eq (.Values.client.enabled | toString) "-") .Values.global.enabled)) }}{{ fail "clients must be enabled" }}{{ end -}} {{- if .Values.global.lifecycleSidecarContainer }}{{ fail "global.lifecycleSidecarContainer has been renamed to global.consulSidecarContainer. Please set values using global.consulSidecarContainer." }}{{ end }} @@ -54,6 +55,20 @@ spec: component: ingress-gateway ingress-gateway-name: {{ template "consul.fullname" $root }}-{{ .name }} annotations: + {{- if (and $root.Values.global.secretsBackend.vault.enabled $root.Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ $root.Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ $root.Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" $root }} + {{- if and $root.Values.global.secretsBackend.vault.ca.secretName $root.Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": {{ $root.Values.global.secretsBackend.vault.ca.secretName }} + "vault.hashicorp.com/ca-cert": /vault/custom/{{ $root.Values.global.secretsBackend.vault.ca.secretKey }} + {{- end }} + {{- if $root.Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl $root.Values.global.secretsBackend.vault.agentAnnotations $root | nindent 8 | trim }} + {{- end }} + {{- end }} "consul.hashicorp.com/connect-inject": "false" {{- if (and $root.Values.global.metrics.enabled $root.Values.global.metrics.enableGatewayMetrics) }} "prometheus.io/scrape": "true" @@ -76,7 +91,7 @@ spec: tolerations: {{ tpl (default $defaults.tolerations .tolerations) $root | nindent 8 | trim }} {{- end }} - terminationGracePeriodSeconds: 10 + terminationGracePeriodSeconds: {{ default $defaults.terminationGracePeriodSeconds .terminationGracePeriodSeconds }} serviceAccountName: {{ template "consul.fullname" $root }}-{{ .name }} volumes: - name: consul-bin @@ -217,6 +232,9 @@ spec: {{- if $root.Values.global.enableConsulNamespaces }} namespace = "{{ (default $defaults.consulNamespace .consulNamespace) }}" {{- end }} + {{- if $root.Values.global.adminPartitions.enabled }} + partition = "{{ $root.Values.global.adminPartitions.name }}" + {{- end }} port = ${WAN_PORT} address = "${WAN_ADDR}" tagged_addresses { @@ -340,6 +358,9 @@ spec: {{- if $root.Values.global.enableConsulNamespaces }} - -namespace={{ default $defaults.consulNamespace .consulNamespace }} {{- end }} + {{- if $root.Values.global.adminPartitions.enabled }} + - -partition={{ $root.Values.global.adminPartitions.name }} + {{- end }} livenessProbe: tcpSocket: port: 21000 @@ -374,6 +395,9 @@ spec: {{- if $root.Values.global.enableConsulNamespaces }} -namespace={{ default $defaults.consulNamespace .consulNamespace }} \ {{- end }} + {{- if $root.Values.global.adminPartitions.enabled }} + -partition={{ $root.Values.global.adminPartitions.name }} \ + {{- end }} -id="${POD_NAME}" # consul-sidecar ensures the ingress gateway is always registered with diff --git a/charts/consul/templates/mesh-gateway-deployment.yaml b/charts/consul/templates/mesh-gateway-deployment.yaml index de3f0ff689..8a0a05caba 100644 --- a/charts/consul/templates/mesh-gateway-deployment.yaml +++ b/charts/consul/templates/mesh-gateway-deployment.yaml @@ -7,6 +7,7 @@ {{- if .Values.global.lifecycleSidecarContainer }}{{ fail "global.lifecycleSidecarContainer has been renamed to global.consulSidecarContainer. Please set values using global.consulSidecarContainer." }}{{ end }} {{- /* The below test checks if clients are disabled (and if so, fails). We use the conditional from other client files and prepend 'not' */ -}} {{- if not (or (and (ne (.Values.client.enabled | toString) "-") .Values.client.enabled) (and (eq (.Values.client.enabled | toString) "-") .Values.global.enabled)) }}{{ fail "clients must be enabled" }}{{ end -}} +{{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -35,6 +36,20 @@ spec: component: mesh-gateway annotations: "consul.hashicorp.com/connect-inject": "false" + {{- if (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- end }} {{- if (and .Values.global.metrics.enabled .Values.global.metrics.enableGatewayMetrics) }} "prometheus.io/scrape": "true" "prometheus.io/path": "/metrics" @@ -185,6 +200,9 @@ spec: {{- end }} port = {{ .Values.meshGateway.containerPort }} address = "${POD_IP}" + {{- if .Values.global.adminPartitions.enabled }} + partition = "{{ .Values.global.adminPartitions.name }}" + {{- end }} tagged_addresses { lan { address = "${POD_IP}" @@ -225,13 +243,9 @@ spec: mountPath: /consul/tls/ca readOnly: true {{- end }} - resources: - requests: - memory: "50Mi" - cpu: "50m" - limits: - memory: "50Mi" - cpu: "50m" + {{- if .Values.meshGateway.initServiceInitContainer.resources }} + resources: {{ toYaml .Values.meshGateway.initServiceInitContainer.resources | nindent 12 }} + {{- end }} containers: - name: mesh-gateway image: {{ .Values.global.imageEnvoy | quote }} @@ -295,6 +309,9 @@ spec: - connect - envoy - -mesh-gateway + {{- if .Values.global.adminPartitions.enabled }} + - -partition={{ .Values.global.adminPartitions.name }} + {{- end }} livenessProbe: tcpSocket: port: {{ .Values.meshGateway.containerPort }} diff --git a/charts/consul/templates/mesh-gateway-podsecuritypolicy.yaml b/charts/consul/templates/mesh-gateway-podsecuritypolicy.yaml index 5257b79ed4..b5bbb2fa03 100644 --- a/charts/consul/templates/mesh-gateway-podsecuritypolicy.yaml +++ b/charts/consul/templates/mesh-gateway-podsecuritypolicy.yaml @@ -30,6 +30,14 @@ spec: {{- else }} hostNetwork: false {{- end }} + hostPorts: + {{- if .Values.meshGateway.hostPort }} + - min: {{ .Values.meshGateway.hostPort }} + max: {{ .Values.meshGateway.hostPort }} + {{- else if .Values.meshGateway.hostNetwork }} + - min: {{ .Values.meshGateway.containerPort }} + max: {{ .Values.meshGateway.containerPort }} + {{- end }} hostIPC: false hostPID: false runAsUser: diff --git a/charts/consul/templates/partition-init-job.yaml b/charts/consul/templates/partition-init-job.yaml new file mode 100644 index 0000000000..fe5f26fd86 --- /dev/null +++ b/charts/consul/templates/partition-init-job.yaml @@ -0,0 +1,103 @@ +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled (not $serverEnabled) (ne .Values.global.adminPartitions.name "default")) }} +{{- template "consul.reservedNamesFailer" (list .Values.global.adminPartitions.name "global.adminPartitions.name") }} +{{- if and (not .Values.externalServers.enabled) (ne .Values.global.adminPartitions.name "default") }}{{ fail "externalServers.enabled needs to be true and configured to create a non-default partition." }}{{ end -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ template "consul.fullname" . }}-partition-init + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: partition-init + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-weight": "2" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +spec: + template: + metadata: + name: {{ template "consul.fullname" . }}-partition-init + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + release: {{ .Release.Name }} + component: partition-init + annotations: + "consul.hashicorp.com/connect-inject": "false" + spec: + restartPolicy: Never + serviceAccountName: {{ template "consul.fullname" . }}-partition-init + {{- if .Values.global.tls.enabled }} + {{- if not .Values.externalServers.useSystemRoots }} + volumes: + - name: consul-ca-cert + secret: + {{- if .Values.global.tls.caCert.secretName }} + secretName: {{ .Values.global.tls.caCert.secretName }} + {{- else }} + secretName: {{ template "consul.fullname" . }}-ca-cert + {{- end }} + items: + - key: {{ default "tls.crt" .Values.global.tls.caCert.secretKey }} + path: tls.crt + {{- end }} + {{- end }} + containers: + - name: partition-init-job + image: {{ .Values.global.imageK8S }} + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- if (and .Values.global.acls.bootstrapToken.secretName .Values.global.acls.bootstrapToken.secretKey) }} + - name: CONSUL_HTTP_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.global.acls.bootstrapToken.secretName }} + key: {{ .Values.global.acls.bootstrapToken.secretKey }} + {{- end }} + {{- if .Values.global.tls.enabled }} + {{- if not .Values.externalServers.useSystemRoots }} + volumeMounts: + - name: consul-ca-cert + mountPath: /consul/tls/ca + readOnly: true + {{- end }} + {{- end }} + command: + - "/bin/sh" + - "-ec" + - | + consul-k8s-control-plane partition-init \ + -log-level={{ .Values.global.logLevel }} \ + -log-json={{ .Values.global.logJSON }} \ + + {{- if and .Values.externalServers.enabled (not .Values.externalServers.hosts) }}{{ fail "externalServers.hosts must be set if externalServers.enabled is true" }}{{ end -}} + {{- range .Values.externalServers.hosts }} + -server-address={{ quote . }} \ + {{- end }} + -server-port={{ .Values.externalServers.httpsPort }} \ + + {{- if .Values.global.tls.enabled }} + -use-https \ + {{- if not .Values.externalServers.useSystemRoots }} + -consul-ca-cert=/consul/tls/ca/tls.crt \ + {{- end }} + {{- if .Values.externalServers.tlsServerName }} + -consul-tls-server-name={{ .Values.externalServers.tlsServerName }} \ + {{- end }} + {{- end }} + -partition-name={{ .Values.global.adminPartitions.name }} + resources: + requests: + memory: "50Mi" + cpu: "50m" + limits: + memory: "50Mi" + cpu: "50m" +{{- end }} diff --git a/charts/consul/templates/partition-init-podsecuritypolicy.yaml b/charts/consul/templates/partition-init-podsecuritypolicy.yaml new file mode 100644 index 0000000000..f335e59084 --- /dev/null +++ b/charts/consul/templates/partition-init-podsecuritypolicy.yaml @@ -0,0 +1,39 @@ +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled (not $serverEnabled)) }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ template "consul.fullname" . }}-partition-init + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: partition-init + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +spec: + privileged: false + # Allow core volume types. + volumes: + - 'secret' + allowPrivilegeEscalation: false + # This is redundant with non-root + disallow privilege escalation, + # but we can provide it for defense in depth. + requiredDropCapabilities: + - ALL + hostNetwork: false + hostIPC: false + hostPID: false + runAsUser: + rule: 'RunAsAny' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'RunAsAny' + fsGroup: + rule: 'RunAsAny' + readOnlyRootFilesystem: false +{{- end }} diff --git a/charts/consul/templates/partition-init-role.yaml b/charts/consul/templates/partition-init-role.yaml new file mode 100644 index 0000000000..c13a5378eb --- /dev/null +++ b/charts/consul/templates/partition-init-role.yaml @@ -0,0 +1,41 @@ +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled (not $serverEnabled)) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "consul.fullname" . }}-partition-init + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: partition-init + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +rules: + - apiGroups: [""] + resources: + - secrets + verbs: + - create + - get +{{- if .Values.connectInject.enabled }} + - apiGroups: [""] + resources: + - serviceaccounts + resourceNames: + - {{ template "consul.fullname" . }}-connect-injector + verbs: + - get +{{- end }} +{{- if .Values.global.enablePodSecurityPolicies }} + - apiGroups: ["policy"] + resources: ["podsecuritypolicies"] + resourceNames: + - {{ template "consul.fullname" . }}-partition-init + verbs: + - use +{{- end }} +{{- end }} diff --git a/charts/consul/templates/partition-init-rolebinding.yaml b/charts/consul/templates/partition-init-rolebinding.yaml new file mode 100644 index 0000000000..432d6df6ec --- /dev/null +++ b/charts/consul/templates/partition-init-rolebinding.yaml @@ -0,0 +1,24 @@ +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled (not $serverEnabled)) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "consul.fullname" . }}-partition-init + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: partition-init + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "consul.fullname" . }}-partition-init +subjects: + - kind: ServiceAccount + name: {{ template "consul.fullname" . }}-partition-init +{{- end }} diff --git a/charts/consul/templates/partition-init-serviceaccount.yaml b/charts/consul/templates/partition-init-serviceaccount.yaml new file mode 100644 index 0000000000..65fcf43b08 --- /dev/null +++ b/charts/consul/templates/partition-init-serviceaccount.yaml @@ -0,0 +1,23 @@ +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled (not $serverEnabled)) }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "consul.fullname" . }}-partition-init + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: partition-init + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": before-hook-creation +{{- with .Values.global.imagePullSecrets }} +imagePullSecrets: +{{- range . }} + - name: {{ .name }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/consul/templates/partition-name-configmap.yaml b/charts/consul/templates/partition-name-configmap.yaml new file mode 100644 index 0000000000..ee330b0f46 --- /dev/null +++ b/charts/consul/templates/partition-name-configmap.yaml @@ -0,0 +1,19 @@ +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled (not $serverEnabled)) }} +# Immutable ConfigMap which saves the partition name. Attempting to update this configmap +# with a new Admin Partition name will cause the helm upgrade to fail +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "consul.fullname" . }}-partition + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: partition-init +immutable: true +data: + partitionName: {{ .Values.global.adminPartitions.name }} +{{- end }} diff --git a/charts/consul/templates/partition-service.yaml b/charts/consul/templates/partition-service.yaml new file mode 100644 index 0000000000..b9266a11c7 --- /dev/null +++ b/charts/consul/templates/partition-service.yaml @@ -0,0 +1,45 @@ +{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}} +{{- if (and .Values.global.adminPartitions.enabled $serverEnabled) }} +# Service with an external IP for clients in non-default Admin Partitions +# to discover Consul servers. This service should only point to Consul servers. +apiVersion: v1 +kind: Service +metadata: + name: {{ template "consul.fullname" . }}-partition + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: server + annotations: + {{- if .Values.global.adminPartitions.service.annotations }} + {{ tpl .Values.global.adminPartitions.service.annotations . | nindent 4 | trim }} + {{- end }} +spec: + type: "{{ .Values.global.adminPartitions.service.type }}" + ports: + - name: https + port: 8501 + targetPort: 8501 + {{ if (and (eq .Values.global.adminPartitions.service.type "NodePort") .Values.global.adminPartitions.service.nodePort.https) }} + nodePort: {{ .Values.global.adminPartitions.service.nodePort.https }} + {{- end }} + - name: serflan + port: 8301 + targetPort: 8301 + {{ if (and (eq .Values.global.adminPartitions.service.type "NodePort") .Values.global.adminPartitions.service.nodePort.serf) }} + nodePort: {{ .Values.global.adminPartitions.service.nodePort.serf }} + {{- end }} + - name: server + port: 8300 + targetPort: 8300 + {{ if (and (eq .Values.global.adminPartitions.service.type "NodePort") .Values.global.adminPartitions.service.nodePort.rpc) }} + nodePort: {{ .Values.global.adminPartitions.service.nodePort.rpc }} + {{- end }} + selector: + app: {{ template "consul.name" . }} + release: "{{ .Release.Name }}" + component: server +{{- end }} diff --git a/charts/consul/templates/server-acl-init-cleanup-job.yaml b/charts/consul/templates/server-acl-init-cleanup-job.yaml index 9ef60cf395..4db5e356e3 100644 --- a/charts/consul/templates/server-acl-init-cleanup-job.yaml +++ b/charts/consul/templates/server-acl-init-cleanup-job.yaml @@ -22,6 +22,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init-cleanup annotations: "helm.sh/hook": post-install,post-upgrade "helm.sh/hook-weight": "0" diff --git a/charts/consul/templates/server-acl-init-cleanup-podsecuritypolicy.yaml b/charts/consul/templates/server-acl-init-cleanup-podsecuritypolicy.yaml index 3d44c7fc18..dd5dad24df 100644 --- a/charts/consul/templates/server-acl-init-cleanup-podsecuritypolicy.yaml +++ b/charts/consul/templates/server-acl-init-cleanup-podsecuritypolicy.yaml @@ -12,6 +12,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init-cleanup spec: privileged: false # Allow core volume types. diff --git a/charts/consul/templates/server-acl-init-cleanup-role.yaml b/charts/consul/templates/server-acl-init-cleanup-role.yaml index 8d2e3d03b3..0a2f296a60 100644 --- a/charts/consul/templates/server-acl-init-cleanup-role.yaml +++ b/charts/consul/templates/server-acl-init-cleanup-role.yaml @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init-cleanup rules: - apiGroups: ["batch"] resources: ["jobs"] diff --git a/charts/consul/templates/server-acl-init-cleanup-rolebinding.yaml b/charts/consul/templates/server-acl-init-cleanup-rolebinding.yaml index d2e61807d2..268eaa5677 100644 --- a/charts/consul/templates/server-acl-init-cleanup-rolebinding.yaml +++ b/charts/consul/templates/server-acl-init-cleanup-rolebinding.yaml @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init-cleanup roleRef: apiGroup: rbac.authorization.k8s.io kind: Role diff --git a/charts/consul/templates/server-acl-init-cleanup-serviceaccount.yaml b/charts/consul/templates/server-acl-init-cleanup-serviceaccount.yaml index a5c39a4e5e..604e6d784c 100644 --- a/charts/consul/templates/server-acl-init-cleanup-serviceaccount.yaml +++ b/charts/consul/templates/server-acl-init-cleanup-serviceaccount.yaml @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init-cleanup {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- range . }} diff --git a/charts/consul/templates/server-acl-init-job.yaml b/charts/consul/templates/server-acl-init-job.yaml index 752774064a..27bc8c1bf0 100644 --- a/charts/consul/templates/server-acl-init-job.yaml +++ b/charts/consul/templates/server-acl-init-job.yaml @@ -4,7 +4,9 @@ {{- if and .Values.global.acls.createReplicationToken (not .Values.global.acls.manageSystemACLs) }}{{ fail "if global.acls.createReplicationToken is true, global.acls.manageSystemACLs must be true" }}{{ end -}} {{- if .Values.global.bootstrapACLs }}{{ fail "global.bootstrapACLs was removed, use global.acls.manageSystemACLs instead" }}{{ end -}} {{- if .Values.global.acls.manageSystemACLs }} -{{- /* We don't render this job when server.updatePartition > 0 because that +{{- if and .Values.global.secretsBackend.vault.enabled .Values.global.acls.replicationToken.secretName (not .Values.global.secretsBackend.vault.manageSystemACLsRole) }}{{ fail "global.secretsBackend.vault.manageSystemACLsRole must be set if global.secretsBackend.vault.enabled is true and global.acls.replicationToken is provided" }}{{ end -}} +{{- if or (and .Values.global.acls.replicationToken.secretName (not .Values.global.acls.replicationToken.secretKey)) (and .Values.global.acls.replicationToken.secretKey (not .Values.global.acls.replicationToken.secretName))}}{{ fail "both global.acls.replicationToken.secretKey and global.acls.replicationToken.secretName must be set if one of them is provided" }}{{ end -}} + {{- /* We don't render this job when server.updatePartition > 0 because that means a server rollout is in progress and this job won't complete unless the rollout is finished (which won't happen until the partition is 0). If we ran it in this case, then the job would not complete which would cause @@ -22,6 +24,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init spec: template: metadata: @@ -33,12 +36,34 @@ spec: component: server-acl-init annotations: "consul.hashicorp.com/connect-inject": "false" + {{- if (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-pre-populate-only": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if .Values.global.secretsBackend.vault.manageSystemACLsRole }} + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.manageSystemACLsRole }} + {{- else if .Values.global.tls.enabled }} + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + {{- end }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- if .Values.global.acls.replicationToken.secretName }} + "vault.hashicorp.com/agent-inject-secret-replication-token": "{{ .Values.global.acls.replicationToken.secretName }}" + "vault.hashicorp.com/agent-inject-template-replication-token": {{ template "consul.vaultReplicationTokenTemplate" . }} + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- end }} spec: restartPolicy: Never serviceAccountName: {{ template "consul.fullname" . }}-server-acl-init - {{- if (or .Values.global.tls.enabled (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey) (and .Values.global.acls.bootstrapToken.secretName .Values.global.acls.bootstrapToken.secretKey)) }} + {{- if (or .Values.global.tls.enabled .Values.global.acls.replicationToken.secretName (and .Values.global.acls.bootstrapToken.secretName .Values.global.acls.bootstrapToken.secretKey)) }} volumes: - {{- if .Values.global.tls.enabled }} + {{- if and .Values.global.tls.enabled (not .Values.global.secretsBackend.vault.enabled) }} - name: consul-ca-cert secret: {{- if .Values.global.tls.caCert.secretName }} @@ -57,7 +82,7 @@ spec: items: - key: {{ .Values.global.acls.bootstrapToken.secretKey }} path: bootstrap-token - {{- else if (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey) }} + {{- else if and .Values.global.acls.replicationToken.secretName (not .Values.global.secretsBackend.vault.enabled) }} - name: acl-replication-token secret: secretName: {{ .Values.global.acls.replicationToken.secretName }} @@ -74,9 +99,9 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - {{- if (or .Values.global.tls.enabled (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey) (and .Values.global.acls.bootstrapToken.secretName .Values.global.acls.bootstrapToken.secretKey)) }} + {{- if (or .Values.global.tls.enabled .Values.global.acls.replicationToken.secretName (and .Values.global.acls.bootstrapToken.secretName .Values.global.acls.bootstrapToken.secretKey)) }} volumeMounts: - {{- if .Values.global.tls.enabled }} + {{- if and .Values.global.tls.enabled (not .Values.global.secretsBackend.vault.enabled) }} - name: consul-ca-cert mountPath: /consul/tls/ca readOnly: true @@ -85,7 +110,7 @@ spec: - name: bootstrap-token mountPath: /consul/acl/tokens readOnly: true - {{- else if (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey) }} + {{- else if and .Values.global.acls.replicationToken.secretName (not .Values.global.secretsBackend.vault.enabled) }} - name: acl-replication-token mountPath: /consul/acl/tokens readOnly: true @@ -118,8 +143,12 @@ spec: {{- if .Values.global.tls.enabled }} -use-https \ {{- if not (and .Values.externalServers.enabled .Values.externalServers.useSystemRoots) }} + {{- if .Values.global.secretsBackend.vault.enabled }} + -consul-ca-cert=/vault/secrets/serverca.crt \ + {{- else }} -consul-ca-cert=/consul/tls/ca/tls.crt \ {{- end }} + {{- end }} {{- if not .Values.externalServers.enabled }} -server-port=8501 \ {{- end }} @@ -134,7 +163,10 @@ spec: -sync-consul-node-name={{ .Values.syncCatalog.consulNodeName }} \ {{- end }} {{- end }} - + {{- if .Values.global.adminPartitions.enabled }} + -enable-partitions=true \ + -partition={{ .Values.global.adminPartitions.name }} \ + {{- end }} {{- if (or (and (ne (.Values.dns.enabled | toString) "-") .Values.dns.enabled) (and (eq (.Values.dns.enabled | toString) "-") .Values.global.enabled)) }} -allow-dns=true \ {{- end }} @@ -188,7 +220,7 @@ spec: -acl-binding-rule-selector={{ .Values.connectInject.aclBindingRuleSelector }} \ {{- end }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.secretKey) }} -create-enterprise-license-token=true \ {{- end }} @@ -210,14 +242,22 @@ spec: {{- if (and .Values.global.acls.bootstrapToken.secretName .Values.global.acls.bootstrapToken.secretKey) }} -bootstrap-token-file=/consul/acl/tokens/bootstrap-token \ - {{- else if (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey) }} + {{- else if .Values.global.acls.replicationToken.secretName }} + {{- if .Values.global.secretsBackend.vault.enabled }} + -acl-replication-token-file=/vault/secrets/replication-token \ + {{- else }} -acl-replication-token-file=/consul/acl/tokens/acl-replication-token \ {{- end }} + {{- end }} {{- if .Values.controller.enabled }} -create-controller-token=true \ {{- end }} + {{- if .Values.apiGateway.enabled }} + -create-api-gateway-token=true \ + {{- end }} + {{- if .Values.global.enableConsulNamespaces }} -enable-namespaces=true \ diff --git a/charts/consul/templates/server-acl-init-podsecuritypolicy.yaml b/charts/consul/templates/server-acl-init-podsecuritypolicy.yaml index a6881ffa68..9bf93e2551 100644 --- a/charts/consul/templates/server-acl-init-podsecuritypolicy.yaml +++ b/charts/consul/templates/server-acl-init-podsecuritypolicy.yaml @@ -12,11 +12,13 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init spec: privileged: false # Allow core volume types. volumes: - 'secret' + - 'emptyDir' allowPrivilegeEscalation: false # This is redundant with non-root + disallow privilege escalation, # but we can provide it for defense in depth. diff --git a/charts/consul/templates/server-acl-init-role.yaml b/charts/consul/templates/server-acl-init-role.yaml index 4f22d47b7f..e828ae9b3f 100644 --- a/charts/consul/templates/server-acl-init-role.yaml +++ b/charts/consul/templates/server-acl-init-role.yaml @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init rules: - apiGroups: [""] resources: @@ -23,7 +24,7 @@ rules: resources: - serviceaccounts resourceNames: - - {{ template "consul.fullname" . }}-connect-injector-authmethod-svc-account + - {{ template "consul.fullname" . }}-connect-injector verbs: - get {{- end }} diff --git a/charts/consul/templates/server-acl-init-rolebinding.yaml b/charts/consul/templates/server-acl-init-rolebinding.yaml index 2c54452603..fda4726d9f 100644 --- a/charts/consul/templates/server-acl-init-rolebinding.yaml +++ b/charts/consul/templates/server-acl-init-rolebinding.yaml @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init roleRef: apiGroup: rbac.authorization.k8s.io kind: Role diff --git a/charts/consul/templates/server-acl-init-serviceaccount.yaml b/charts/consul/templates/server-acl-init-serviceaccount.yaml index 0f8e5e817c..c0e257de96 100644 --- a/charts/consul/templates/server-acl-init-serviceaccount.yaml +++ b/charts/consul/templates/server-acl-init-serviceaccount.yaml @@ -11,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server-acl-init {{- with .Values.global.imagePullSecrets }} imagePullSecrets: {{- range . }} diff --git a/charts/consul/templates/server-config-configmap.yaml b/charts/consul/templates/server-config-configmap.yaml index 6faf60b4b9..3ac7a29fd4 100644 --- a/charts/consul/templates/server-config-configmap.yaml +++ b/charts/consul/templates/server-config-configmap.yaml @@ -10,7 +10,42 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server data: + {{- $vaultConnectCAEnabled := and .Values.global.secretsBackend.vault.connectCA.address .Values.global.secretsBackend.vault.connectCA.rootPKIPath .Values.global.secretsBackend.vault.connectCA.intermediatePKIPath -}} + {{- if and .Values.global.secretsBackend.vault.enabled $vaultConnectCAEnabled }} + {{- with .Values.global.secretsBackend.vault }} + connect-ca-config.json: | + { + "connect": [ + { + "ca_config": [ + { + "address": "{{ .connectCA.address }}", + {{- if and .ca.secretName .ca.secretKey }} + "ca_file": "/consul/vault-ca/tls.crt", + {{- end }} + "intermediate_pki_path": "{{ .connectCA.intermediatePKIPath }}", + "root_pki_path": "{{ .connectCA.rootPKIPath }}", + "auth_method": { + "type": "kubernetes", + "mount_path": "{{ .connectCA.authMethodPath }}", + "params": { + "role": "{{ .consulServerRole }}" + } + } + } + ], + "ca_provider": "vault" + } + ] + } + {{- if .connectCA.additionalConfig }} + additional-connect-ca-config.json: | +{{ tpl .connectCA.additionalConfig $ | trimAll "\"" | indent 4 }} + {{- end }} + {{- end }} + {{- end }} extra-from-values.json: |- {{ tpl .Values.server.extraConfig . | trimAll "\"" | indent 4 }} {{- if .Values.global.acls.manageSystemACLs }} @@ -27,7 +62,7 @@ data: } } {{- end }} - {{- if (and .Values.ui.enabled (or .Values.ui.metrics.enabled (and .Values.global.metrics.enabled (eq (.Values.ui.metrics.enabled | toString) "-")))) }} + {{- if (and .Values.ui.enabled (or (eq "true" (.Values.ui.metrics.enabled | toString) ) (and .Values.global.metrics.enabled (eq "-" (.Values.ui.metrics.enabled | toString))))) }} ui-config.json: |- { "ui_config": { @@ -43,4 +78,11 @@ data: { "enable_central_service_config": true } + {{- if .Values.global.federation.enabled }} + federation-config.json: |- + { + "primary_datacenter": "{{ .Values.global.federation.primaryDatacenter }}", + "primary_gateways": {{ .Values.global.federation.primaryGateways | toJson }} + } + {{- end }} {{- end }} diff --git a/charts/consul/templates/server-disruptionbudget.yaml b/charts/consul/templates/server-disruptionbudget.yaml index 34b1118f5c..edf9c1c57f 100644 --- a/charts/consul/templates/server-disruptionbudget.yaml +++ b/charts/consul/templates/server-disruptionbudget.yaml @@ -1,7 +1,7 @@ {{- if (and .Values.server.disruptionBudget.enabled (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled))) }} # PodDisruptionBudget to prevent degrading the server cluster through # voluntary cluster changes. -{{- if .Capabilities.APIVersions.Has "policy/v1" }} +{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} apiVersion: policy/v1 {{- else }} apiVersion: policy/v1beta1 @@ -15,6 +15,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server spec: maxUnavailable: {{ template "consul.pdb.maxUnavailable" . }} selector: diff --git a/charts/consul/templates/server-podsecuritypolicy.yaml b/charts/consul/templates/server-podsecuritypolicy.yaml index afd1c3e839..c037ee9b8e 100644 --- a/charts/consul/templates/server-podsecuritypolicy.yaml +++ b/charts/consul/templates/server-podsecuritypolicy.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server spec: privileged: false # Required to prevent escalations to root. @@ -26,6 +27,15 @@ spec: - 'downwardAPI' - 'persistentVolumeClaim' hostNetwork: false + hostPorts: + {{- if .Values.server.exposeGossipAndRPCPorts }} + - min: 8300 + max: 8300 + - min: {{ .Values.server.ports.serflan.port }} + max: {{ .Values.server.ports.serflan.port }} + - min: 8302 + max: 8302 + {{- end }} hostIPC: false hostPID: false runAsUser: diff --git a/charts/consul/templates/server-role.yaml b/charts/consul/templates/server-role.yaml index c01f3dabb4..202518bf67 100644 --- a/charts/consul/templates/server-role.yaml +++ b/charts/consul/templates/server-role.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server {{- if (or (and .Values.global.openshift.enabled .Values.server.exposeGossipAndRPCPorts) .Values.global.enablePodSecurityPolicies) }} rules: {{- if .Values.global.enablePodSecurityPolicies }} diff --git a/charts/consul/templates/server-rolebinding.yaml b/charts/consul/templates/server-rolebinding.yaml index 788fc978f7..8ab705ddbc 100644 --- a/charts/consul/templates/server-rolebinding.yaml +++ b/charts/consul/templates/server-rolebinding.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server roleRef: apiGroup: rbac.authorization.k8s.io kind: Role diff --git a/charts/consul/templates/server-securitycontextconstraints.yaml b/charts/consul/templates/server-securitycontextconstraints.yaml index e9acadceae..8edd784ea7 100644 --- a/charts/consul/templates/server-securitycontextconstraints.yaml +++ b/charts/consul/templates/server-securitycontextconstraints.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server annotations: kubernetes.io/description: {{ template "consul.fullname" . }}-server are the security context constraints required to run the consul server. diff --git a/charts/consul/templates/server-serviceaccount.yaml b/charts/consul/templates/server-serviceaccount.yaml index 815e1e71f9..a1617975ae 100644 --- a/charts/consul/templates/server-serviceaccount.yaml +++ b/charts/consul/templates/server-serviceaccount.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: server {{- if .Values.server.serviceAccount.annotations }} annotations: {{ tpl .Values.server.serviceAccount.annotations . | nindent 4 | trim }} diff --git a/charts/consul/templates/server-statefulset.yaml b/charts/consul/templates/server-statefulset.yaml index 09668f2859..0d669f3a81 100644 --- a/charts/consul/templates/server-statefulset.yaml +++ b/charts/consul/templates/server-statefulset.yaml @@ -1,10 +1,20 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} +{{- if and .Values.global.federation.enabled .Values.global.adminPartitions.enabled }}{{ fail "If global.federation.enabled is true, global.adminPartitions.enabled must be false because they are mutually exclusive" }}{{ end }} {{- if and .Values.global.federation.enabled (not .Values.global.tls.enabled) }}{{ fail "If global.federation.enabled is true, global.tls.enabled must be true because federation is only supported with TLS enabled" }}{{ end }} {{- if and .Values.global.federation.enabled (not .Values.meshGateway.enabled) }}{{ fail "If global.federation.enabled is true, meshGateway.enabled must be true because mesh gateways are required for federation" }}{{ end }} {{- if and .Values.server.serverCert.secretName (not .Values.global.tls.caCert.secretName) }}{{ fail "If server.serverCert.secretName is provided, global.tls.caCert must also be provided" }}{{ end }} {{- if .Values.server.disableFsGroupSecurityContext }}{{ fail "server.disableFsGroupSecurityContext has been removed. Please use global.openshift.enabled instead." }}{{ end }} {{- if .Values.server.bootstrapExpect }}{{ if lt (int .Values.server.bootstrapExpect) (int .Values.server.replicas) }}{{ fail "server.bootstrapExpect cannot be less than server.replicas" }}{{ end }}{{ end }} {{- if (and (and .Values.global.tls.enabled .Values.global.tls.httpsOnly) (and .Values.global.metrics.enabled .Values.global.metrics.enableAgentMetrics))}}{{ fail "global.metrics.enableAgentMetrics cannot be enabled if TLS (HTTPS only) is enabled" }}{{ end -}} +{{- if (and .Values.global.gossipEncryption.secretName (not .Values.global.gossipEncryption.secretKey)) }}{{fail "gossipEncryption.secretKey and secretName must both be specified." }}{{ end -}} +{{- if (and (not .Values.global.gossipEncryption.secretName) .Values.global.gossipEncryption.secretKey) }}{{fail "gossipEncryption.secretKey and secretName must both be specified." }}{{ end -}} +{{- if (and .Values.global.secretsBackend.vault.enabled (not .Values.global.secretsBackend.vault.consulServerRole)) }}{{ fail "global.secretsBackend.vault.consulServerRole must be provided if global.secretsBackend.vault.enabled=true." }}{{ end -}} +{{- if (and .Values.server.serverCert.secretName (not .Values.global.tls.caCert.secretName)) }}{{ fail "If server.serverCert.secretName is provided, global.tls.caCert.secretName must also be provided" }}{{ end }} +{{- if (and (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) (not .Values.global.tls.caCert.secretName)) }}{{ fail "global.tls.caCert.secretName must be provided if global.tls.enabled=true and global.secretsBackend.vault.enabled=true." }}{{ end -}} +{{- if (and (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) (not .Values.global.tls.enableAutoEncrypt)) }}{{ fail "global.tls.enableAutoEncrypt must be true if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" }}{{ end -}} +{{- if (and (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) (not .Values.global.secretsBackend.vault.consulCARole)) }}{{ fail "global.secretsBackend.vault.consulCARole must be provided if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" }}{{ end -}} +{{- if (and .Values.global.enterpriseLicense.secretName (not .Values.global.enterpriseLicense.secretKey)) }}{{fail "enterpriseLicense.secretKey and secretName must both be specified." }}{{ end -}} +{{- if (and (not .Values.global.enterpriseLicense.secretName) .Values.global.enterpriseLicense.secretKey) }}{{fail "enterpriseLicense.secretKey and secretName must both be specified." }}{{ end -}} # StatefulSet to run the actual Consul server cluster. apiVersion: apps/v1 kind: StatefulSet @@ -46,6 +56,41 @@ spec: {{- toYaml .Values.server.extraLabels | nindent 8 }} {{- end }} annotations: + {{- if .Values.global.secretsBackend.vault.enabled }} + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": "{{ .Values.global.secretsBackend.vault.consulServerRole }}" + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": {{ .Values.global.secretsBackend.vault.ca.secretName }} + "vault.hashicorp.com/ca-cert": /vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }} + {{- end }} + {{- if .Values.global.gossipEncryption.secretName }} + {{- with .Values.global.gossipEncryption }} + "vault.hashicorp.com/agent-inject-secret-gossip.txt": "{{ .secretName }}" + "vault.hashicorp.com/agent-inject-template-gossip.txt": {{ template "consul.vaultSecretTemplate" . }} + {{- end }} + {{- end }} + {{- if .Values.server.serverCert.secretName }} + "vault.hashicorp.com/agent-inject-secret-servercert.crt": {{ .Values.server.serverCert.secretName }} + "vault.hashicorp.com/agent-inject-template-servercert.crt": {{ include "consul.serverTLSCertTemplate" . }} + "vault.hashicorp.com/agent-inject-secret-servercert.key": {{ .Values.server.serverCert.secretName }} + "vault.hashicorp.com/agent-inject-template-servercert.key": {{ include "consul.serverTLSKeyTemplate" . }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ include "consul.serverTLSCATemplate" . }} + {{- end }} + {{- if (and .Values.global.acls.replicationToken.secretName (not .Values.global.acls.createReplicationToken)) }} + "vault.hashicorp.com/agent-inject-secret-replication-token-config.hcl": "{{ .Values.global.acls.replicationToken.secretName }}" + "vault.hashicorp.com/agent-inject-template-replication-token-config.hcl": {{ template "consul.vaultReplicationTokenConfigTemplate" . }} + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- if .Values.global.enterpriseLicense.secretName }} + {{- with .Values.global.enterpriseLicense }} + "vault.hashicorp.com/agent-inject-secret-enterpriselicense.txt": "{{ .secretName }}" + "vault.hashicorp.com/agent-inject-template-enterpriselicense.txt": {{ template "consul.vaultSecretTemplate" . }} + {{- end }} + {{- end }} + {{- end }} "consul.hashicorp.com/connect-inject": "false" "consul.hashicorp.com/config-checksum": {{ include (print $.Template.BasePath "/server-config-configmap.yaml") . | sha256sum }} {{- if .Values.server.annotations }} @@ -66,12 +111,8 @@ spec: {{ tpl .Values.server.tolerations . | nindent 8 | trim }} {{- end }} {{- if .Values.server.topologySpreadConstraints }} - {{- if and (ge .Capabilities.KubeVersion.Major "1") (ge .Capabilities.KubeVersion.Minor "18") }} topologySpreadConstraints: {{ tpl .Values.server.topologySpreadConstraints . | nindent 8 | trim }} - {{- else }} - {{- fail "`topologySpreadConstraints` requires Kubernetes 1.18 and above." }} - {{- end }} {{- end }} terminationGracePeriodSeconds: 30 serviceAccountName: {{ template "consul.fullname" . }}-server @@ -83,7 +124,7 @@ spec: - name: config configMap: name: {{ template "consul.fullname" . }}-server-config - {{- if .Values.global.tls.enabled }} + {{- if (and .Values.global.tls.enabled (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-ca-cert secret: {{- if .Values.global.tls.caCert.secretName }} @@ -102,10 +143,18 @@ spec: secretName: {{ template "consul.fullname" . }}-server-cert {{- end }} {{- end }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.enableLicenseAutoload (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-license secret: - secretName: {{ .Values.server.enterpriseLicense.secretName }} + secretName: {{ .Values.global.enterpriseLicense.secretName }} + {{- end }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + - name: vault-ca + secret: + secretName: {{ .Values.global.secretsBackend.vault.ca.secretName }} + items: + - key: {{ .Values.global.secretsBackend.vault.ca.secretKey }} + path: tls.crt {{- end }} {{- range .Values.server.extraVolumes }} - name: userconfig-{{ .name }} @@ -156,24 +205,39 @@ spec: fieldPath: metadata.namespace - name: CONSUL_DISABLE_PERM_MGMT value: "true" - {{- if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} + {{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }} + {{- if not .Values.global.secretsBackend.vault.enabled }} - name: GOSSIP_KEY valueFrom: secretKeyRef: + {{- if .Values.global.gossipEncryption.autoGenerate }} + name: {{ template "consul.fullname" . }}-gossip-encryption-key + key: key + {{- else if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} name: {{ .Values.global.gossipEncryption.secretName }} key: {{ .Values.global.gossipEncryption.secretKey }} + {{- end }} + {{- end }} {{- end }} {{- if .Values.global.tls.enabled }} - name: CONSUL_HTTP_ADDR value: https://localhost:8501 - name: CONSUL_CACERT + {{- if .Values.global.secretsBackend.vault.enabled }} + value: /vault/secrets/serverca.crt + {{- else }} value: /consul/tls/ca/tls.crt + {{- end }} {{- end }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.enableLicenseAutoload) }} - name: CONSUL_LICENSE_PATH - value: /consul/license/{{ .Values.server.enterpriseLicense.secretKey }} + {{- if .Values.global.secretsBackend.vault.enabled }} + value: /vault/secrets/enterpriselicense.txt + {{- else }} + value: /consul/license/{{ .Values.global.enterpriseLicense.secretKey }} + {{- end }} {{- end }} - {{- if (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey) }} + {{- if (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey (not .Values.global.secretsBackend.vault.enabled)) }} - name: ACL_REPLICATION_TOKEN valueFrom: secretKeyRef: @@ -187,6 +251,14 @@ spec: - | CONSUL_FULLNAME="{{template "consul.fullname" . }}" + {{- if and .Values.global.secretsBackend.vault.enabled .Values.global.gossipEncryption.secretName }} + GOSSIP_KEY=`cat /vault/secrets/gossip.txt` + {{- end }} + + {{- if (and .Values.dns.enabled .Values.dns.enableRedirection) }} + {{ template "consul.recursors" }} + {{- end }} + {{ template "consul.extraconfig" }} exec /usr/local/bin/docker-entrypoint.sh consul agent \ @@ -194,9 +266,15 @@ spec: -bind=0.0.0.0 \ -bootstrap-expect={{ if .Values.server.bootstrapExpect }}{{ .Values.server.bootstrapExpect }}{{ else }}{{ .Values.server.replicas }}{{ end }} \ {{- if .Values.global.tls.enabled }} + {{- if .Values.global.secretsBackend.vault.enabled }} + -hcl='ca_file = "/vault/secrets/serverca.crt"' \ + -hcl='cert_file = "/vault/secrets/servercert.crt"' \ + -hcl='key_file = "/vault/secrets/servercert.key"' \ + {{- else }} -hcl='ca_file = "/consul/tls/ca/tls.crt"' \ -hcl='cert_file = "/consul/tls/server/tls.crt"' \ -hcl='key_file = "/consul/tls/server/tls.key"' \ + {{- end }} {{- if .Values.global.tls.enableAutoEncrypt }} -hcl='auto_encrypt = {allow_tls = true}' \ {{- end }} @@ -223,7 +301,7 @@ spec: -datacenter={{ .Values.global.datacenter }} \ -data-dir=/consul/data \ -domain={{ .Values.global.domain }} \ - {{- if (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey) }} + {{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }} -encrypt="${GOSSIP_KEY}" \ {{- end }} {{- if .Values.server.connect }} @@ -236,10 +314,17 @@ spec: -hcl="connect { enable_mesh_gateway_wan_federation = true }" \ {{- end }} {{- if (and .Values.global.acls.replicationToken.secretName .Values.global.acls.replicationToken.secretKey) }} + {{- if (and .Values.global.secretsBackend.vault.enabled (not .Values.global.acls.createReplicationToken)) }} + -config-file=/vault/secrets/replication-token-config.hcl \ + {{- else }} -hcl="acl { tokens { agent = \"${ACL_REPLICATION_TOKEN}\", replication = \"${ACL_REPLICATION_TOKEN}\" } }" \ {{- end }} + {{- end }} {{- if .Values.ui.enabled }} -ui \ + {{- if .Values.ui.dashboardURLTemplates.service }} + -hcl='ui_config { dashboard_url_templates { service = "{{ .Values.ui.dashboardURLTemplates.service }}" } }' \ + {{- end }} {{- end }} {{- $serverSerfLANPort := .Values.server.ports.serflan.port -}} {{- range $index := until (.Values.server.replicas | int) }} @@ -249,14 +334,17 @@ spec: {{- range $value := .Values.global.recursors }} -recursor={{ quote $value }} \ {{- end }} + {{- if (and .Values.dns.enabled .Values.dns.enableRedirection) }} + $recursor_flags \ + {{- end }} -config-file=/consul/extra-config/extra-from-values.json \ -server volumeMounts: - - name: data-{{ .Release.Namespace }} + - name: data-{{ .Release.Namespace | trunc 58 | trimSuffix "-" }} mountPath: /consul/data - name: config mountPath: /consul/config - {{- if .Values.global.tls.enabled }} + {{- if (and .Values.global.tls.enabled (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-ca-cert mountPath: /consul/tls/ca/ readOnly: true @@ -264,7 +352,7 @@ spec: mountPath: /consul/tls/server readOnly: true {{- end }} - {{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload) }} + {{- if (and .Values.global.enterpriseLicense.secretName .Values.global.enterpriseLicense.enableLicenseAutoload (not .Values.global.secretsBackend.vault.enabled)) }} - name: consul-license mountPath: /consul/license readOnly: true @@ -274,6 +362,11 @@ spec: readOnly: true mountPath: /consul/userconfig/{{ .name }} {{- end }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + - name: vault-ca + mountPath: /consul/vault-ca/ + readOnly: true + {{- end }} ports: {{- if (or (not .Values.global.tls.enabled) (not .Values.global.tls.httpsOnly)) }} - name: http @@ -327,8 +420,7 @@ spec: - "-ec" - | {{- if .Values.global.tls.enabled }} - curl \ - --cacert /consul/tls/ca/tls.crt \ + curl -k \ https://127.0.0.1:8501/v1/status/leader \ {{- else }} curl http://127.0.0.1:8500/v1/status/leader \ @@ -351,13 +443,16 @@ spec: securityContext: {{- toYaml .Values.server.containerSecurityContext.server | nindent 12 }} {{- end }} + {{- if .Values.server.extraContainers }} + {{ toYaml .Values.server.extraContainers | nindent 8 }} + {{- end }} {{- if .Values.server.nodeSelector }} nodeSelector: {{ tpl .Values.server.nodeSelector . | indent 8 | trim }} {{- end }} volumeClaimTemplates: - metadata: - name: data-{{ .Release.Namespace }} + name: data-{{ .Release.Namespace | trunc 58 | trimSuffix "-" }} spec: accessModes: - ReadWriteOnce diff --git a/charts/consul/templates/sync-catalog-clusterrole.yaml b/charts/consul/templates/sync-catalog-clusterrole.yaml index 41609f7747..5ceeb03d47 100644 --- a/charts/consul/templates/sync-catalog-clusterrole.yaml +++ b/charts/consul/templates/sync-catalog-clusterrole.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: sync-catalog rules: - apiGroups: [""] resources: diff --git a/charts/consul/templates/sync-catalog-clusterrolebinding.yaml b/charts/consul/templates/sync-catalog-clusterrolebinding.yaml index 648bd30727..818823cca3 100644 --- a/charts/consul/templates/sync-catalog-clusterrolebinding.yaml +++ b/charts/consul/templates/sync-catalog-clusterrolebinding.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: sync-catalog roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole diff --git a/charts/consul/templates/sync-catalog-deployment.yaml b/charts/consul/templates/sync-catalog-deployment.yaml index 50a847b354..2aedc54460 100644 --- a/charts/consul/templates/sync-catalog-deployment.yaml +++ b/charts/consul/templates/sync-catalog-deployment.yaml @@ -1,5 +1,6 @@ {{- $clientEnabled := (or (and (ne (.Values.client.enabled | toString) "-") .Values.client.enabled) (and (eq (.Values.client.enabled | toString) "-") .Values.global.enabled)) }} {{- if (or (and (ne (.Values.syncCatalog.enabled | toString) "-") .Values.syncCatalog.enabled) (and (eq (.Values.syncCatalog.enabled | toString) "-") .Values.global.enabled)) }} +{{- template "consul.reservedNamesFailer" (list .Values.syncCatalog.consulNamespaces.consulDestinationNamespace "syncCatalog.consulNamespaces.consulDestinationNamespace") }} # The deployment for running the sync-catalog pod apiVersion: apps/v1 kind: Deployment @@ -11,6 +12,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: sync-catalog spec: replicas: 1 selector: @@ -31,6 +33,20 @@ spec: {{- end }} annotations: "consul.hashicorp.com/connect-inject": "false" + {{- if (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl .Values.global.secretsBackend.vault.agentAnnotations . | nindent 8 | trim }} + {{- end }} + {{- end }} spec: serviceAccountName: {{ template "consul.fullname" . }}-sync-catalog {{- if .Values.global.tls.enabled }} diff --git a/charts/consul/templates/sync-catalog-podsecuritypolicy.yaml b/charts/consul/templates/sync-catalog-podsecuritypolicy.yaml index 22f3e23eaa..cc70feaab1 100644 --- a/charts/consul/templates/sync-catalog-podsecuritypolicy.yaml +++ b/charts/consul/templates/sync-catalog-podsecuritypolicy.yaml @@ -9,6 +9,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: sync-catalog spec: privileged: false # Required to prevent escalations to root. diff --git a/charts/consul/templates/sync-catalog-serviceaccount.yaml b/charts/consul/templates/sync-catalog-serviceaccount.yaml index d029088a1b..deab1ad075 100644 --- a/charts/consul/templates/sync-catalog-serviceaccount.yaml +++ b/charts/consul/templates/sync-catalog-serviceaccount.yaml @@ -10,6 +10,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: sync-catalog {{- if .Values.syncCatalog.serviceAccount.annotations }} annotations: {{ tpl .Values.syncCatalog.serviceAccount.annotations . | nindent 4 | trim }} diff --git a/charts/consul/templates/terminating-gateways-deployment.yaml b/charts/consul/templates/terminating-gateways-deployment.yaml index f1a1a2fb26..902329a74a 100644 --- a/charts/consul/templates/terminating-gateways-deployment.yaml +++ b/charts/consul/templates/terminating-gateways-deployment.yaml @@ -1,6 +1,7 @@ {{- if .Values.terminatingGateways.enabled }} {{- if not .Values.connectInject.enabled }}{{ fail "connectInject.enabled must be true" }}{{ end -}} {{- if not .Values.client.grpc }}{{ fail "client.grpc must be true" }}{{ end -}} +{{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} {{- if not (or (and (ne (.Values.client.enabled | toString) "-") .Values.client.enabled) (and (eq (.Values.client.enabled | toString) "-") .Values.global.enabled)) }}{{ fail "clients must be enabled" }}{{ end -}} {{- if .Values.global.lifecycleSidecarContainer }}{{ fail "global.lifecycleSidecarContainer has been renamed to global.consulSidecarContainer. Please set values using global.consulSidecarContainer." }}{{ end }} @@ -52,6 +53,20 @@ spec: component: terminating-gateway terminating-gateway-name: {{ template "consul.fullname" $root }}-{{ .name }} annotations: + {{- if (and $root.Values.global.secretsBackend.vault.enabled $root.Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ $root.Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ $root.Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" $root }} + {{- if and $root.Values.global.secretsBackend.vault.ca.secretName $root.Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": {{ $root.Values.global.secretsBackend.vault.ca.secretName }} + "vault.hashicorp.com/ca-cert": /vault/custom/{{ $root.Values.global.secretsBackend.vault.ca.secretKey }} + {{- end }} + {{- if $root.Values.global.secretsBackend.vault.agentAnnotations }} + {{ tpl $root.Values.global.secretsBackend.vault.agentAnnotations $root | nindent 8 | trim }} + {{- end }} + {{- end }} "consul.hashicorp.com/connect-inject": "false" {{- if (and $root.Values.global.metrics.enabled $root.Values.global.metrics.enableGatewayMetrics) }} "prometheus.io/scrape": "true" @@ -183,6 +198,9 @@ spec: {{- if $root.Values.global.enableConsulNamespaces }} namespace = "{{ (default $defaults.consulNamespace .consulNamespace) }}" {{- end }} + {{- if $root.Values.global.adminPartitions.enabled }} + partition = "{{ $root.Values.global.adminPartitions.name }}" + {{- end }} address = "${POD_IP}" port = 8443 {{- if (and $root.Values.global.metrics.enabled $root.Values.global.metrics.enableGatewayMetrics) }} @@ -290,6 +308,9 @@ spec: {{- if $root.Values.global.enableConsulNamespaces }} - -namespace={{ default $defaults.consulNamespace .consulNamespace }} {{- end }} + {{- if $root.Values.global.adminPartitions.enabled }} + - -partition={{ $root.Values.global.adminPartitions.name }} + {{- end }} livenessProbe: tcpSocket: port: 8443 @@ -320,6 +341,9 @@ spec: {{- if $root.Values.global.enableConsulNamespaces }} -namespace={{ default $defaults.consulNamespace .consulNamespace }} \ {{- end }} + {{- if $root.Values.global.adminPartitions.enabled }} + -partition={{ $root.Values.global.adminPartitions.name }} \ + {{- end }} -id="${POD_NAME}" # consul-sidecar ensures the terminating gateway is always registered with diff --git a/charts/consul/templates/tls-init-cleanup-job.yaml b/charts/consul/templates/tls-init-cleanup-job.yaml index dc658d733d..9a8898cc10 100644 --- a/charts/consul/templates/tls-init-cleanup-job.yaml +++ b/charts/consul/templates/tls-init-cleanup-job.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} # tls-init-cleanup job deletes Kubernetes secrets created by tls-init apiVersion: batch/v1 kind: Job @@ -11,6 +12,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init-cleanup annotations: "helm.sh/hook": pre-delete "helm.sh/hook-delete-policy": hook-succeeded @@ -62,3 +64,4 @@ spec: cpu: "50m" {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-cleanup-podsecuritypolicy.yaml b/charts/consul/templates/tls-init-cleanup-podsecuritypolicy.yaml index c8dc00f62d..ed99d5f297 100644 --- a/charts/consul/templates/tls-init-cleanup-podsecuritypolicy.yaml +++ b/charts/consul/templates/tls-init-cleanup-podsecuritypolicy.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and (and .Values.global.tls.enabled .Values.global.enablePodSecurityPolicies) (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init-cleanup annotations: "helm.sh/hook": pre-delete "helm.sh/hook-delete-policy": hook-succeeded @@ -38,3 +40,4 @@ spec: readOnlyRootFilesystem: false {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-cleanup-role.yaml b/charts/consul/templates/tls-init-cleanup-role.yaml index 3922144997..aa66e3edc4 100644 --- a/charts/consul/templates/tls-init-cleanup-role.yaml +++ b/charts/consul/templates/tls-init-cleanup-role.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init-cleanup annotations: "helm.sh/hook": pre-delete "helm.sh/hook-delete-policy": hook-succeeded @@ -36,3 +38,4 @@ rules: {{- end }} {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-cleanup-rolebinding.yaml b/charts/consul/templates/tls-init-cleanup-rolebinding.yaml index c02f4d2e40..0d3bfe38e7 100644 --- a/charts/consul/templates/tls-init-cleanup-rolebinding.yaml +++ b/charts/consul/templates/tls-init-cleanup-rolebinding.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init-cleanup annotations: "helm.sh/hook": pre-delete "helm.sh/hook-delete-policy": hook-succeeded @@ -22,3 +24,4 @@ subjects: name: {{ template "consul.fullname" . }}-tls-init-cleanup {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-cleanup-serviceaccount.yaml b/charts/consul/templates/tls-init-cleanup-serviceaccount.yaml index f26d14122f..57e40dd3af 100644 --- a/charts/consul/templates/tls-init-cleanup-serviceaccount.yaml +++ b/charts/consul/templates/tls-init-cleanup-serviceaccount.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: v1 kind: ServiceAccount metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init-cleanup annotations: "helm.sh/hook": pre-delete "helm.sh/hook-delete-policy": hook-succeeded @@ -21,3 +23,4 @@ imagePullSecrets: {{- end }} {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-job.yaml b/charts/consul/templates/tls-init-job.yaml index 019513c897..ba75d94460 100644 --- a/charts/consul/templates/tls-init-job.yaml +++ b/charts/consul/templates/tls-init-job.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} # tls-init job generate Consul cluster CA and certificates for the Consul servers # and creates Kubernetes secrets for them. apiVersion: batch/v1 @@ -12,6 +13,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init annotations: "helm.sh/hook": pre-install,pre-upgrade "helm.sh/hook-weight": "1" @@ -102,3 +104,4 @@ spec: cpu: "50m" {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-podsecuritypolicy.yaml b/charts/consul/templates/tls-init-podsecuritypolicy.yaml index 4f188bd819..5d2a393955 100644 --- a/charts/consul/templates/tls-init-podsecuritypolicy.yaml +++ b/charts/consul/templates/tls-init-podsecuritypolicy.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and (and .Values.global.tls.enabled .Values.global.enablePodSecurityPolicies) (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init annotations: "helm.sh/hook": pre-install,pre-upgrade "helm.sh/hook-delete-policy": before-hook-creation @@ -38,3 +40,4 @@ spec: readOnlyRootFilesystem: false {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-role.yaml b/charts/consul/templates/tls-init-role.yaml index 5a27d8b44b..216602ee9f 100644 --- a/charts/consul/templates/tls-init-role.yaml +++ b/charts/consul/templates/tls-init-role.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init annotations: "helm.sh/hook": pre-install,pre-upgrade "helm.sh/hook-delete-policy": before-hook-creation @@ -33,3 +35,4 @@ rules: {{- end }} {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-rolebinding.yaml b/charts/consul/templates/tls-init-rolebinding.yaml index 3ac92e7316..9b68d97d8c 100644 --- a/charts/consul/templates/tls-init-rolebinding.yaml +++ b/charts/consul/templates/tls-init-rolebinding.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init annotations: "helm.sh/hook": pre-install,pre-upgrade "helm.sh/hook-delete-policy": before-hook-creation @@ -22,3 +24,4 @@ subjects: name: {{ template "consul.fullname" . }}-tls-init {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/tls-init-serviceaccount.yaml b/charts/consul/templates/tls-init-serviceaccount.yaml index e8b2d94ab1..f8504da94c 100644 --- a/charts/consul/templates/tls-init-serviceaccount.yaml +++ b/charts/consul/templates/tls-init-serviceaccount.yaml @@ -1,5 +1,6 @@ {{- if (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) }} {{- if (and .Values.global.tls.enabled (not .Values.server.serverCert.secretName)) }} +{{- if not .Values.global.secretsBackend.vault.enabled }} apiVersion: v1 kind: ServiceAccount metadata: @@ -10,6 +11,7 @@ metadata: chart: {{ template "consul.chart" . }} heritage: {{ .Release.Service }} release: {{ .Release.Name }} + component: tls-init annotations: "helm.sh/hook": pre-install,pre-upgrade "helm.sh/hook-delete-policy": before-hook-creation @@ -21,3 +23,4 @@ imagePullSecrets: {{- end }} {{- end }} {{- end }} +{{- end }} diff --git a/charts/consul/templates/ui-ingress.yaml b/charts/consul/templates/ui-ingress.yaml index 28d3069768..0414a7cc2d 100644 --- a/charts/consul/templates/ui-ingress.yaml +++ b/charts/consul/templates/ui-ingress.yaml @@ -12,7 +12,7 @@ apiVersion: networking.k8s.io/v1beta1 {{- end }} kind: Ingress metadata: - name: {{ template "consul.fullname" . }}-ingress + name: {{ template "consul.fullname" . }}-ui namespace: {{ .Release.Namespace }} labels: app: {{ template "consul.name" . }} @@ -25,6 +25,7 @@ metadata: {{ tpl .Values.ui.ingress.annotations . | nindent 4 | trim }} {{- end }} spec: + ingressClassName: {{ .Values.ui.ingress.ingressClassName }} rules: {{ $global := .Values.global }} {{- if or ( gt .Capabilities.KubeVersion.Major "1" ) ( ge .Capabilities.KubeVersion.Minor "19" ) }} diff --git a/charts/consul/templates/webhook-cert-manager-clusterrole.yaml b/charts/consul/templates/webhook-cert-manager-clusterrole.yaml index fd4e819d03..9708380617 100644 --- a/charts/consul/templates/webhook-cert-manager-clusterrole.yaml +++ b/charts/consul/templates/webhook-cert-manager-clusterrole.yaml @@ -45,7 +45,7 @@ rules: resources: - podsecuritypolicies resourceNames: - - {{ template "consul.fullname" . }}-connect-injector-webhook + - {{ template "consul.fullname" . }}-connect-injector verbs: - use {{- end }} diff --git a/charts/consul/templates/webhook-cert-manager-configmap.yaml b/charts/consul/templates/webhook-cert-manager-configmap.yaml index cbf8770d04..e13d14a7ab 100644 --- a/charts/consul/templates/webhook-cert-manager-configmap.yaml +++ b/charts/consul/templates/webhook-cert-manager-configmap.yaml @@ -15,19 +15,19 @@ data: [ {{- if .Values.connectInject.enabled }} { - "name": "{{ template "consul.fullname" . }}-connect-injector-cfg", + "name": "{{ template "consul.fullname" . }}-connect-injector", "tlsAutoHosts": [ - "{{ template "consul.fullname" . }}-connect-injector-svc", - "{{ template "consul.fullname" . }}-connect-injector-svc.{{ .Release.Namespace }}", - "{{ template "consul.fullname" . }}-connect-injector-svc.{{ .Release.Namespace }}.svc", - "{{ template "consul.fullname" . }}-connect-injector-svc.{{ .Release.Namespace }}.svc.cluster.local" + "{{ template "consul.fullname" . }}-connect-injector", + "{{ template "consul.fullname" . }}-connect-injector.{{ .Release.Namespace }}", + "{{ template "consul.fullname" . }}-connect-injector.{{ .Release.Namespace }}.svc", + "{{ template "consul.fullname" . }}-connect-injector.{{ .Release.Namespace }}.svc.cluster.local" ], "secretName": "{{ template "consul.fullname" . }}-connect-inject-webhook-cert", "secretNamespace": "{{ .Release.Namespace }}" }{{- if and .Values.controller.enabled }},{{- end }}{{- end }} {{- if and .Values.controller.enabled }} { - "name": "{{ template "consul.fullname" . }}-controller-mutating-webhook-configuration", + "name": "{{ template "consul.fullname" . }}-controller", "tlsAutoHosts": [ "{{ template "consul.fullname" . }}-controller-webhook", "{{ template "consul.fullname" . }}-controller-webhook.{{ .Release.Namespace }}", diff --git a/charts/consul/templates/webhook-cert-manager-deployment.yaml b/charts/consul/templates/webhook-cert-manager-deployment.yaml index 1d27a2a5f3..9974c4c1cd 100644 --- a/charts/consul/templates/webhook-cert-manager-deployment.yaml +++ b/charts/consul/templates/webhook-cert-manager-deployment.yaml @@ -60,4 +60,9 @@ spec: - name: config configMap: name: {{ template "consul.fullname" . }}-webhook-cert-manager-config + {{- if .Values.webhookCertManager.tolerations }} + tolerations: + {{ tpl .Values.webhookCertManager.tolerations . | indent 8 | trim }} + {{- end}} + {{- end }} diff --git a/charts/consul/test/acceptance/framework/helpers/helpers.go b/charts/consul/test/acceptance/framework/helpers/helpers.go deleted file mode 100644 index df8ae218a3..0000000000 --- a/charts/consul/test/acceptance/framework/helpers/helpers.go +++ /dev/null @@ -1,146 +0,0 @@ -package helpers - -import ( - "context" - "fmt" - "os" - "os/signal" - "strings" - "syscall" - "testing" - "time" - - terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" - "github.com/gruntwork-io/terratest/modules/random" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" - "github.com/hashicorp/consul/sdk/testutil/retry" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -// RandomName generates a random string with a 'test-' prefix. -func RandomName() string { - return fmt.Sprintf("test-%s", strings.ToLower(random.UniqueId())) -} - -// WaitForAllPodsToBeReady waits until all pods with the provided podLabelSelector -// are in the ready status. It checks every 5 seconds for a total of 20 tries. -// If there is at least one container in a pod that isn't ready after that, -// it fails the test. -func WaitForAllPodsToBeReady(t *testing.T, client kubernetes.Interface, namespace, podLabelSelector string) { - t.Helper() - - logger.Log(t, "Waiting for pods to be ready.") - - // Wait up to 15m. - // On Azure, volume provisioning can sometimes take close to 5 min, - // so we need to give a bit more time for pods to become healthy. - counter := &retry.Counter{Count: 180, Wait: 1 * time.Second} - retry.RunWith(counter, t, func(r *retry.R) { - pods, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: podLabelSelector}) - require.NoError(r, err) - - var notReadyPods []string - for _, pod := range pods.Items { - if !IsReady(pod) { - notReadyPods = append(notReadyPods, pod.Name) - } - } - if len(notReadyPods) > 0 { - r.Errorf("%d pods are not ready: %s", len(notReadyPods), strings.Join(notReadyPods, ",")) - } - }) -} - -// Sets up a goroutine that will wait for interrupt signals -// and call cleanup function when it catches it. -func SetupInterruptHandler(cleanup func()) { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { - <-c - fmt.Println("\r- Ctrl+C pressed in Terminal. Cleaning up resources.") - cleanup() - os.Exit(1) - }() -} - -// Cleanup will both register a cleanup function with t -// and SetupInterruptHandler to make sure resources get cleaned up -// if an interrupt signal is caught. -func Cleanup(t *testing.T, noCleanupOnFailure bool, cleanup func()) { - t.Helper() - - // Always clean up when an interrupt signal is caught. - SetupInterruptHandler(cleanup) - - // If noCleanupOnFailure is set, don't clean up resources if tests fail. - // We need to wrap the cleanup function because t that is passed in to this function - // might not have the information on whether the test has failed yet. - wrappedCleanupFunc := func() { - if !(noCleanupOnFailure && t.Failed()) { - logger.Logf(t, "cleaning up resources for %s", t.Name()) - cleanup() - } else { - logger.Log(t, "skipping resource cleanup") - } - } - - t.Cleanup(wrappedCleanupFunc) -} - -// KubernetesClientFromOptions takes KubectlOptions and returns Kubernetes API client. -func KubernetesClientFromOptions(t *testing.T, options *terratestk8s.KubectlOptions) kubernetes.Interface { - configPath, err := options.GetConfigPath(t) - require.NoError(t, err) - - config, err := terratestk8s.LoadApiClientConfigE(configPath, options.ContextName) - require.NoError(t, err) - - client, err := kubernetes.NewForConfig(config) - require.NoError(t, err) - - return client -} - -// KubernetesContextFromOptions returns the Kubernetes context from options. -// If context is explicitly set in options, it returns that context. -// Otherwise, it returns the current context. -func KubernetesContextFromOptions(t *testing.T, options *terratestk8s.KubectlOptions) string { - t.Helper() - - // First, check if context set in options and return that - if options.ContextName != "" { - return options.ContextName - } - - // Otherwise, get current context from config - configPath, err := options.GetConfigPath(t) - require.NoError(t, err) - - rawConfig, err := terratestk8s.LoadConfigFromPath(configPath).RawConfig() - require.NoError(t, err) - - return rawConfig.CurrentContext -} - -// IsReady returns true if pod is ready. -func IsReady(pod corev1.Pod) bool { - if pod.Status.Phase == corev1.PodPending { - return false - } - - for _, cond := range pod.Status.Conditions { - if cond.Type == corev1.PodReady { - if cond.Status == corev1.ConditionTrue { - return true - } else { - return false - } - } - } - - return false -} diff --git a/charts/consul/test/acceptance/go.mod b/charts/consul/test/acceptance/go.mod deleted file mode 100644 index 08fe978f06..0000000000 --- a/charts/consul/test/acceptance/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module github.com/hashicorp/consul-k8s/charts/consul/test/acceptance - -go 1.14 - -require ( - github.com/gruntwork-io/terratest v0.31.2 - github.com/hashicorp/consul/api v1.9.0 - github.com/hashicorp/consul/sdk v0.8.0 - github.com/stretchr/testify v1.5.1 - gopkg.in/yaml.v2 v2.2.8 - k8s.io/api v0.19.3 - k8s.io/apimachinery v0.19.3 - k8s.io/client-go v0.19.3 -) diff --git a/charts/consul/test/acceptance/tests/connect/connect_inject_test.go b/charts/consul/test/acceptance/tests/connect/connect_inject_test.go deleted file mode 100644 index 93981fd2cc..0000000000 --- a/charts/consul/test/acceptance/tests/connect/connect_inject_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package connect - -import ( - "context" - "fmt" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/consul" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/helpers" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/k8s" - "github.com/hashicorp/consul-k8s/charts/consul/test/acceptance/framework/logger" - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/sdk/testutil/retry" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const staticClientName = "static-client" -const staticServerName = "static-server" - -// Test that Connect works in a default and a secure installation. -func TestConnectInject(t *testing.T) { - cases := []struct { - secure bool - autoEncrypt bool - }{ - {false, false}, - {true, false}, - {true, true}, - } - - for _, c := range cases { - name := fmt.Sprintf("secure: %t; auto-encrypt: %t", c.secure, c.autoEncrypt) - t.Run(name, func(t *testing.T) { - cfg := suite.Config() - ctx := suite.Environment().DefaultContext(t) - - helmValues := map[string]string{ - "connectInject.enabled": "true", - - "global.tls.enabled": strconv.FormatBool(c.secure), - "global.tls.enableAutoEncrypt": strconv.FormatBool(c.autoEncrypt), - "global.acls.manageSystemACLs": strconv.FormatBool(c.secure), - } - - releaseName := helpers.RandomName() - consulCluster := consul.NewHelmCluster(t, helmValues, ctx, cfg, releaseName) - - consulCluster.Create(t) - - consulClient := consulCluster.SetupConsulClient(t, c.secure) - - // Check that the ACL token is deleted. - if c.secure { - // We need to register the cleanup function before we create the deployments - // because golang will execute them in reverse order i.e. the last registered - // cleanup function will be executed first. - t.Cleanup(func() { - retry.Run(t, func(r *retry.R) { - tokens, _, err := consulClient.ACL().TokenList(nil) - require.NoError(r, err) - for _, token := range tokens { - require.NotContains(r, token.Description, staticServerName) - require.NotContains(r, token.Description, staticClientName) - } - }) - }) - } - - logger.Log(t, "creating static-server and static-client deployments") - k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") - if cfg.EnableTransparentProxy { - k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") - } else { - k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") - } - - // Check that both static-server and static-client have been injected and now have 2 containers. - for _, labelSelector := range []string{"app=static-server", "app=static-client"} { - podList, err := ctx.KubernetesClient(t).CoreV1().Pods(ctx.KubectlOptions(t).Namespace).List(context.Background(), metav1.ListOptions{ - LabelSelector: labelSelector, - }) - require.NoError(t, err) - require.Len(t, podList.Items, 1) - require.Len(t, podList.Items[0].Spec.Containers, 2) - } - - if c.secure { - logger.Log(t, "checking that the connection is not successful because there's no intention") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionFailing(t, ctx.KubectlOptions(t), "http://static-server") - } else { - k8s.CheckStaticServerConnectionFailing(t, ctx.KubectlOptions(t), "http://localhost:1234") - } - - logger.Log(t, "creating intention") - _, err := consulClient.Connect().IntentionUpsert(&api.Intention{ - SourceName: staticClientName, - DestinationName: staticServerName, - Action: api.IntentionActionAllow, - }, nil) - require.NoError(t, err) - } - - logger.Log(t, "checking that connection is successful") - if cfg.EnableTransparentProxy { - // todo: add an assertion that the traffic is going through the proxy - k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://static-server") - } else { - k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://localhost:1234") - } - - // Test that kubernetes readiness status is synced to Consul. - // Create the file so that the readiness probe of the static-server pod fails. - logger.Log(t, "testing k8s -> consul health checks sync by making the static-server unhealthy") - k8s.RunKubectl(t, ctx.KubectlOptions(t), "exec", "deploy/"+staticServerName, "--", "touch", "/tmp/unhealthy") - - // The readiness probe should take a moment to be reflected in Consul, CheckStaticServerConnection will retry - // until Consul marks the service instance unavailable for mesh traffic, causing the connection to fail. - // We are expecting a "connection reset by peer" error because in a case of health checks, - // there will be no healthy proxy host to connect to. That's why we can't assert that we receive an empty reply - // from server, which is the case when a connection is unsuccessful due to intentions in other tests. - logger.Log(t, "checking that connection is unsuccessful") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionMultipleFailureMessages(t, ctx.KubectlOptions(t), false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server port 80: Connection refused"}, "http://static-server") - } else { - k8s.CheckStaticServerConnectionMultipleFailureMessages(t, ctx.KubectlOptions(t), false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server"}, "http://localhost:1234") - } - - }) - } -} - -// Test the endpoints controller cleans up force-killed pods. -func TestConnectInject_CleanupKilledPods(t *testing.T) { - cases := []struct { - secure bool - autoEncrypt bool - }{ - {false, false}, - {true, false}, - {true, true}, - } - - for _, c := range cases { - name := fmt.Sprintf("secure: %t; auto-encrypt: %t", c.secure, c.autoEncrypt) - t.Run(name, func(t *testing.T) { - cfg := suite.Config() - ctx := suite.Environment().DefaultContext(t) - - helmValues := map[string]string{ - "connectInject.enabled": "true", - "global.tls.enabled": strconv.FormatBool(c.secure), - "global.tls.enableAutoEncrypt": strconv.FormatBool(c.autoEncrypt), - "global.acls.manageSystemACLs": strconv.FormatBool(c.secure), - } - - releaseName := helpers.RandomName() - consulCluster := consul.NewHelmCluster(t, helmValues, ctx, cfg, releaseName) - - consulCluster.Create(t) - - logger.Log(t, "creating static-client deployment") - k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") - - logger.Log(t, "waiting for static-client to be registered with Consul") - consulClient := consulCluster.SetupConsulClient(t, c.secure) - retry.Run(t, func(r *retry.R) { - for _, name := range []string{"static-client", "static-client-sidecar-proxy"} { - instances, _, err := consulClient.Catalog().Service(name, "", nil) - r.Check(err) - - if len(instances) != 1 { - r.Errorf("expected 1 instance of %s", name) - } - } - }) - - ns := ctx.KubectlOptions(t).Namespace - pods, err := ctx.KubernetesClient(t).CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{LabelSelector: "app=static-client"}) - require.NoError(t, err) - require.Len(t, pods.Items, 1) - podName := pods.Items[0].Name - - logger.Logf(t, "force killing the static-client pod %q", podName) - var gracePeriod int64 = 0 - err = ctx.KubernetesClient(t).CoreV1().Pods(ns).Delete(context.Background(), podName, metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod}) - require.NoError(t, err) - - logger.Log(t, "ensuring pod is deregistered") - retry.Run(t, func(r *retry.R) { - for _, name := range []string{"static-client", "static-client-sidecar-proxy"} { - instances, _, err := consulClient.Catalog().Service(name, "", nil) - r.Check(err) - - for _, instance := range instances { - if strings.Contains(instance.ServiceID, podName) { - r.Errorf("%s is still registered", instance.ServiceID) - } - } - } - }) - }) - } -} - -// Test that when Consul clients are restarted and lose all their registrations, -// the services get re-registered and can continue to talk to each other. -func TestConnectInject_RestartConsulClients(t *testing.T) { - cfg := suite.Config() - if cfg.EnableTransparentProxy { - t.Skip("skipping this test because it's currently flakey when transparent proxy is enabled") - } - ctx := suite.Environment().DefaultContext(t) - - helmValues := map[string]string{ - "connectInject.enabled": "true", - } - - releaseName := helpers.RandomName() - consulCluster := consul.NewHelmCluster(t, helmValues, ctx, cfg, releaseName) - - consulCluster.Create(t) - - logger.Log(t, "creating static-server and static-client deployments") - k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject") - if cfg.EnableTransparentProxy { - k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy") - } else { - k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject") - } - - logger.Log(t, "checking that connection is successful") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://static-server") - } else { - k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://localhost:1234") - } - - logger.Log(t, "restarting Consul client daemonset") - k8s.RunKubectl(t, ctx.KubectlOptions(t), "rollout", "restart", fmt.Sprintf("ds/%s-consul", releaseName)) - k8s.RunKubectl(t, ctx.KubectlOptions(t), "rollout", "status", fmt.Sprintf("ds/%s-consul", releaseName)) - - logger.Log(t, "checking that connection is still successful") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://static-server") - } else { - k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://localhost:1234") - } -} diff --git a/charts/consul/test/terraform/aks/main.tf b/charts/consul/test/terraform/aks/main.tf index 77df4bcd05..e12ad64cf9 100644 --- a/charts/consul/test/terraform/aks/main.tf +++ b/charts/consul/test/terraform/aks/main.tf @@ -1,5 +1,5 @@ provider "azurerm" { - version = "~> 2.0" + version = "2.90.0" features {} } @@ -18,19 +18,58 @@ resource "azurerm_resource_group" "default" { tags = var.tags } +resource "azurerm_virtual_network" "default" { + count = var.cluster_count + name = "consul-k8s-${random_id.suffix[count.index].dec}" + location = azurerm_resource_group.default[count.index].location + resource_group_name = azurerm_resource_group.default[count.index].name + address_space = ["192.${count.index + 168}.0.0/16"] + + subnet { + name = "consul-k8s-${random_id.suffix[count.index].dec}-subnet" + address_prefix = "192.${count.index + 168}.1.0/24" + } +} + +resource "azurerm_virtual_network_peering" "default" { + count = var.cluster_count > 1 ? var.cluster_count : 0 + name = "peering-${count.index}" + resource_group_name = azurerm_resource_group.default[count.index].name + virtual_network_name = azurerm_virtual_network.default[count.index].name + remote_virtual_network_id = azurerm_virtual_network.default[count.index == 0 ? 1 : 0].id +} + resource "azurerm_kubernetes_cluster" "default" { count = var.cluster_count name = "consul-k8s-${random_id.suffix[count.index].dec}" location = azurerm_resource_group.default[count.index].location resource_group_name = azurerm_resource_group.default[count.index].name dns_prefix = "consul-k8s-${random_id.suffix[count.index].dec}" - kubernetes_version = "1.19.9" + kubernetes_version = "1.21.7" + + // We're setting the network plugin and other network properties explicitly + // here even though they are the same as defaults to ensure that none of these CIDRs + // overlap with our vnet and subnet. Please see + // https://docs.microsoft.com/en-us/azure/aks/configure-kubenet#create-an-aks-cluster-in-the-virtual-network. + // We want to use kubenet plugin rather than Azure CNI because pods + // using kubenet will not be routable when we peer VNets, + // and that gives us more confidence that in any tests where cross-cluster + // communication is tested, the connections goes through the appropriate gateway + // rather than directly from pod to pod. + network_profile { + network_plugin = "kubenet" + service_cidr = "10.0.0.0/16" + dns_service_ip = "10.0.0.10" + pod_cidr = "10.244.0.0/16" + docker_bridge_cidr = "172.17.0.1/16" + } default_node_pool { name = "default" node_count = 3 vm_size = "Standard_D2_v2" os_disk_size_gb = 30 + vnet_subnet_id = azurerm_virtual_network.default[count.index].subnet.*.id[0] } service_principal { diff --git a/charts/consul/test/terraform/aks/outputs.tf b/charts/consul/test/terraform/aks/outputs.tf index 666fa81394..9ba75d10f8 100644 --- a/charts/consul/test/terraform/aks/outputs.tf +++ b/charts/consul/test/terraform/aks/outputs.tf @@ -1,3 +1,3 @@ output "kubeconfigs" { value = local_file.kubeconfigs.*.filename -} +} \ No newline at end of file diff --git a/charts/consul/test/terraform/aks/variables.tf b/charts/consul/test/terraform/aks/variables.tf index 6c06405bfe..1651ce7b09 100644 --- a/charts/consul/test/terraform/aks/variables.tf +++ b/charts/consul/test/terraform/aks/variables.tf @@ -16,10 +16,18 @@ variable "client_secret" { variable "cluster_count" { default = 1 description = "The number of Kubernetes clusters to create." + + // We currently cannot support more than 2 clusters + // because setting up peering is more complicated if cluster count is + // more than two. + validation { + condition = var.cluster_count < 3 && var.cluster_count > 0 + error_message = "The cluster_count value must be 1 or 2." + } } variable "tags" { - type = map - default = {} + type = map + default = {} description = "Tags to attach to the created resources." } diff --git a/charts/consul/test/terraform/eks/main.tf b/charts/consul/test/terraform/eks/main.tf index b16646af33..7b8160ea7d 100644 --- a/charts/consul/test/terraform/eks/main.tf +++ b/charts/consul/test/terraform/eks/main.tf @@ -15,6 +15,8 @@ resource "random_id" "suffix" { data "aws_availability_zones" "available" {} +data "aws_caller_identity" "caller" {} + resource "random_string" "suffix" { length = 8 special = false @@ -23,13 +25,14 @@ resource "random_string" "suffix" { module "vpc" { count = var.cluster_count source = "terraform-aws-modules/vpc/aws" - version = "2.47.0" + version = "3.11.0" - name = "consul-k8s-${random_id.suffix[count.index].dec}" - cidr = "10.0.0.0/16" + name = "consul-k8s-${random_id.suffix[count.index].dec}" + # The cidr range needs to be unique in each VPC to allow setting up a peering connection. + cidr = format("10.%s.0.0/16", count.index) azs = data.aws_availability_zones.available.names - private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] - public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] + private_subnets = [format("10.%s.1.0/24", count.index), format("10.%s.2.0/24", count.index), format("10.%s.3.0/24", count.index)] + public_subnets = [format("10.%s.4.0/24", count.index), format("10.%s.5.0/24", count.index), format("10.%s.6.0/24", count.index)] enable_nat_gateway = true single_nat_gateway = true enable_dns_hostnames = true @@ -51,10 +54,10 @@ module "eks" { count = var.cluster_count source = "terraform-aws-modules/eks/aws" - version = "12.2.0" + version = "17.20.0" cluster_name = "consul-k8s-${random_id.suffix[count.index].dec}" - cluster_version = "1.18" + cluster_version = "1.19" subnets = module.vpc[count.index].private_subnets vpc_id = module.vpc[count.index].vpc_id @@ -69,9 +72,9 @@ module "eks" { } } - manage_aws_auth = false - write_kubeconfig = true - config_output_path = pathexpand("~/.kube/consul-k8s-${random_id.suffix[count.index].dec}") + manage_aws_auth = false + write_kubeconfig = true + kubeconfig_output_path = pathexpand("~/.kube/consul-k8s-${random_id.suffix[count.index].dec}") tags = var.tags } @@ -84,4 +87,71 @@ data "aws_eks_cluster" "cluster" { data "aws_eks_cluster_auth" "cluster" { count = var.cluster_count name = module.eks[count.index].cluster_id +} + +# The following resources are only applied when cluster_count=2 to set up vpc peering and the appropriate routes and +# security groups so traffic between VPCs is allowed. There is validation to ensure cluster_count can be 1 or 2. + +# Each EKS cluster needs to allow ingress traffic from the other VPC. +resource "aws_security_group_rule" "allowingressfrom1-0" { + count = var.cluster_count > 1 ? 1 : 0 + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [module.vpc[1].vpc_cidr_block] + security_group_id = module.eks[0].cluster_primary_security_group_id +} + +resource "aws_security_group_rule" "allowingressfrom0-1" { + count = var.cluster_count > 1 ? 1 : 0 + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [module.vpc[0].vpc_cidr_block] + security_group_id = module.eks[1].cluster_primary_security_group_id +} + +# Create a peering connection. This is the requester's side of the connection. +resource "aws_vpc_peering_connection" "peer" { + count = var.cluster_count > 1 ? 1 : 0 + vpc_id = module.vpc[0].vpc_id + peer_vpc_id = module.vpc[1].vpc_id + peer_owner_id = data.aws_caller_identity.caller.account_id + peer_region = var.region + auto_accept = false + + tags = { + Side = "Requester" + } +} + +# Accepter's side of the vpc peering connection. +resource "aws_vpc_peering_connection_accepter" "peer" { + count = var.cluster_count > 1 ? 1 : 0 + vpc_peering_connection_id = aws_vpc_peering_connection.peer[0].id + auto_accept = true + + tags = { + Side = "Accepter" + } +} + +# Add routes that so traffic going from VPC 0 to VPC 1 is routed through the peering connection. +resource "aws_route" "peering0" { + # We have 2 route tables to add a route to, the public and private route tables. + count = var.cluster_count > 1 ? 2 : 0 + route_table_id = [module.vpc[0].public_route_table_ids[0], module.vpc[0].private_route_table_ids[0]][count.index] + destination_cidr_block = module.vpc[1].vpc_cidr_block + vpc_peering_connection_id = aws_vpc_peering_connection.peer[0].id +} + +# Add routes that so traffic going from VPC 1 to VPC 0 is routed through the peering connection. +resource "aws_route" "peering1" { + # We have 2 route tables to add a route to, the public and private route tables. + count = var.cluster_count > 1 ? 2 : 0 + route_table_id = [module.vpc[1].public_route_table_ids[0], module.vpc[1].private_route_table_ids[0]][count.index] + destination_cidr_block = module.vpc[0].vpc_cidr_block + vpc_peering_connection_id = aws_vpc_peering_connection.peer[0].id } \ No newline at end of file diff --git a/charts/consul/test/terraform/eks/variables.tf b/charts/consul/test/terraform/eks/variables.tf index a401312a45..361a5f5c45 100644 --- a/charts/consul/test/terraform/eks/variables.tf +++ b/charts/consul/test/terraform/eks/variables.tf @@ -6,6 +6,13 @@ variable "region" { variable "cluster_count" { default = 1 description = "The number of Kubernetes clusters to create." + // We currently cannot support more than 2 clusters + // because setting up peering is more complicated if cluster count is + // more than two. + validation { + condition = var.cluster_count < 3 && var.cluster_count > 0 + error_message = "The cluster_count value must be 1 or 2." + } } variable "role_arn" { @@ -14,7 +21,7 @@ variable "role_arn" { } variable "tags" { - type = map - default = {} + type = map + default = {} description = "Tags to attach to the created resources." } diff --git a/charts/consul/test/terraform/gke/main.tf b/charts/consul/test/terraform/gke/main.tf index a6c294e2e6..7f303639ad 100644 --- a/charts/consul/test/terraform/gke/main.tf +++ b/charts/consul/test/terraform/gke/main.tf @@ -10,7 +10,7 @@ resource "random_id" "suffix" { data "google_container_engine_versions" "main" { location = var.zone - version_prefix = "1.17." + version_prefix = "1.20." } resource "google_container_cluster" "cluster" { @@ -24,7 +24,10 @@ resource "google_container_cluster" "cluster" { location = var.zone min_master_version = data.google_container_engine_versions.main.latest_master_version node_version = data.google_container_engine_versions.main.latest_master_version - + node_config { + tags = ["consul-k8s-${random_id.suffix[count.index].dec}"] + machine_type = "e2-standard-4" + } pod_security_policy_config { enabled = true } @@ -32,6 +35,23 @@ resource "google_container_cluster" "cluster" { resource_labels = var.labels } +resource "google_compute_firewall" "firewall-rules" { + project = var.project + name = "consul-k8s-acceptance-firewall-${random_id.suffix[count.index].dec}" + network = "default" + description = "Creates firewall rule allowing traffic from nodes and pods of the ${random_id.suffix[count.index == 0 ? 1 : 0].dec} Kubernetes cluster." + + count = var.cluster_count > 1 ? var.cluster_count : 0 + + allow { + protocol = "all" + } + + source_ranges = [google_container_cluster.cluster[count.index == 0 ? 1 : 0].cluster_ipv4_cidr] + source_tags = ["consul-k8s-${random_id.suffix[count.index == 0 ? 1 : 0].dec}"] + target_tags = ["consul-k8s-${random_id.suffix[count.index].dec}"] +} + resource "null_resource" "kubectl" { count = var.init_cli ? var.cluster_count : 0 diff --git a/charts/consul/test/terraform/gke/variables.tf b/charts/consul/test/terraform/gke/variables.tf index 28defc2526..04d214cedb 100644 --- a/charts/consul/test/terraform/gke/variables.tf +++ b/charts/consul/test/terraform/gke/variables.tf @@ -19,10 +19,18 @@ variable "init_cli" { variable "cluster_count" { default = 1 description = "The number of Kubernetes clusters to create." + + // We currently cannot support more than 2 cluster + // because setting up peering is more complicated if cluster count is + // more than two. + validation { + condition = var.cluster_count < 3 && var.cluster_count > 0 + error_message = "The cluster_count value must be 1 or 2." + } } variable "labels" { - type = map - default = {} + type = map + default = {} description = "Labels to attach to the created resources." } diff --git a/charts/consul/test/unit/api-gateway-controller-clusterrole.bats b/charts/consul/test/unit/api-gateway-controller-clusterrole.bats new file mode 100644 index 0000000000..a3edec027d --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-clusterrole.bats @@ -0,0 +1,21 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/ClusterRole: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-clusterrole.yaml \ + . +} + +@test "apiGateway/ClusterRole: enabled with apiGateway.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-clusterrole.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/api-gateway-controller-clusterrolebinding.bats b/charts/consul/test/unit/api-gateway-controller-clusterrolebinding.bats new file mode 100644 index 0000000000..3dfd94c36f --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-clusterrolebinding.bats @@ -0,0 +1,22 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/ClusterRoleBinding: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-clusterrolebinding.yaml \ + . +} + +@test "apiGateway/ClusterRoleBinding: enabled with global.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-clusterrolebinding.yaml \ + --set 'global.enabled=false' \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq -s 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/api-gateway-controller-deployment.bats b/charts/consul/test/unit/api-gateway-controller-deployment.bats new file mode 100755 index 0000000000..6810c5dde0 --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-deployment.bats @@ -0,0 +1,511 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/Deployment: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + . +} + +@test "apiGateway/Deployment: fails if no image is set" { + cd `chart_dir` + run helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "apiGateway.image must be set to enable api gateway" ]] +} + +@test "apiGateway/Deployment: disable with apiGateway.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=false' \ + . +} + +@test "apiGateway/Deployment: disable with global.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'global.enabled=false' \ + . +} + +@test "apiGateway/Deployment: enable namespaces" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=bar' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | join(" ") | contains("-consul-destination-namespace=default")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: enable namespace mirroring" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=bar' \ + --set 'global.enableConsulNamespaces=true' \ + --set 'apiGateway.consulNamespaces.mirroringK8S=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | join(" ") | contains("-mirroring-k8s=true")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: enable namespace mirroring prefixes" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=bar' \ + --set 'global.enableConsulNamespaces=true' \ + --set 'apiGateway.consulNamespaces.mirroringK8S=true' \ + --set 'apiGateway.consulNamespaces.mirroringK8SPrefix=foo' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | join(" ") | contains("-mirroring-k8s-prefix=foo")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: container image overrides" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=bar' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].image' | tee /dev/stderr) + [ "${actual}" = "\"bar\"" ] +} + +@test "apiGateway/Deployment: SDS host set correctly" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=bar' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | join(" ") | contains("-sds-server-host RELEASE-NAME-consul-api-gateway-controller.default.svc")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# nodeSelector + +@test "apiGateway/Deployment: nodeSelector is not set by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.spec.template.spec.nodeSelector' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "apiGateway/Deployment: specified nodeSelector" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.controller.nodeSelector=testing' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.nodeSelector' | tee /dev/stderr) + [ "${actual}" = "testing" ] +} + +#-------------------------------------------------------------------- +# global.tls.enabled + +@test "apiGateway/Deployment: Adds tls-ca-cert volume when global.tls.enabled is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name == "consul-ca-cert")' | tee /dev/stderr) + [ "${actual}" != "" ] +} + +@test "apiGateway/Deployment: Adds tls-ca-cert volumeMounts when global.tls.enabled is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-ca-cert")' | tee /dev/stderr) + [ "${actual}" != "" ] +} + +@test "apiGateway/Deployment: can overwrite CA secret with the provided one" { + cd `chart_dir` + local ca_cert_volume=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.caCert.secretName=foo-ca-cert' \ + --set 'global.tls.caCert.secretKey=key' \ + --set 'global.tls.caKey.secretName=foo-ca-key' \ + --set 'global.tls.caKey.secretKey=key' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name=="consul-ca-cert")' | tee /dev/stderr) + + # check that the provided ca cert secret is attached as a volume + local actual + actual=$(echo $ca_cert_volume | jq -r '.secret.secretName' | tee /dev/stderr) + [ "${actual}" = "foo-ca-cert" ] + + # check that the volume uses the provided secret key + actual=$(echo $ca_cert_volume | jq -r '.secret.items[0].key' | tee /dev/stderr) + [ "${actual}" = "key" ] +} + +#-------------------------------------------------------------------- +# global.tls.enableAutoEncrypt + +@test "apiGateway/Deployment: consul-auto-encrypt-ca-cert volume is added when TLS with auto-encrypt is enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name == "consul-auto-encrypt-ca-cert") | length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: consul-auto-encrypt-ca-cert volumeMount is added when TLS with auto-encrypt is enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-auto-encrypt-ca-cert") | length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: get-auto-encrypt-client-ca init container is created when TLS with auto-encrypt is enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.initContainers[] | select(.name == "get-auto-encrypt-client-ca") | length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: adds both init containers when TLS with auto-encrypt and ACLs + namespaces are enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.enableConsulNamespaces=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.initContainers | length == 2' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: consul-ca-cert volume is not added if externalServers.enabled=true and externalServers.useSystemRoots=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo.com' \ + --set 'externalServers.useSystemRoots=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name == "consul-ca-cert")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +#-------------------------------------------------------------------- +# global.acls.manageSystemACLs + +@test "apiGateway/Deployment: CONSUL_HTTP_TOKEN env variable created when global.acls.manageSystemACLs=true" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.acls.manageSystemACLs=true' \ + . | tee /dev/stderr | + yq '[.spec.template.spec.containers[0].env[].name] ' | tee /dev/stderr) + + local actual=$(echo $object | + yq 'any(contains("CONSUL_HTTP_TOKEN"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | + yq 'map(select(test("CONSUL_HTTP_TOKEN"))) | length' | tee /dev/stderr) + [ "${actual}" = "1" ] +} + +@test "apiGateway/Deployment: init container is created when global.acls.manageSystemACLs=true" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.acls.manageSystemACLs=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.initContainers[0]' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.name' | tee /dev/stderr) + [ "${actual}" = "api-gateway-controller-acl-init" ] + + local actual=$(echo $object | + yq -r '.command | any(contains("consul-k8s-control-plane acl-init"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# priorityClassName + +@test "apiGateway/Deployment: no priorityClassName by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.priorityClassName' | tee /dev/stderr) + + [ "${actual}" = "null" ] +} + +@test "apiGateway/Deployment: can set a priorityClassName" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.controller.priorityClassName=name' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.priorityClassName' | tee /dev/stderr) + + [ "${actual}" = "name" ] +} + +#-------------------------------------------------------------------- +# logLevel + +@test "apiGateway/Deployment: logLevel info by default from global" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo "$cmd" | + yq 'any(contains("-log-level info"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: logLevel can be overridden" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.logLevel=debug' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo "$cmd" | + yq 'any(contains("-log-level debug"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# replicas + +@test "apiGateway/Deployment: replicas defaults to 1" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.spec.replicas' | tee /dev/stderr) + + [ "${actual}" = "1" ] +} + +@test "apiGateway/Deployment: replicas can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.controller.replicas=3' \ + . | tee /dev/stderr | + yq '.spec.replicas' | tee /dev/stderr) + + [ "${actual}" = "3" ] +} + +#-------------------------------------------------------------------- +# Vault + +@test "apiGateway/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "apiGateway/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "apiGateway/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "apiGateway/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "apiGateway/Deployment: vault tls annotations are set when tls is enabled" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=bar' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/cert/ca\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/cert/ca" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr)" + [ "${actual}" = "test" ] +} diff --git a/charts/consul/test/unit/api-gateway-controller-service.bats b/charts/consul/test/unit/api-gateway-controller-service.bats new file mode 100755 index 0000000000..47cb7ff9aa --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-service.bats @@ -0,0 +1,30 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/Service: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-service.yaml \ + . +} + +@test "apiGateway/Service: enable with apiGateway.enabled set to true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-service.yaml \ + --set 'global.enabled=false' \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Service: disable with apiGateway.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-service.yaml \ + --set 'apiGateway.enabled=false' \ + . +} diff --git a/charts/consul/test/unit/api-gateway-controller-serviceaccount.bats b/charts/consul/test/unit/api-gateway-controller-serviceaccount.bats new file mode 100644 index 0000000000..22486799b2 --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-serviceaccount.bats @@ -0,0 +1,76 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/ServiceAccount: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + . +} + +@test "apiGateway/ServiceAccount: enabled with apiGateway.enabled true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq -s 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/ServiceAccount: disabled with apiGateway.enabled false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=false' \ + . +} +#-------------------------------------------------------------------- +# global.imagePullSecrets + +@test "apiGateway/ServiceAccount: can set image pull secrets" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.imagePullSecrets[0].name=my-secret' \ + --set 'global.imagePullSecrets[1].name=my-secret2' \ + . | tee /dev/stderr) + + local actual=$(echo "$object" | + yq -r '.imagePullSecrets[0].name' | tee /dev/stderr) + [ "${actual}" = "my-secret" ] + + local actual=$(echo "$object" | + yq -r '.imagePullSecrets[1].name' | tee /dev/stderr) + [ "${actual}" = "my-secret2" ] +} + +#-------------------------------------------------------------------- +# apiGateway.serviceAccount.annotations + +@test "apiGateway/ServiceAccount: no annotations by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.metadata.annotations | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "apiGateway/ServiceAccount: annotations when enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set "apiGateway.serviceAccount.annotations=foo: bar" \ + . | tee /dev/stderr | + yq -r '.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} diff --git a/charts/consul/test/unit/api-gateway-gatewayclass.bats b/charts/consul/test/unit/api-gateway-gatewayclass.bats new file mode 100755 index 0000000000..c79753c2f3 --- /dev/null +++ b/charts/consul/test/unit/api-gateway-gatewayclass.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/GatewayClass: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + . +} + +@test "apiGateway/GatewayClass: enable with global.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'global.enabled=false' \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/GatewayClass: disable with apiGateway.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'apiGateway.enabled=false' \ + . +} + +@test "apiGateway/GatewayClass: disable with global.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'global.enabled=false' \ + . +} + +@test "apiGateway/GatewayClass: disable with apiGateway.managedGatewayClass.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.managedGatewayClass.enabled=false' \ + . +} diff --git a/charts/consul/test/unit/client-daemonset.bats b/charts/consul/test/unit/client-daemonset.bats index d1a0e0614c..eb1c2b5048 100755 --- a/charts/consul/test/unit/client-daemonset.bats +++ b/charts/consul/test/unit/client-daemonset.bats @@ -543,33 +543,33 @@ load _helpers #-------------------------------------------------------------------- # config-configmap -@test "client/DaemonSet: adds config-checksum annotation when extraConfig is blank" { +@test "client/DaemonSet: config-checksum annotation when extraConfig is blank" { cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ . | tee /dev/stderr | yq -r '.spec.template.metadata.annotations."consul.hashicorp.com/config-checksum"' | tee /dev/stderr) - [ "${actual}" = 779a0e24c2ed561c727730698a75b1c552f562c100f0c3315ff2cb925f5e296b ] + [ "${actual}" = 004aa147bf69db24da4d7f61ee4e3fc725dcb04effcec707a66dab1ae91543cc ] } -@test "client/DaemonSet: adds config-checksum annotation when extraConfig is provided" { +@test "client/DaemonSet: config-checksum annotation changes when extraConfig is provided" { cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ --set 'client.extraConfig="{\"hello\": \"world\"}"' \ . | tee /dev/stderr | yq -r '.spec.template.metadata.annotations."consul.hashicorp.com/config-checksum"' | tee /dev/stderr) - [ "${actual}" = ba1ceb79d2d18e136d3cc40a9dfddcf2a252aa19ca1703bee3219ca28f1ee187 ] + [ "${actual}" = 6ab8217573bf5486889ff6d3fe8d2f70a0a1d0bfbb48c20f568a4fc566cb3909 ] } -@test "client/DaemonSet: adds config-checksum annotation when client config is updated" { +@test "client/DaemonSet: config-checksum annotation changes when connectInject.enabled=true" { cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ --set 'connectInject.enabled=true' \ . | tee /dev/stderr | yq -r '.spec.template.metadata.annotations."consul.hashicorp.com/config-checksum"' | tee /dev/stderr) - [ "${actual}" = 8496f6bcdec460eac8a5c890e7899f5757111e13e54808af533aaf205ef18bd0 ] + [ "${actual}" = b0be8c9b3ae8692a4e393b93976c55988e95cb9d9dae96fbd8626f3f5b6c404b ] } #-------------------------------------------------------------------- @@ -602,10 +602,31 @@ load _helpers local actual=$(helm template \ -s templates/client-daemonset.yaml \ . | tee /dev/stderr | - yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY") | length > 0' | tee /dev/stderr) + yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY")' | tee /dev/stderr) [ "${actual}" = "" ] } +@test "client/DaemonSet: gossip encryption autogeneration properly sets secretName and secretKey" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY") | .valueFrom.secretKeyRef | [.name=="RELEASE-NAME-consul-gossip-encryption-key", .key="key"] | all' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "client/DaemonSet: gossip encryption key is passed in via the -encrypt flag" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[] | select(.name=="consul") | .command | any(contains("-encrypt=\"${GOSSIP_KEY}\""))' \ + | tee /dev/stderr) + [ "${actual}" = "true" ] +} + @test "client/DaemonSet: gossip encryption disabled in client DaemonSet when secretName is missing" { cd `chart_dir` local actual=$(helm template \ @@ -1149,6 +1170,28 @@ load _helpers [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# DNS + +@test "client/DaemonSet: recursor flags is not set by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("$recursor_flags")' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "client/DaemonSet: add recursor flags if dns.enableRedirection is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'dns.enableRedirection=true' \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("$recursor_flags")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + #-------------------------------------------------------------------- # hostNetwork @@ -1306,8 +1349,8 @@ rollingUpdate: cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) [ "${actual}" = '{"name":"consul-license","secret":{"secretName":"foo"}}' ] @@ -1317,8 +1360,8 @@ rollingUpdate: cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) [ "${actual}" = '{"name":"consul-license","mountPath":"/consul/license","readOnly":true}' ] @@ -1328,8 +1371,8 @@ rollingUpdate: cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_LICENSE_PATH")' | tee /dev/stderr) [ "${actual}" = '{"name":"CONSUL_LICENSE_PATH","value":"/consul/license/bar"}' ] @@ -1339,8 +1382,8 @@ rollingUpdate: cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) @@ -1351,8 +1394,8 @@ rollingUpdate: cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) @@ -1363,14 +1406,35 @@ rollingUpdate: cd `chart_dir` local actual=$(helm template \ -s templates/client-daemonset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_LICENSE_PATH")' | tee /dev/stderr) [ "${actual}" = "" ] } +@test "client/DaemonSet: when global.enterpriseLicense.secretKey!=null and global.enterpriseLicense.secretName=null, fail" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.enterpriseLicense.secretName=' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "enterpriseLicense.secretKey and secretName must both be specified." ]] +} + +@test "client/DaemonSet: when global.enterpriseLicense.secretName!=null and global.enterpriseLicense.secretKey=null, fail" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "enterpriseLicense.secretKey and secretName must both be specified." ]] +} #-------------------------------------------------------------------- # recursors @@ -1383,3 +1447,523 @@ rollingUpdate: yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("-recursor=\"1.2.3.4\"")' | tee /dev/stderr) [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# partitions + +@test "client/DaemonSet: -partitions can be set by global.adminPartitions.enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.adminPartitions.enabled=true' \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("partition = \"default\"")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "client/DaemonSet: -partitions can be overridden by global.adminPartitions.name" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=test' \ + --set 'server.enabled=false' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=bar' \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("partition = \"test\"")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "client/DaemonSet: partition name has to be default in server cluster" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=test' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "global.adminPartitions.name has to be \"default\" in the server cluster" ]] +} + +@test "client/DaemonSet: federation and admin partitions cannot be enabled together" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.federation.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "If global.federation.enabled is true, global.adminPartitions.enabled must be false because they are mutually exclusive" ]] +} + +#-------------------------------------------------------------------- +# extraContainers + +@test "client/DaemonSet: extraContainers adds extra container" { + cd `chart_dir` + + # Test that it defines the extra container + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'client.extraContainers[0].image=test-image' \ + --set 'client.extraContainers[0].name=test-container' \ + --set 'client.extraContainers[0].ports[0].name=test-port' \ + --set 'client.extraContainers[0].ports[0].containerPort=9410' \ + --set 'client.extraContainers[0].ports[0].protocol=TCP' \ + --set 'client.extraContainers[0].env[0].name=TEST_ENV' \ + --set 'client.extraContainers[0].env[0].value=test_env_value' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[] | select(.name == "test-container")' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.name' | tee /dev/stderr) + [ "${actual}" = "test-container" ] + + local actual=$(echo $object | + yq -r '.image' | tee /dev/stderr) + [ "${actual}" = "test-image" ] + + local actual=$(echo $object | + yq -r '.ports[0].name' | tee /dev/stderr) + [ "${actual}" = "test-port" ] + + local actual=$(echo $object | + yq -r '.ports[0].containerPort' | tee /dev/stderr) + [ "${actual}" = "9410" ] + + local actual=$(echo $object | + yq -r '.ports[0].protocol' | tee /dev/stderr) + [ "${actual}" = "TCP" ] + + local actual=$(echo $object | + yq -r '.env[0].name' | tee /dev/stderr) + [ "${actual}" = "TEST_ENV" ] + + local actual=$(echo $object | + yq -r '.env[0].value' | tee /dev/stderr) + [ "${actual}" = "test_env_value" ] + +} + +@test "client/DaemonSet: extraContainers supports adding two containers" { + cd `chart_dir` + + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'client.extraContainers[0].image=test-image' \ + --set 'client.extraContainers[0].name=test-container' \ + --set 'client.extraContainers[1].image=test-image' \ + --set 'client.extraContainers[1].name=test-container-2' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers | length' | tee /dev/stderr) + + [ "${object}" = 3 ] + +} + +@test "client/DaemonSet: no extra client containers added by default" { + cd `chart_dir` + + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers | length' | tee /dev/stderr) + + [ "${object}" = 1 ] +} + +#-------------------------------------------------------------------- +# vault integration + +@test "client/DaemonSet: fail when vault is enabled but the consulClientRole is not provided" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.secretsBackend.vault.consulClientRole must be provided if global.secretsBackend.vault.enabled=true" ]] +} + +@test "client/DaemonSet: fail when vault, tls are enabled but no caCert provided" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.tls.enabled=true' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.tls.caCert.secretName must be provided if global.tls.enabled=true and global.secretsBackend.vault.enabled=true." ]] +} + +@test "client/DaemonSet: fail when vault, tls are enabled with a serverCert but no autoencrypt" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.tls.enableAutoEncrypt must be true if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" ]] +} + +@test "client/DaemonSet: fail when vault is enabled with tls but autoencrypt is disabled" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.server.serverCert.secretName=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.tls.enableAutoEncrypt must be true if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" ]] +} + +@test "client/DaemonSet: fail when vault is enabled with tls but no consulCARole is provided" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.server.serverCert.secretName=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.enabled=true' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.secretsBackend.vault.consulCARole must be provided if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" ]] +} + +@test "client/DaemonSet: vault annotations not set by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/role"] | length > 0 ' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "client/DaemonSet: vault annotations added when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + [ "${actual}" = "foo" ] +} + +@test "client/DaemonSet: vault gossip annotations are set when gossip encryption enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.gossipEncryption.secretName=path/to/secret' \ + --set 'global.gossipEncryption.secretKey=gossip' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-gossip.txt"]' | tee /dev/stderr) + [ "${actual}" = "path/to/secret" ] + local actual="$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-gossip.txt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"path/to/secret\" -}}\n{{- .Data.data.gossip -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "client/DaemonSet: GOSSIP_KEY env variable is not set and command defines GOSSIP_KEY when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.gossipEncryption.secretName=a/b/c/d' \ + --set 'global.gossipEncryption.secretKey=gossip' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec' | tee /dev/stderr) + + + local actual=$(echo $object | + yq -r '.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY")' | tee /dev/stderr) + [ "${actual}" = "" ] + + local actual=$(echo $object | + yq -r '.containers[] | select(.name=="consul") | .command | any(contains("GOSSIP_KEY="))' \ + | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "client/DaemonSet: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "client/DaemonSet: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "client/DaemonSet: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "client/DaemonSet: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "client/DaemonSet: vault tls annotations are set when tls is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual="$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/cert/ca" ] + + local actual="$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/cert/ca\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "client/DaemonSet: tls related volumes not attached and command is modified correctly when tls is enabled with vault" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=pki_int/ca/pem' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec' | tee /dev/stderr) + + + local actual=$(echo $object | + yq -r '.volumes[] | select(.name == "consul-ca-cert") | length > 0' | tee /dev/stderr) + [ "${actual}" = "" ] + + local actual=$(echo $object | + yq -r '.volumes[] | select(.name == "consul-ca-key") | length > 0' | tee /dev/stderr) + [ "${actual}" = "" ] + + local actual=$(echo $object | + yq -r '.containers[0].volumeMounts[] | select(.name == "consul-client-cert")' | tee /dev/stderr) + [ "${actual}" = "" ] + + local actual=$(echo $object | + yq -r '.containers[0].volumeMounts[] | select(.name == "consul-ca-key")' | tee /dev/stderr) + [ "${actual}" = "" ] + + local actual=$(echo $object | + yq -r '.containers[0].command | any(contains("ca_file = \"/vault/secrets/serverca.crt\""))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "client/DaemonSet: vault enterprise license annotations are correct when enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=path/to/secret' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-enterpriselicense.txt"]' | tee /dev/stderr) + [ "${actual}" = "path/to/secret" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-enterpriselicense.txt"]' | tee /dev/stderr) + local actual="$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-enterpriselicense.txt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"path/to/secret\" -}}\n{{- .Data.data.enterpriselicense -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "client/DaemonSet: vault CONSUL_LICENSE_PATH is set to /vault/secrets/enterpriselicense.txt" { + cd `chart_dir` + local env=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].env[]' | tee /dev/stderr) + + local actual + + local actual=$(echo $env | jq -r '. | select(.name == "CONSUL_LICENSE_PATH") | .value' | tee /dev/stderr) + [ "${actual}" = "/vault/secrets/enterpriselicense.txt" ] +} + +@test "client/DaemonSet: vault does not add volume for license secret" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +@test "client/DaemonSet: vault does not add volume mount for license secret" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "client/DaemonSet: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."consul.hashicorp.com/config-checksum") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "client/DaemonSet: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + +#-------------------------------------------------------------------- +# global.imageK8s + +@test "client/DaemonSet: errors on global.imageK8s" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.imageK8s=something' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "global.imageK8s is not a valid key, use global.imageK8S (note the capital 'S')" ]] +} \ No newline at end of file diff --git a/charts/consul/test/unit/client-podsecuritypolicy.bats b/charts/consul/test/unit/client-podsecuritypolicy.bats index 701bd3a850..a37d4ec147 100644 --- a/charts/consul/test/unit/client-podsecuritypolicy.bats +++ b/charts/consul/test/unit/client-podsecuritypolicy.bats @@ -140,7 +140,21 @@ load _helpers [ "${actual}" = "true" ] } +@test "client/PodSecurityPolicy: hostPorts when hostNetwork=true" { + # hostPorts must be allowed because when Kube sets all container ports as host ports when hostNetwork is true. + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-podsecuritypolicy.yaml \ + --set 'global.enablePodSecurityPolicies=true' \ + --set 'client.hostNetwork=true' \ + . | tee /dev/stderr | + yq -c '.spec.hostPorts' | tee /dev/stderr) + [ "${actual}" = '[{"min":8500,"max":8500},{"min":8502,"max":8502},{"min":8301,"max":8301},{"min":8600,"max":8600}]' ] +} + +#-------------------------------------------------------------------- # client.hostNetwork = false + @test "client/PodSecurityPolicy: enabled with global.enablePodSecurityPolicies=true and default hostNetwork=false" { cd `chart_dir` local actual=$(helm template \ diff --git a/charts/consul/test/unit/client-snapshot-agent-deployment.bats b/charts/consul/test/unit/client-snapshot-agent-deployment.bats index 07d96a68bf..8e345189d7 100644 --- a/charts/consul/test/unit/client-snapshot-agent-deployment.bats +++ b/charts/consul/test/unit/client-snapshot-agent-deployment.bats @@ -389,8 +389,8 @@ exec /bin/consul snapshot agent \' local actual=$(helm template \ -s templates/client-snapshot-agent-deployment.yaml \ --set 'client.snapshotAgent.enabled=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) [ "${actual}" = '{"name":"consul-license","secret":{"secretName":"foo"}}' ] @@ -401,8 +401,8 @@ exec /bin/consul snapshot agent \' local actual=$(helm template \ -s templates/client-snapshot-agent-deployment.yaml \ --set 'client.snapshotAgent.enabled=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) [ "${actual}" = '{"name":"consul-license","mountPath":"/consul/license","readOnly":true}' ] @@ -413,8 +413,8 @@ exec /bin/consul snapshot agent \' local actual=$(helm template \ -s templates/client-snapshot-agent-deployment.yaml \ --set 'client.snapshotAgent.enabled=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_LICENSE_PATH")' | tee /dev/stderr) [ "${actual}" = '{"name":"CONSUL_LICENSE_PATH","value":"/consul/license/bar"}' ] @@ -425,8 +425,8 @@ exec /bin/consul snapshot agent \' local actual=$(helm template \ -s templates/client-snapshot-agent-deployment.yaml \ --set 'client.snapshotAgent.enabled=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) @@ -438,8 +438,8 @@ exec /bin/consul snapshot agent \' local actual=$(helm template \ -s templates/client-snapshot-agent-deployment.yaml \ --set 'client.snapshotAgent.enabled=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) @@ -451,10 +451,243 @@ exec /bin/consul snapshot agent \' local actual=$(helm template \ -s templates/client-snapshot-agent-deployment.yaml \ --set 'client.snapshotAgent.enabled=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_LICENSE_PATH")' | tee /dev/stderr) [ "${actual}" = "" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "client/SnapshotAgentDeployment: configures server CA to come from vault when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check annotations + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + [ "${actual}" = "carole" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = "foo" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = $'{{- with secret \"foo\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' ] +} + +@test "client/SnapshotAgentDeployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "client/SnapshotAgentDeployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "client/SnapshotAgentDeployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "client/SnapshotAgentDeployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "client/SnapshotAgentDeployment: vault enterprise license annotations are correct when enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=path/to/secret' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-enterpriselicense.txt"]' | tee /dev/stderr) + [ "${actual}" = "path/to/secret" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-enterpriselicense.txt"]' | tee /dev/stderr) + local actual="$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-enterpriselicense.txt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"path/to/secret\" -}}\n{{- .Data.data.enterpriselicense -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "client/SnapshotAgentDeployment: vault CONSUL_LICENSE_PATH is set to /vault/secrets/enterpriselicense.txt" { + cd `chart_dir` + local env=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].env[]' | tee /dev/stderr) + + local actual + + local actual=$(echo $env | jq -r '. | select(.name == "CONSUL_LICENSE_PATH") | .value' | tee /dev/stderr) + [ "${actual}" = "/vault/secrets/enterpriselicense.txt" ] +} + +@test "client/SnapshotAgentDeployment: vault does not add volume for license secret" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +@test "client/SnapshotAgentDeployment: vault does not add volume mount for license secret" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "client/SnapshotAgentDeployment: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "client/SnapshotAgentDeployment: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} \ No newline at end of file diff --git a/charts/consul/test/unit/connect-inject-authmethod-clusterrole.bats b/charts/consul/test/unit/connect-inject-authmethod-clusterrole.bats deleted file mode 100644 index 2437522441..0000000000 --- a/charts/consul/test/unit/connect-inject-authmethod-clusterrole.bats +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bats - -load _helpers - -@test "connectInjectAuthMethod/ClusterRole: disabled by default" { - cd `chart_dir` - assert_empty helm template \ - -s templates/connect-inject-authmethod-clusterrole.yaml \ - . -} - -@test "connectInjectAuthMethod/ClusterRole: enabled with global.enabled false" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/connect-inject-authmethod-clusterrole.yaml \ - --set 'global.enabled=false' \ - --set 'client.enabled=true' \ - --set 'connectInject.enabled=true' \ - --set 'global.acls.manageSystemACLs=true' \ - . | tee /dev/stderr | - yq -s 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] -} - -@test "connectInjectAuthMethod/ClusterRole: disabled with connectInject.enabled" { - cd `chart_dir` - assert_empty helm template \ - -s templates/connect-inject-authmethod-clusterrole.yaml \ - --set 'connectInject.enabled=true' \ - . -} - -@test "connectInjectAuthMethod/ClusterRole: enabled with global.acls.manageSystemACLs.enabled=true" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/connect-inject-authmethod-clusterrole.yaml \ - --set 'connectInject.enabled=true' \ - --set 'global.acls.manageSystemACLs=true' \ - . | tee /dev/stderr | - yq -s 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] -} diff --git a/charts/consul/test/unit/connect-inject-authmethod-serviceaccount.bats b/charts/consul/test/unit/connect-inject-authmethod-serviceaccount.bats deleted file mode 100644 index 68788cadc5..0000000000 --- a/charts/consul/test/unit/connect-inject-authmethod-serviceaccount.bats +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bats - -load _helpers - -@test "connectInjectAuthMethod/ServiceAccount: disabled by default" { - cd `chart_dir` - assert_empty helm template \ - -s templates/connect-inject-authmethod-serviceaccount.yaml \ - . -} - -@test "connectInjectAuthMethod/ServiceAccount: enabled with global.enabled false" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/connect-inject-authmethod-serviceaccount.yaml \ - --set 'global.enabled=false' \ - --set 'client.enabled=true' \ - --set 'connectInject.enabled=true' \ - --set 'global.acls.manageSystemACLs=true' \ - . | tee /dev/stderr | - yq -s 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] -} - -@test "connectInjectAuthMethod/ServiceAccount: disabled with connectInject.enabled" { - cd `chart_dir` - assert_empty helm template \ - -s templates/connect-inject-authmethod-serviceaccount.yaml \ - --set 'connectInject.enabled=true' \ - . -} - -@test "connectInjectAuthMethod/ServiceAccount: enabled with global.acls.manageSystemACLs.enabled=true" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/connect-inject-authmethod-serviceaccount.yaml \ - --set 'connectInject.enabled=true' \ - --set 'global.acls.manageSystemACLs=true' \ - . | tee /dev/stderr | - yq -s 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] -} - -#-------------------------------------------------------------------- -# global.imagePullSecrets - -@test "connectInjectAuthMethod/ServiceAccount: can set image pull secrets" { - cd `chart_dir` - local object=$(helm template \ - -s templates/connect-inject-authmethod-serviceaccount.yaml \ - --set 'connectInject.enabled=true' \ - --set 'global.acls.manageSystemACLs=true' \ - --set 'global.imagePullSecrets[0].name=my-secret' \ - --set 'global.imagePullSecrets[1].name=my-secret2' \ - . | tee /dev/stderr) - - local actual=$(echo "$object" | - yq -r '.imagePullSecrets[0].name' | tee /dev/stderr) - [ "${actual}" = "my-secret" ] - - local actual=$(echo "$object" | - yq -r '.imagePullSecrets[1].name' | tee /dev/stderr) - [ "${actual}" = "my-secret2" ] -} - diff --git a/charts/consul/test/unit/connect-inject-deployment.bats b/charts/consul/test/unit/connect-inject-deployment.bats index de747995a1..a3da403005 100755 --- a/charts/consul/test/unit/connect-inject-deployment.bats +++ b/charts/consul/test/unit/connect-inject-deployment.bats @@ -519,6 +519,40 @@ EOF [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# DNS + +@test "connectInject/Deployment: -enable-consul-dns unset by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("-enable-consul-dns=true")' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "connectInject/Deployment: -enable-consul-dns is true if dns.enabled=true and dns.enableRedirection=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'dns.enableRedirection=true' \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("-enable-consul-dns=true")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "connectInject/Deployment: -resource-prefix always set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("-resource-prefix=RELEASE-NAME-consul")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + #-------------------------------------------------------------------- # global.tls.enabled @@ -686,6 +720,45 @@ EOF [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# partitions + +@test "connectInject/Deployment: partitions options disabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("enable-partitions"))' | tee /dev/stderr) + + [ "${actual}" = "false" ] +} + +@test "connectInject/Deployment: partitions set with .global.adminPartitions.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("enable-partitions"))' | tee /dev/stderr) + + [ "${actual}" = "true" ] +} + +@test "connectInject/Deployment: fails if namespaces are disabled and .global.adminPartitions.enabled=true" { + cd `chart_dir` + run helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=false' \ + --set 'connectInject.enabled=true' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" ]] +} + #-------------------------------------------------------------------- # namespaces @@ -1092,19 +1165,19 @@ EOF yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-request=25Mi"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-request=25Mi"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-request=20m"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-request=20m"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-limit=50Mi"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-limit=50Mi"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-limit=20m"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-limit=20m"))' | tee /dev/stderr) [ "${actual}" = "true" ] } @@ -1121,19 +1194,19 @@ EOF yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-request=100Mi"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-request=100Mi"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-request=100m"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-request=100m"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-limit=200Mi"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-limit=200Mi"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-limit=200m"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-limit=200m"))' | tee /dev/stderr) [ "${actual}" = "true" ] } @@ -1150,19 +1223,19 @@ EOF yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-request=0"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-request=0"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-request=0"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-request=0"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-limit=0"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-limit=0"))' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-limit=0"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-limit=0"))' | tee /dev/stderr) [ "${actual}" = "true" ] } @@ -1179,19 +1252,19 @@ EOF yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-request"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-request"))' | tee /dev/stderr) [ "${actual}" = "false" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-request"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-request"))' | tee /dev/stderr) [ "${actual}" = "false" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-limit"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-limit"))' | tee /dev/stderr) [ "${actual}" = "false" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-limit"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-limit"))' | tee /dev/stderr) [ "${actual}" = "false" ] } @@ -1205,19 +1278,19 @@ EOF yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-request"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-request"))' | tee /dev/stderr) [ "${actual}" = "false" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-request"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-request"))' | tee /dev/stderr) [ "${actual}" = "false" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-memory-limit"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-memory-limit"))' | tee /dev/stderr) [ "${actual}" = "false" ] local actual=$(echo "$cmd" | - yq 'any(contains("-consul-sidecar-cpu-limit"))' | tee /dev/stderr) + yq 'any(contains("-default-consul-sidecar-cpu-limit"))' | tee /dev/stderr) [ "${actual}" = "false" ] } @@ -1473,4 +1546,205 @@ EOF yq '.spec.replicas' | tee /dev/stderr) [ "${actual}" = "3" ] -} \ No newline at end of file +} + +#-------------------------------------------------------------------- +# Vault + +@test "connectInject/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "connectInject/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "connectInject/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "connectInject/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "connectInject/Deployment: vault tls annotations are set when tls is enabled" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=bar' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/cert/ca\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/cert/ca" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr)" + [ "${actual}" = "test" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "connectInject/Deployment: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "connectInject/Deployment: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + +# consulDestinationNamespace reserved name + +@test "connectInject/Deployment: fails when consulDestinationNamespace=system" { + reservedNameTest "system" +} + +@test "connectInject/Deployment: fails when consulDestinationNamespace=universal" { + reservedNameTest "universal" +} + +@test "connectInject/Deployment: fails when consulDestinationNamespace=consul" { + reservedNameTest "consul" +} + +@test "connectInject/Deployment: fails when consulDestinationNamespace=operator" { + reservedNameTest "operator" +} + +@test "connectInject/Deployment: fails when consulDestinationNamespace=root" { + reservedNameTest "root" +} + +# reservedNameTest is a helper function that tests if certain Consul destination +# namespace names fail because the name is reserved. +reservedNameTest() { + cd `chart_dir` + local -r name="$1" + run helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set "connectInject.consulNamespaces.consulDestinationNamespace=$name" . + + [ "$status" -eq 1 ] + [[ "$output" =~ "The name $name set for key connectInject.consulNamespaces.consulDestinationNamespace is reserved by Consul for future use" ]] +} diff --git a/charts/consul/test/unit/connect-inject-mutatingwebhook.bats b/charts/consul/test/unit/connect-inject-mutatingwebhookconfiguration.bats similarity index 77% rename from charts/consul/test/unit/connect-inject-mutatingwebhook.bats rename to charts/consul/test/unit/connect-inject-mutatingwebhookconfiguration.bats index 5e60732353..11c3a6b0a5 100755 --- a/charts/consul/test/unit/connect-inject-mutatingwebhook.bats +++ b/charts/consul/test/unit/connect-inject-mutatingwebhookconfiguration.bats @@ -5,14 +5,14 @@ load _helpers @test "connectInject/MutatingWebhookConfiguration: disabled by default" { cd `chart_dir` assert_empty helm template \ - -s templates/connect-inject-mutatingwebhook.yaml \ + -s templates/connect-inject-mutatingwebhookconfiguration.yaml \ . } @test "connectInject/MutatingWebhookConfiguration: enable with global.enabled false" { cd `chart_dir` local actual=$(helm template \ - -s templates/connect-inject-mutatingwebhook.yaml \ + -s templates/connect-inject-mutatingwebhookconfiguration.yaml \ --set 'global.enabled=false' \ --set 'client.enabled=true' \ --set 'connectInject.enabled=true' \ @@ -24,7 +24,7 @@ load _helpers @test "connectInject/MutatingWebhookConfiguration: disable with connectInject.enabled" { cd `chart_dir` assert_empty helm template \ - -s templates/connect-inject-mutatingwebhook.yaml \ + -s templates/connect-inject-mutatingwebhookconfiguration.yaml \ --set 'connectInject.enabled=false' \ . } @@ -32,7 +32,7 @@ load _helpers @test "connectInject/MutatingWebhookConfiguration: disable with global.enabled" { cd `chart_dir` assert_empty helm template \ - -s templates/connect-inject-mutatingwebhook.yaml \ + -s templates/connect-inject-mutatingwebhookconfiguration.yaml \ --set 'global.enabled=false' \ . } @@ -40,7 +40,7 @@ load _helpers @test "connectInject/MutatingWebhookConfiguration: namespace is set" { cd `chart_dir` local actual=$(helm template \ - -s templates/connect-inject-mutatingwebhook.yaml \ + -s templates/connect-inject-mutatingwebhookconfiguration.yaml \ --set 'connectInject.enabled=true' \ --namespace foo \ . | tee /dev/stderr | diff --git a/charts/consul/test/unit/controller-deployment.bats b/charts/consul/test/unit/controller-deployment.bats index d78b7654c5..248811867d 100644 --- a/charts/consul/test/unit/controller-deployment.bats +++ b/charts/consul/test/unit/controller-deployment.bats @@ -190,6 +190,44 @@ load _helpers [ "${actual}" = "" ] } +#-------------------------------------------------------------------- +# partitions + +@test "controller/Deployment: partitions options disabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("partition"))' | tee /dev/stderr) + + [ "${actual}" = "false" ] +} + +@test "controller/Deployment: partition name set with .global.adminPartitions.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("partition=default"))' | tee /dev/stderr) + + [ "${actual}" = "true" ] +} + +@test "controller/Deployment: fails if namespaces are disabled and .global.adminPartitions.enabled=true" { + cd `chart_dir` + run helm template \ + -s templates/controller-deployment.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=false' \ + --set 'controller.enabled=true' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" ]] +} #-------------------------------------------------------------------- # namespaces @@ -512,3 +550,169 @@ load _helpers [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# Vault + +@test "controller/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "controller/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "controller/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "controller/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "controller/Deployment: vault tls annotations are set when tls is enabled" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=bar' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/cert/ca\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/cert/ca" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr)" + [ "${actual}" = "test" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "controller/Deployment: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "controller/Deployment: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + + diff --git a/charts/consul/test/unit/crd-exportedservices.bats b/charts/consul/test/unit/crd-exportedservices.bats new file mode 100644 index 0000000000..cf1a35a587 --- /dev/null +++ b/charts/consul/test/unit/crd-exportedservices.bats @@ -0,0 +1,24 @@ +#!/usr/bin/env bats + +load _helpers + +@test "exportedServices/CustomerResourceDefinition: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/crd-exportedservices.yaml \ + . +} + +@test "exportedServices/CustomerResourceDefinition: enabled with controller.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/crd-exportedservices.yaml \ + --set 'controller.enabled=true' \ + . | tee /dev/stderr | + # The generated CRDs have "---" at the top which results in two objects + # being detected by yq, the first of which is null. We must therefore use + # yq -s so that length operates on both objects at once rather than + # individually, which would output false\ntrue and fail the test. + yq -s 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/enterprise-license-job.bats b/charts/consul/test/unit/enterprise-license-job.bats index dfaad6bf03..cbdb913df6 100644 --- a/charts/consul/test/unit/enterprise-license-job.bats +++ b/charts/consul/test/unit/enterprise-license-job.bats @@ -2,86 +2,81 @@ load _helpers -@test "server/EnterpriseLicense: disabled by default" { +@test "enterpriseLicense/Job: disabled by default" { cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-job.yaml \ . } -@test "server/EnterpriseLicense: disabled if autoload is true (default) { +@test "enterpriseLicense/Job: disabled if autoload is true (default) { cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . } -@test "server/EnterpriseLicense: disabled when servers are disabled" { +@test "enterpriseLicense/Job: disabled when servers are disabled" { cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-job.yaml \ --set 'server.enabled=false' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . } -@test "server/EnterpriseLicense: disabled when secretName is missing" { +@test "enterpriseLicense/Job: enabled when secretName, secretKey is provided and autoload is disabled" { cd `chart_dir` - assert_empty helm template \ + local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] } -@test "server/EnterpriseLicense: disabled when secretKey is missing" { +@test "enterpriseLicense/Job: fail is server.enterpriseLicense is set" { cd `chart_dir` - assert_empty helm template \ + run helm template \ -s templates/enterprise-license-job.yaml \ --set 'server.enterpriseLicense.secretName=foo' \ + --set 'server.enterpriseLicense.secretKey=bar' \ --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ . -} -@test "server/EnterpriseLicense: enabled when secretName, secretKey is provided and autoload is disabled" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . | tee /dev/stderr | - yq 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] + [ "$status" -eq 1 ] + [[ "$output" =~ "server.enterpriseLicense has been moved to global.enterpriseLicense" ]] } #-------------------------------------------------------------------- # global.acls.manageSystemACLs -@test "server/EnterpriseLicense: CONSUL_HTTP_TOKEN env variable created when global.acls.manageSystemACLs=true" { +@test "enterpriseLicense/Job: CONSUL_HTTP_TOKEN env variable created when global.acls.manageSystemACLs=true" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq '[.spec.template.spec.containers[0].env[].name] | any(contains("CONSUL_HTTP_TOKEN"))' | tee /dev/stderr) [ "${actual}" = "true" ] } -@test "server/EnterpriseLicense: init container is created when global.acls.manageSystemACLs=true" { +@test "enterpriseLicense/Job: init container is created when global.acls.manageSystemACLs=true" { cd `chart_dir` local object=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq '.spec.template.spec.initContainers[0]' | tee /dev/stderr) @@ -98,104 +93,104 @@ load _helpers #-------------------------------------------------------------------- # global.tls.enabled -@test "server/EnterpriseLicense: no volumes when TLS is disabled" { +@test "enterpriseLicense/Job: no volumes when TLS is disabled" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=false' \ . | tee /dev/stderr | yq '.spec.template.spec.volumes | length' | tee /dev/stderr) [ "${actual}" = "0" ] } -@test "server/EnterpriseLicense: volumes present when TLS is enabled" { +@test "enterpriseLicense/Job: volumes present when TLS is enabled" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=true' \ . | tee /dev/stderr | yq '.spec.template.spec.volumes | length' | tee /dev/stderr) [ "${actual}" = "1" ] } -@test "server/EnterpriseLicense: no volumes mounted when TLS is disabled" { +@test "enterpriseLicense/Job: no volumes mounted when TLS is disabled" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=false' \ . | tee /dev/stderr | yq '.spec.template.spec.containers[0].volumeMounts | length' | tee /dev/stderr) [ "${actual}" = "0" ] } -@test "server/EnterpriseLicense: volumes mounted when TLS is enabled" { +@test "enterpriseLicense/Job: volumes mounted when TLS is enabled" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=true' \ . | tee /dev/stderr | yq '.spec.template.spec.containers[0].volumeMounts | length' | tee /dev/stderr) [ "${actual}" = "1" ] } -@test "server/EnterpriseLicense: URL is http when TLS is disabled" { +@test "enterpriseLicense/Job: URL is http when TLS is disabled" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=false' \ . | tee /dev/stderr | yq -r '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_HTTP_ADDR") | .value' | tee /dev/stderr) [ "${actual}" = "http://RELEASE-NAME-consul-server:8500" ] } -@test "server/EnterpriseLicense: URL is https when TLS is enabled" { +@test "enterpriseLicense/Job: URL is https when TLS is enabled" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=true' \ . | tee /dev/stderr | yq -r '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_HTTP_ADDR") | .value' | tee /dev/stderr) [ "${actual}" = "https://RELEASE-NAME-consul-server:8501" ] } -@test "server/EnterpriseLicense: CA certificate is specified when TLS is enabled" { +@test "enterpriseLicense/Job: CA certificate is specified when TLS is enabled" { cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=true' \ . | tee /dev/stderr | yq -r '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_CACERT") | .value' | tee /dev/stderr) [ "${actual}" = "/consul/tls/ca/tls.crt" ] } -@test "server/EnterpriseLicense: can overwrite CA secret with the provided one" { +@test "enterpriseLicense/Job: can overwrite CA secret with the provided one" { cd `chart_dir` local ca_cert_volume=$(helm template \ -s templates/enterprise-license-job.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.tls.enabled=true' \ --set 'global.tls.caCert.secretName=foo-ca-cert' \ --set 'global.tls.caCert.secretKey=key' \ diff --git a/charts/consul/test/unit/enterprise-license-podsecuritypolicy.bats b/charts/consul/test/unit/enterprise-license-podsecuritypolicy.bats index f22fc9010e..90442ec902 100644 --- a/charts/consul/test/unit/enterprise-license-podsecuritypolicy.bats +++ b/charts/consul/test/unit/enterprise-license-podsecuritypolicy.bats @@ -13,9 +13,9 @@ load _helpers cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-podsecuritypolicy.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . } @@ -24,27 +24,9 @@ load _helpers assert_empty helm template \ -s templates/enterprise-license-podsecuritypolicy.yaml \ --set 'server.enabled=false' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/PodSecurityPolicy: disabled when ent secretName missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-podsecuritypolicy.yaml \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/PodSecurityPolicy: disabled when ent secretKey missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-podsecuritypolicy.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . } @@ -52,9 +34,9 @@ load _helpers cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-podsecuritypolicy.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.enablePodSecurityPolicies=false' \ . } @@ -63,9 +45,9 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-podsecuritypolicy.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.enablePodSecurityPolicies=true' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) diff --git a/charts/consul/test/unit/enterprise-license-role.bats b/charts/consul/test/unit/enterprise-license-role.bats index f3eca4abdb..e30d0cfd16 100644 --- a/charts/consul/test/unit/enterprise-license-role.bats +++ b/charts/consul/test/unit/enterprise-license-role.bats @@ -13,8 +13,8 @@ load _helpers cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-role.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . } @@ -23,27 +23,9 @@ load _helpers assert_empty helm template \ -s templates/enterprise-license-role.yaml \ --set 'server.enabled=false' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/Role: disabled when ent secretName missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-role.yaml \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/Role: disabled when ent secretKey missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-role.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . } @@ -51,9 +33,9 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-role.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] @@ -63,9 +45,9 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-role.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . | tee /dev/stderr | yq '.rules | length' | tee /dev/stderr) [ "${actual}" = "0" ] @@ -78,16 +60,15 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-role.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r '.rules | map(select(.resourceNames[0] == "RELEASE-NAME-consul-enterprise-license-acl-token")) | length' | tee /dev/stderr) [ "${actual}" = "1" ] } - #-------------------------------------------------------------------- # global.enablePodSecurityPolicies @@ -95,9 +76,9 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-role.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.enablePodSecurityPolicies=true' \ . | tee /dev/stderr | yq -r '.rules | map(select(.resources[0] == "podsecuritypolicies")) | length' | tee /dev/stderr) diff --git a/charts/consul/test/unit/enterprise-license-rolebinding.bats b/charts/consul/test/unit/enterprise-license-rolebinding.bats index edb8970fcb..d0052e1b68 100644 --- a/charts/consul/test/unit/enterprise-license-rolebinding.bats +++ b/charts/consul/test/unit/enterprise-license-rolebinding.bats @@ -13,8 +13,8 @@ load _helpers cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-rolebinding.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . } @@ -23,27 +23,9 @@ load _helpers assert_empty helm template \ -s templates/enterprise-license-rolebinding.yaml \ --set 'server.enabled=false' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/RoleBinding: disabled when ent secretName missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-rolebinding.yaml \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/RoleBinding: disabled when ent secretKey missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-rolebinding.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . } @@ -51,9 +33,9 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-rolebinding.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] diff --git a/charts/consul/test/unit/enterprise-license-serviceaccount.bats b/charts/consul/test/unit/enterprise-license-serviceaccount.bats index a863e0091c..98b5fd80af 100644 --- a/charts/consul/test/unit/enterprise-license-serviceaccount.bats +++ b/charts/consul/test/unit/enterprise-license-serviceaccount.bats @@ -13,8 +13,8 @@ load _helpers cd `chart_dir` assert_empty helm template \ -s templates/enterprise-license-serviceaccount.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . } @@ -23,27 +23,9 @@ load _helpers assert_empty helm template \ -s templates/enterprise-license-serviceaccount.yaml \ --set 'server.enabled=false' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/ServiceAccount: disabled when ent secretName missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-serviceaccount.yaml \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ - . -} - -@test "enterpriseLicense/ServiceAccount: disabled when ent secretKey missing" { - cd `chart_dir` - assert_empty helm template \ - -s templates/enterprise-license-serviceaccount.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . } @@ -51,9 +33,9 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/enterprise-license-serviceaccount.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] @@ -66,9 +48,9 @@ load _helpers cd `chart_dir` local object=$(helm template \ -s templates/enterprise-license-serviceaccount.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - --set 'server.enterpriseLicense.enableLicenseAutoload=false' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.enableLicenseAutoload=false' \ --set 'global.imagePullSecrets[0].name=my-secret' \ --set 'global.imagePullSecrets[1].name=my-secret2' \ . | tee /dev/stderr) diff --git a/charts/consul/test/unit/gossip-encryption-autogenerate-job.bats b/charts/consul/test/unit/gossip-encryption-autogenerate-job.bats new file mode 100644 index 0000000000..4b5938ab91 --- /dev/null +++ b/charts/consul/test/unit/gossip-encryption-autogenerate-job.bats @@ -0,0 +1,53 @@ +#!/usr/bin/env bats + +load _helpers + +@test "gossipEncryptionAutogenerate/Job: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-job.yaml \ + . +} + +@test "gossipEncryptionAutogenerate/Job: enabled with global.gossipEncryption.autoGenerate=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/gossip-encryption-autogenerate-job.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "gossipEncryptionAutogenerate/Job: disabled when global.gossipEncryption.autoGenerate=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-job.yaml \ + --set 'global.gossipEncryption.autoGenerate=false' \ + . +} + +@test "gossipEncryptionAutogenerate/Job: fails if global.gossipEncryption.autoGenerate=true and global.gossipEncryption.secretName and global.gossipEncryption.secretKey are set" { + cd `chart_dir` + run helm template \ + -s templates/gossip-encryption-autogenerate-job.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + --set 'global.gossipEncryption.secretName=name' \ + --set 'global.gossipEncryption.secretKey=key' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "If global.gossipEncryption.autoGenerate is true, global.gossipEncryption.secretName and global.gossipEncryption.secretKey must not be set." ]] +} + +@test "gossipEncryptionAutogenerate/Job: fails if global.gossipEncryption.autoGenerate=true and global.gossipEncryption.secretName+key are set" { + cd `chart_dir` + run helm template \ + -s templates/gossip-encryption-autogenerate-job.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + --set 'global.gossipEncryption.secretName=name' \ + --set 'global.gossipEncryption.secretKey=name' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "If global.gossipEncryption.autoGenerate is true, global.gossipEncryption.secretName and global.gossipEncryption.secretKey must not be set." ]] +} + diff --git a/charts/consul/test/unit/gossip-encryption-autogenerate-podsecuritypolicy.bats b/charts/consul/test/unit/gossip-encryption-autogenerate-podsecuritypolicy.bats new file mode 100644 index 0000000000..810147bed3 --- /dev/null +++ b/charts/consul/test/unit/gossip-encryption-autogenerate-podsecuritypolicy.bats @@ -0,0 +1,28 @@ +#!/usr/bin/env bats + +load _helpers + +@test "gossipEncryptionAutogenerate/PodSecurityPolicy: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-podsecuritypolicy.yaml \ + . +} + +@test "gossipEncryptionAutogenerate/PodSecurityPolicy: disabled with global.gossipEncryption.autoGenerate=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-podsecuritypolicy.yaml \ + --set 'global.gossipEncryption.autoGenerate=false' \ + . +} + +@test "gossipEncryptionAutogenerate/PodSecurityPolicy: enabled with global.gossipEncryption.autoGenerate=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/gossip-encryption-autogenerate-podsecuritypolicy.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq -s 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/gossip-encryption-autogenerate-role.bats b/charts/consul/test/unit/gossip-encryption-autogenerate-role.bats new file mode 100644 index 0000000000..7707a872f0 --- /dev/null +++ b/charts/consul/test/unit/gossip-encryption-autogenerate-role.bats @@ -0,0 +1,28 @@ +#!/usr/bin/env bats + +load _helpers + +@test "gossipEncryptionAutogenerate/Role: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-role.yaml \ + . +} + +@test "gossipEncryptionAutogenerate/Role: disabled with global.gossipEncryption.autoGenerate=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-role.yaml \ + --set 'global.gossipEncryption.autoGenerate=false' \ + . +} + +@test "gossipEncryptionAutogenerate/Role: enabled when global.gossipEncryption.autoGenerate=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/gossip-encryption-autogenerate-role.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/gossip-encryption-autogenerate-rolebinding.bats b/charts/consul/test/unit/gossip-encryption-autogenerate-rolebinding.bats new file mode 100644 index 0000000000..9847beaa24 --- /dev/null +++ b/charts/consul/test/unit/gossip-encryption-autogenerate-rolebinding.bats @@ -0,0 +1,29 @@ + +#!/usr/bin/env bats + +load _helpers + +@test "gossipEncryptionAutogenerate/RoleBinding: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-rolebinding.yaml \ + . +} + +@test "gossipEncryptionAutogenerate/RoleBinding: disabled with global.gossipEncryption.autoGenerate=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-rolebinding.yaml \ + --set 'global.gossipEncryption.autoGenerate=false' \ + . +} + +@test "gossipEncryptionAutogenerate/RoleBinding: enabled with global.gossipEncryption.autoGenerate=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/gossip-encryption-autogenerate-rolebinding.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/gossip-encryption-autogenerate-serviceaccount.bats b/charts/consul/test/unit/gossip-encryption-autogenerate-serviceaccount.bats new file mode 100644 index 0000000000..782d1b1ad6 --- /dev/null +++ b/charts/consul/test/unit/gossip-encryption-autogenerate-serviceaccount.bats @@ -0,0 +1,50 @@ +#!/usr/bin/env bats + +load _helpers + +@test "gossipEncryptionAutogenerate/ServiceAccount: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-serviceaccount.yaml \ + . +} + +@test "gossipEncryptionAutogenerate/ServiceAccount: disabled with global.gossipEncryption.autoGenerate=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/gossip-encryption-autogenerate-serviceaccount.yaml \ + --set 'global.gossipEncryption.autoGenerate=false' \ + . +} + +@test "gossipEncryptionAutogenerate/ServiceAccount: enabled with global.gossipEncryption.autoGenerate=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/gossip-encryption-autogenerate-serviceaccount.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# global.imagePullSecrets + +@test "gossipEncryptionAutogenerate/ServiceAccount: can set image pull secrets" { + cd `chart_dir` + local object=$(helm template \ + -s templates/gossip-encryption-autogenerate-serviceaccount.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + --set 'global.imagePullSecrets[0].name=my-secret' \ + --set 'global.imagePullSecrets[1].name=my-secret2' \ + . | tee /dev/stderr) + + local actual=$(echo "$object" | + yq -r '.imagePullSecrets[0].name' | tee /dev/stderr) + [ "${actual}" = "my-secret" ] + + local actual=$(echo "$object" | + yq -r '.imagePullSecrets[1].name' | tee /dev/stderr) + [ "${actual}" = "my-secret2" ] +} + diff --git a/charts/consul/test/unit/helpers.bats b/charts/consul/test/unit/helpers.bats index 235338adc6..ee524a6842 100644 --- a/charts/consul/test/unit/helpers.bats +++ b/charts/consul/test/unit/helpers.bats @@ -115,7 +115,21 @@ load _helpers @test "helper/namespace: used everywhere" { cd `chart_dir` # Grep for files that don't have 'namespace: ' in them - local actual=$(grep -L 'namespace: ' templates/*.yaml | grep -v 'crd' | grep -v 'clusterrole' | tee /dev/stderr ) + local actual=$(grep -L 'namespace: ' templates/*.yaml | grep -v 'crd' | grep -v 'clusterrole' | grep -v 'api-gateway-gateway' | tee /dev/stderr ) + [ "${actual}" = '' ] +} + +#-------------------------------------------------------------------- +# component label +# +# This test ensures that we set a "component: " in every file. +# +# If this test fails, you're likely missing setting that label somewhere. + +@test "helper/component-label: used everywhere" { + cd `chart_dir` + # Grep for files that don't have 'component: ' in them + local actual=$(grep -L 'component: ' templates/*.yaml | tee /dev/stderr ) [ "${actual}" = '' ] } @@ -270,3 +284,26 @@ load _helpers [ "${actual}" = "" ] } + +@test "helper/consul.getAutoEncryptClientCA: uses the correct -ca-file when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/tests/test-runner.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/ca/pem' \ + --set 'server.enabled=false' \ + . | tee /dev/stderr | + yq '.spec.initContainers[] | select(.name == "get-auto-encrypt-client-ca")' | tee /dev/stderr) + + actual=$(echo $object | jq '.command | join(" ") | contains("-ca-file=/vault/secrets/serverca.crt")') + [ "${actual}" = "true" ] + + actual=$(echo $object | jq '.volumeMounts[] | select(.name == "consul-ca-cert")') + [ "${actual}" = "" ] +} diff --git a/charts/consul/test/unit/ingress-gateways-deployment.bats b/charts/consul/test/unit/ingress-gateways-deployment.bats index 15fd1e1ef0..76b20dd501 100644 --- a/charts/consul/test/unit/ingress-gateways-deployment.bats +++ b/charts/consul/test/unit/ingress-gateways-deployment.bats @@ -83,7 +83,7 @@ load _helpers --set 'connectInject.enabled=true' \ . | tee /dev/stderr | yq -s -r '.[0].spec.template.spec.containers[0].image' | tee /dev/stderr) - [ "${actual}" = "envoyproxy/envoy-alpine:v1.18.4" ] + [ "${actual}" = "envoyproxy/envoy-alpine:v1.20.2" ] } @test "ingressGateways/Deployment: envoy image can be set using the global value" { @@ -1405,6 +1405,57 @@ EOF [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# partitions + +@test "ingressGateways/Deployment: partition command flag is not present by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq -s -r '.[0].spec.template.spec.containers[0]' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.command | any(contains("-partition"))' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(echo $object | yq -r '.lifecycle.preStop.exec.command | any(contains("-partition"))' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "ingressGateways/Deployment: partition command flag is specified through partition name" { + cd `chart_dir` + local object=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=default' \ + . | tee /dev/stderr | + yq -s -r '.[0].spec.template.spec.containers[0]' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.command | any(contains("-partition=default"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | yq -r '.lifecycle.preStop.exec.command | any(contains("-partition=default"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "ingressGateways/Deployment: fails if admin partitions are enabled but namespaces aren't" { + cd `chart_dir` + run helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.enableConsulNamespaces=false' \ + --set 'global.adminPartitions.enabled=true' . + + [ "$status" -eq 1 ] + [[ "$output" =~ "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" ]] +} + #-------------------------------------------------------------------- # multiple gateways @@ -1434,3 +1485,206 @@ EOF local actual=$(echo $object | yq '.[2] | length > 0' | tee /dev/stderr) [ "${actual}" = "false" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "ingressGateway/Deployment: vault tls annotations are set when tls is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check annotations + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + [ "${actual}" = "carole" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = $'{{- with secret \"foo\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' ] +} + +@test "ingressGateway/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + + +@test "ingressGateway/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "ingressGateway/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "ingressGateway/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "ingressGateway/Deployment: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "ingressGateway/Deployment: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + +#-------------------------------------------------------------------- +# terminationGracePeriodSeconds + +@test "ingressGateways/Deployment: terminationGracePeriodSeconds defaults to 10" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq -s -r '.[0].spec.template.spec.terminationGracePeriodSeconds' | tee /dev/stderr) + [ "${actual}" = "10" ] +} + +@test "ingressGateways/Deployment: terminationGracePeriodSeconds can be set through defaults" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'ingressGateways.defaults.terminationGracePeriodSeconds=5' \ + . | tee /dev/stderr | + yq -s -r '.[0].spec.template.spec.terminationGracePeriodSeconds' | tee /dev/stderr) + [ "${actual}" = "5" ] +} + +@test "ingressGateways/Deployment: can set terminationGracePeriodSeconds through specific gateway overriding defaults" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'ingressGateways.defaults.terminationGracePeriodSeconds=5' \ + --set 'ingressGateways.gateways[0].name=gateway1' \ + --set 'ingressGateways.gateways[0].terminationGracePeriodSeconds=30' \ + . | tee /dev/stderr | + yq -s -r '.[0].spec.template.spec.terminationGracePeriodSeconds' | tee /dev/stderr) + [ "${actual}" = "30" ] +} diff --git a/charts/consul/test/unit/mesh-gateway-deployment.bats b/charts/consul/test/unit/mesh-gateway-deployment.bats index 4e12efa89d..e8a2b1eeec 100755 --- a/charts/consul/test/unit/mesh-gateway-deployment.bats +++ b/charts/consul/test/unit/mesh-gateway-deployment.bats @@ -335,7 +335,7 @@ key2: value2' \ --set 'connectInject.enabled=true' \ . | tee /dev/stderr | yq -r '.spec.template.spec.containers[0].image' | tee /dev/stderr) - [ "${actual}" = "envoyproxy/envoy-alpine:v1.18.4" ] + [ "${actual}" = "envoyproxy/envoy-alpine:v1.20.2" ] } @test "meshGateway/Deployment: setting meshGateway.imageEnvoy fails" { @@ -437,6 +437,50 @@ key2: value2' \ [ "${actual}" = "cpu2" ] } +#-------------------------------------------------------------------- +# service-init container resources + +@test "meshGateway/Deployment: init service-init container has default resources" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.initContainers[1].resources' | tee /dev/stderr) + + [ $(echo "${actual}" | yq -r '.requests.memory') = "50Mi" ] + [ $(echo "${actual}" | yq -r '.requests.cpu') = "50m" ] + [ $(echo "${actual}" | yq -r '.limits.memory') = "50Mi" ] + [ $(echo "${actual}" | yq -r '.limits.cpu') = "50m" ] +} + +@test "meshGateway/Deployment: init service-init container resources can be set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.initServiceInitContainer.resources.requests.memory=memory' \ + --set 'meshGateway.initServiceInitContainer.resources.requests.cpu=cpu' \ + --set 'meshGateway.initServiceInitContainer.resources.limits.memory=memory2' \ + --set 'meshGateway.initServiceInitContainer.resources.limits.cpu=cpu2' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.initContainers[1].resources' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.requests.memory' | tee /dev/stderr) + [ "${actual}" = "memory" ] + + local actual=$(echo $object | yq -r '.requests.cpu' | tee /dev/stderr) + [ "${actual}" = "cpu" ] + + local actual=$(echo $object | yq -r '.limits.memory' | tee /dev/stderr) + [ "${actual}" = "memory2" ] + + local actual=$(echo $object | yq -r '.limits.cpu' | tee /dev/stderr) + [ "${actual}" = "cpu2" ] +} + #-------------------------------------------------------------------- # consul sidecar resources @@ -1431,3 +1475,231 @@ EOF [ "$status" -eq 1 ] [[ "$output" =~ "meshGateway.globalMode is no longer supported; instead, you must migrate to CRDs (see www.consul.io/docs/k8s/crds/upgrade-to-crds)" ]] } + +#-------------------------------------------------------------------- +# partitions + +@test "meshGateway/Deployment: partitions options disabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("partition"))' | tee /dev/stderr) + + [ "${actual}" = "false" ] +} + +@test "meshGateway/Deployment: partition name set on initContainer with .global.adminPartitions.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.initContainers[1].command | any(contains("partition = \"default\""))' | tee /dev/stderr) + + [ "${actual}" = "true" ] +} + +@test "meshGateway/Deployment: partition name set on container with .global.adminPartitions.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command | any(contains("partition=default"))' | tee /dev/stderr) + + [ "${actual}" = "true" ] +} + +@test "meshGateway/Deployment: fails if namespaces are disabled and .global.adminPartitions.enabled=true" { + cd `chart_dir` + run helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=false' \ + --set 'meshGateway.enabled=true' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" ]] +} + +#-------------------------------------------------------------------- +# Vault + +@test "meshGateway/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "meshGateway/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "meshGateway/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "meshGateway/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "meshGateway/Deployment: vault tls annotations are set when tls is enabled" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=bar' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/cert/ca\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/cert/ca" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr)" + [ "${actual}" = "test" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "meshGateway/Deployment: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "meshGateway/Deployment: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} diff --git a/charts/consul/test/unit/mesh-gateway-podsecuritypolicy.bats b/charts/consul/test/unit/mesh-gateway-podsecuritypolicy.bats index 66e71d97bb..22565c9b02 100644 --- a/charts/consul/test/unit/mesh-gateway-podsecuritypolicy.bats +++ b/charts/consul/test/unit/mesh-gateway-podsecuritypolicy.bats @@ -45,3 +45,29 @@ load _helpers yq '.spec.hostNetwork' | tee /dev/stderr) [ "${actual}" = "true" ] } + +@test "meshGateway/PodSecurityPolicy: hostPorts are allowed when setting hostPort" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-podsecuritypolicy.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.enablePodSecurityPolicies=true' \ + --set 'meshGateway.hostPort=9999' \ + . | tee /dev/stderr | + yq -c '.spec.hostPorts' | tee /dev/stderr) + [ "${actual}" = '[{"min":9999,"max":9999}]' ] +} + +@test "meshGateway/PodSecurityPolicy: hostPorts are allowed when hostNetwork=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/mesh-gateway-podsecuritypolicy.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.enablePodSecurityPolicies=true' \ + --set 'meshGateway.hostNetwork=true' \ + . | tee /dev/stderr | + yq -c '.spec.hostPorts' | tee /dev/stderr) + [ "${actual}" = '[{"min":8443,"max":8443}]' ] +} diff --git a/charts/consul/test/unit/partition-init-job.bats b/charts/consul/test/unit/partition-init-job.bats new file mode 100644 index 0000000000..ca1a9a6d37 --- /dev/null +++ b/charts/consul/test/unit/partition-init-job.bats @@ -0,0 +1,204 @@ +#!/usr/bin/env bats + +load _helpers + +@test "partitionInit/Job: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-job.yaml \ + . +} + +@test "partitionInit/Job: enabled with global.adminPartitions.enabled=true and servers = false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + --set 'global.adminPartitions.name=bar' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partitionInit/Job: disabled with global.adminPartitions.enabled=true and servers = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} + +@test "partitionInit/Job: disabled with global.adminPartitions.enabled=true and adminPartition.name = default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + . +} + +@test "partitionInit/Job: disabled with global.adminPartitions.enabled=true and global.enabled = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enabled=true' \ + . +} + +@test "partitionInit/Job: disabled with global.adminPartitions.enabled=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} + +@test "partitionInit/Job: fails if externalServers.enabled = false with non-default adminPartition" { + cd `chart_dir` + run helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=bar' \ + --set 'server.enabled=false' \ + --set 'externalServers.enabled=false' . + [ "$status" -eq 1 ] + [[ "$output" =~ "externalServers.enabled needs to be true and configured to create a non-default partition." ]] +} + +#-------------------------------------------------------------------- +# global.tls.enabled + +@test "partitionInit/Job: sets TLS flags when global.tls.enabled" { + cd `chart_dir` + local command=$(helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.enabled=false' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.adminPartitions.name=bar' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual + actual=$(echo $command | jq -r '. | any(contains("-use-https"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + actual=$(echo $command | jq -r '. | any(contains("-consul-ca-cert=/consul/tls/ca/tls.crt"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + actual=$(echo $command | jq -r '. | any(contains("-server-port=8501"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partitionInit/Job: does not set consul ca cert or server-port when .externalServers.useSystemRoots is true" { + cd `chart_dir` + local command=$(helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.enabled=false' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=bar' \ + --set 'global.tls.enabled=true' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo' \ + --set 'externalServers.useSystemRoots=true' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual + actual=$(echo $command | jq -r '. | any(contains("-consul-ca-cert=/consul/tls/ca/tls.crt"))' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "partitionInit/Job: can overwrite CA secret with the provided one" { + cd `chart_dir` + local ca_cert_volume=$(helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.enabled=false' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=bar' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.caCert.secretName=foo-ca-cert' \ + --set 'global.tls.caCert.secretKey=key' \ + --set 'global.tls.caKey.secretName=foo-ca-key' \ + --set 'global.tls.caKey.secretKey=key' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name=="consul-ca-cert")' | tee /dev/stderr) + + # check that the provided ca cert secret is attached as a volume + local actual + actual=$(echo $ca_cert_volume | jq -r '.secret.secretName' | tee /dev/stderr) + [ "${actual}" = "foo-ca-cert" ] + + # check that the volume uses the provided secret key + actual=$(echo $ca_cert_volume | jq -r '.secret.items[0].key' | tee /dev/stderr) + [ "${actual}" = "key" ] +} + +#-------------------------------------------------------------------- +# global.acls.bootstrapToken + +@test "partitionInit/Job: HTTP_TOKEN is set when global.acls.bootstrapToken is provided" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.enabled=false' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=bar' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo' \ + --set 'global.acls.bootstrapToken.secretName=partition-token' \ + --set 'global.acls.bootstrapToken.secretKey=token' \ + . | tee /dev/stderr | + yq '[.spec.template.spec.containers[0].env[].name] | any(contains("CONSUL_HTTP_TOKEN"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# partition reserved name + +@test "partitionInit/Job: fails when adminPartitions.name=system" { + reservedNameTest "system" +} + +@test "partitionInit/Job: fails when adminPartitions.name=universal" { + reservedNameTest "universal" +} + +@test "partitionInit/Job: fails when adminPartitions.name=consul" { + reservedNameTest "consul" +} + +@test "partitionInit/Job: fails when adminPartitions.name=operator" { + reservedNameTest "operator" +} + +@test "partitionInit/Job: fails when adminPartitions.name=root" { + reservedNameTest "root" +} + +# reservedNameTest is a helper function that tests if certain partition names +# fail because the name is reserved. +reservedNameTest() { + cd `chart_dir` + local -r name="$1" + run helm template \ + -s templates/partition-init-job.yaml \ + --set 'global.enabled=false' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo' \ + --set 'global.adminPartitions.enabled=true' \ + --set "global.adminPartitions.name=$name" . + + [ "$status" -eq 1 ] + [[ "$output" =~ "The name $name set for key global.adminPartitions.name is reserved by Consul for future use" ]] +} diff --git a/charts/consul/test/unit/partition-init-podsecuritypolicy.bats b/charts/consul/test/unit/partition-init-podsecuritypolicy.bats new file mode 100644 index 0000000000..d00c915f6e --- /dev/null +++ b/charts/consul/test/unit/partition-init-podsecuritypolicy.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +load _helpers + +@test "partitionInit/PodSecurityPolicy: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-podsecuritypolicy.yaml \ + . +} + +@test "partitionInit/PodSecurityPolicy: enabled with global.adminPartitions.enabled=true and servers = false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-init-podsecuritypolicy.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partitionInit/PodSecurityPolicy: disabled with global.adminPartitions.enabled=true and servers = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-podsecuritypolicy.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} + +@test "partitionInit/PodSecurityPolicy: disabled with global.adminPartitions.enabled=true and global.enabled = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-podsecuritypolicy.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enabled=true' \ + . +} + +@test "partitionInit/PodSecurityPolicy: disabled with global.adminPartitions.enabled=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-podsecuritypolicy.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} \ No newline at end of file diff --git a/charts/consul/test/unit/partition-init-role.bats b/charts/consul/test/unit/partition-init-role.bats new file mode 100644 index 0000000000..c434aa3d87 --- /dev/null +++ b/charts/consul/test/unit/partition-init-role.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +load _helpers + +@test "partitionInit/Role: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + . +} + +@test "partitionInit/Role: enabled with global.adminPartitions.enabled=true and servers = false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partitionInit/Role: disabled with global.adminPartitions.enabled=true and servers = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} + +@test "partitionInit/Role: disabled with global.adminPartitions.enabled=true and global.enabled = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enabled=true' \ + . +} + +@test "partitionInit/Role: disabled with global.adminPartitions.enabled=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} \ No newline at end of file diff --git a/charts/consul/test/unit/partition-init-rolebinding.bats b/charts/consul/test/unit/partition-init-rolebinding.bats new file mode 100644 index 0000000000..d96f6e6cd3 --- /dev/null +++ b/charts/consul/test/unit/partition-init-rolebinding.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +load _helpers + +@test "partitionInit/RoleBinding: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-rolebinding.yaml \ + . +} + +@test "partitionInit/RoleBinding: enabled with global.adminPartitions.enabled=true and servers = false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-init-rolebinding.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partitionInit/RoleBinding: disabled with global.adminPartitions.enabled=true and servers = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-rolebinding.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} + +@test "partitionInit/RoleBinding: disabled with global.adminPartitions.enabled=true and global.enabled = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-rolebinding.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enabled=true' \ + . +} + +@test "partitionInit/RoleBinding: disabled with global.adminPartitions.enabled=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-rolebinding.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} \ No newline at end of file diff --git a/charts/consul/test/unit/partition-init-serviceaccount.bats b/charts/consul/test/unit/partition-init-serviceaccount.bats new file mode 100644 index 0000000000..6195969686 --- /dev/null +++ b/charts/consul/test/unit/partition-init-serviceaccount.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +load _helpers + +@test "partitionInit/ServiceAccount: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-serviceaccount.yaml \ + . +} + +@test "partitionInit/ServiceAccount: enabled with global.adminPartitions.enabled=true and servers = false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-init-serviceaccount.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partitionInit/ServiceAccount: disabled with global.adminPartitions.enabled=true and servers = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-serviceaccount.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} + +@test "partitionInit/ServiceAccount: disabled with global.adminPartitions.enabled=true and global.enabled = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-serviceaccount.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enabled=true' \ + . +} + +@test "partitionInit/ServiceAccount: disabled with global.adminPartitions.enabled=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-serviceaccount.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} \ No newline at end of file diff --git a/charts/consul/test/unit/partition-name-configmap.bats b/charts/consul/test/unit/partition-name-configmap.bats new file mode 100644 index 0000000000..e516c9ae13 --- /dev/null +++ b/charts/consul/test/unit/partition-name-configmap.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats + +load _helpers + +@test "partitionName/ConfigMap: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + . +} + +@test "partitionName/ConfigMap: enabled with global.adminPartitions.enabled=true and servers = false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partitionName/ConfigMap: disabled with global.adminPartitions.enabled=true and servers = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=true' \ + . +} + +@test "partitionName/ConfigMap: disabled with global.adminPartitions.enabled=true and global.enabled = true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enabled=true' \ + . +} + +@test "partitionName/ConfigMap: disabled with global.adminPartitions.enabled=false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-init-role.yaml \ + --set 'global.adminPartitions.enabled=false' \ + . +} \ No newline at end of file diff --git a/charts/consul/test/unit/partition-service.bats b/charts/consul/test/unit/partition-service.bats new file mode 100755 index 0000000000..b772b32d5e --- /dev/null +++ b/charts/consul/test/unit/partition-service.bats @@ -0,0 +1,133 @@ +#!/usr/bin/env bats + +load _helpers + +@test "partition/Service: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-service.yaml \ + . +} + +@test "partition/Service: enable with global.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-service.yaml \ + --set 'global.enabled=false' \ + --set 'server.enabled=true' \ + --set 'global.adminPartitions.enabled=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "partition/Service: disable with adminPartitions.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=false' \ + . +} + +@test "partition/Service: disable with server.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'server.enabled=false' \ + . +} + +@test "partition/Service: disable with global.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/partition-service.yaml \ + --set 'global.enabled=false' \ + . +} + +#-------------------------------------------------------------------- +# annotations + +@test "partition/Service: no annotations by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=true' \ + . | tee /dev/stderr | + yq -r '.metadata.annotations | length' | tee /dev/stderr) + [ "${actual}" = "0" ] +} + +@test "partition/Service: can set annotations" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.service.annotations=key: value' \ + . | tee /dev/stderr | + yq -r '.metadata.annotations.key' | tee /dev/stderr) + [ "${actual}" = "value" ] +} + +#-------------------------------------------------------------------- +# nodePort + +@test "partition/Service: RPC node port can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.service.type=NodePort' \ + --set 'global.adminPartitions.service.nodePort.rpc=4443' \ + . | tee /dev/stderr | + yq -r '.spec.ports[] | select(.name == "server") | .nodePort' | tee /dev/stderr) + [ "${actual}" == "4443" ] +} + +@test "partition/Service: Serf node port can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.service.type=NodePort' \ + --set 'global.adminPartitions.service.nodePort.serf=4444' \ + . | tee /dev/stderr | + yq -r '.spec.ports[] | select(.name == "serflan") | .nodePort' | tee /dev/stderr) + [ "${actual}" == "4444" ] +} + +@test "partition/Service: HTTPS node port can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.service.type=NodePort' \ + --set 'global.adminPartitions.service.nodePort.https=4444' \ + . | tee /dev/stderr | + yq -r '.spec.ports[] | select(.name == "https") | .nodePort' | tee /dev/stderr) + [ "${actual}" == "4444" ] +} + +@test "partition/Service: RPC, Serf and HTTPS node ports can be set" { + cd `chart_dir` + local ports=$(helm template \ + -s templates/partition-service.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.service.type=NodePort' \ + --set 'global.adminPartitions.service.nodePort.rpc=4443' \ + --set 'global.adminPartitions.service.nodePort.https=4444' \ + --set 'global.adminPartitions.service.nodePort.serf=4445' \ + . | tee /dev/stderr | + yq -r '.spec.ports[]' | tee /dev/stderr) + + local actual + actual=$(echo $ports | jq -r 'select(.name == "server") | .nodePort' | tee /dev/stderr) + [ "${actual}" == "4443" ] + + actual=$(echo $ports | jq -r 'select(.name == "https") | .nodePort' | tee /dev/stderr) + [ "${actual}" == "4444" ] + + actual=$(echo $ports | jq -r 'select(.name == "serflan") | .nodePort' | tee /dev/stderr) + [ "${actual}" == "4445" ] +} diff --git a/charts/consul/test/unit/server-acl-init-job.bats b/charts/consul/test/unit/server-acl-init-job.bats index 4fd6867d17..7eb37a2329 100644 --- a/charts/consul/test/unit/server-acl-init-job.bats +++ b/charts/consul/test/unit/server-acl-init-job.bats @@ -204,40 +204,18 @@ load _helpers #-------------------------------------------------------------------- # enterpriseLicense -@test "serverACLInit/Job: ent license acl option enabled with server.enterpriseLicense.secretName and server.enterpriseLicense.secretKey set" { +@test "serverACLInit/Job: ent license acl option enabled with global.enterpriseLicense.secretName and global.enterpriseLicense.secretKey set" { cd `chart_dir` local actual=$(helm template \ -s templates/server-acl-init-job.yaml \ --set 'global.acls.manageSystemACLs=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq '.spec.template.spec.containers[0].command | any(contains("-create-enterprise-license-token"))' | tee /dev/stderr) [ "${actual}" = "true" ] } -@test "serverACLInit/Job: ent license acl option disabled missing server.enterpriseLicense.secretName" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/server-acl-init-job.yaml \ - --set 'global.acls.manageSystemACLs=true' \ - --set 'server.enterpriseLicense.secretKey=bar' \ - . | tee /dev/stderr | - yq '.spec.template.spec.containers[0].command | any(contains("-create-enterprise-license-token"))' | tee /dev/stderr) - [ "${actual}" = "false" ] -} - -@test "serverACLInit/Job: ent license acl option disabled missing server.enterpriseLicense.secretKey" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/server-acl-init-job.yaml \ - --set 'global.acls.manageSystemACLs=true' \ - --set 'server.enterpriseLicense.secretName=foo' \ - . | tee /dev/stderr | - yq '.spec.template.spec.containers[0].command | any(contains("-create-enterprise-license-token"))' | tee /dev/stderr) - [ "${actual}" = "false" ] -} - #-------------------------------------------------------------------- # client.snapshotAgent @@ -593,6 +571,241 @@ load _helpers [ "${actual}" = "key" ] } +@test "serverACLInit/Job: configures server CA to come from vault when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.server.serverCert.secretName=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check annotations + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-pre-populate-only"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + [ "${actual}" = "carole" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = "foo" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = $'{{- with secret \"foo\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' ] + + # Check that the consul-ca-cert volume is not attached + local actual=$(echo $object | jq -r '.spec.volumes') + [ "${actual}" = "null" ] + + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name="post-install-job").volumeMounts') + [ "${actual}" = "null" ] +} + +@test "serverACLInit/Job: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.server.serverCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "serverACLInit/Job: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.server.serverCert.secretName=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "serverACLInit/Job: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.server.serverCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "serverACLInit/Job: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.server.serverCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +#-------------------------------------------------------------------- +# Replication token in Vault + +@test "serverACLInit/Job: vault replication token can be provided" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.manageSystemACLsRole=acl-role' \ + --set 'global.acls.replicationToken.secretName=/vault/secret' \ + --set 'global.acls.replicationToken.secretKey=token' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check that the role is set. + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/role"') + [ "${actual}" = "acl-role" ] + + # Check Vault secret annotations. + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-inject-secret-replication-token"') + [ "${actual}" = "/vault/secret" ] + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-inject-template-replication-token"') + local expected=$'{{- with secret \"/vault/secret\" -}}\n{{- .Data.data.token -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + # Check that replication token Kubernetes secret volumes and volumeMounts are not attached. + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-inject-secret-replication-token"') + [ "${actual}" = "/vault/secret" ] + + local actual=$(echo $object | jq -r '.spec.volumes') + [ "${actual}" = "null" ] + + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name="post-install-job").volumeMounts') + [ "${actual}" = "null" ] + + # Check that the replication token flag is set to the path of the Vault secret. + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name="post-install-job").command | any(contains("-acl-replication-token-file=/vault/secrets/replication-token"))') + [ "${actual}" = "true" ] +} + +@test "serverACLInit/Job: manageSystemACLsRole is required when Vault is enabled and replication token is set" { + cd `chart_dir` + run helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.acls.replicationToken.secretName=/vault/secret' \ + --set 'global.acls.replicationToken.secretKey=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.secretsBackend.vault.manageSystemACLsRole must be set if global.secretsBackend.vault.enabled is true and global.acls.replicationToken is provided" ]] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "serverACLInit/Job: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "serverACLInit/Job: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + #-------------------------------------------------------------------- # namespaces @@ -995,6 +1208,45 @@ load _helpers [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# admin partitions + +@test "serverACLInit/Job: admin partitions disabled by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo $object | + yq 'any(contains("enable-partitions"))' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(echo $object | + yq 'any(contains("partition"))' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "serverACLInit/Job: admin partitions enabled when admin partitions are enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo $object | + yq 'any(contains("enable-partitions"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | + yq 'any(contains("partition"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + #-------------------------------------------------------------------- # global.acls.createReplicationToken @@ -1022,59 +1274,31 @@ load _helpers #-------------------------------------------------------------------- # global.acls.replicationToken -@test "serverACLInit/Job: -acl-replication-token-file is not set by default" { +@test "serverACLInit/Job: replicationToken.secretKey is required when replicationToken.secretName is set" { cd `chart_dir` - local object=$(helm template \ + run helm template \ -s templates/server-acl-init-job.yaml \ --set 'global.acls.manageSystemACLs=true' \ - . | tee /dev/stderr) - - # Test the flag is not set. - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].command | any(contains("-acl-replication-token-file"))' | tee /dev/stderr) - [ "${actual}" = "false" ] - - # Test the volume doesn't exist - local actual=$(echo "$object" | - yq '.spec.template.spec.volumes | length == 0' | tee /dev/stderr) - [ "${actual}" = "true" ] - - # Test the volume mount doesn't exist - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].volumeMounts | length == 0' | tee /dev/stderr) - [ "${actual}" = "true" ] + --set 'global.acls.replicationToken.secretName=name' \ . + [ "$status" -eq 1 ] + [[ "$output" =~ "both global.acls.replicationToken.secretKey and global.acls.replicationToken.secretName must be set if one of them is provided" ]] } -@test "serverACLInit/Job: -acl-replication-token-file is not set when acls.replicationToken.secretName is set but secretKey is not" { +@test "serverACLInit/Job: replicationToken.secretName is required when replicationToken.secretKey is set" { cd `chart_dir` - local object=$(helm template \ + run helm template \ -s templates/server-acl-init-job.yaml \ --set 'global.acls.manageSystemACLs=true' \ - --set 'global.acls.replicationToken.secretName=name' \ - . | tee /dev/stderr) - - # Test the flag is not set. - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].command | any(contains("-acl-replication-token-file"))' | tee /dev/stderr) - [ "${actual}" = "false" ] - - # Test the volume doesn't exist - local actual=$(echo "$object" | - yq '.spec.template.spec.volumes | length == 0' | tee /dev/stderr) - [ "${actual}" = "true" ] - - # Test the volume mount doesn't exist - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].volumeMounts | length == 0' | tee /dev/stderr) - [ "${actual}" = "true" ] + --set 'global.acls.replicationToken.secretKey=key' \ . + [ "$status" -eq 1 ] + [[ "$output" =~ "both global.acls.replicationToken.secretKey and global.acls.replicationToken.secretName must be set if one of them is provided" ]] } -@test "serverACLInit/Job: -acl-replication-token-file is not set when acls.replicationToken.secretKey is set but secretName is not" { +@test "serverACLInit/Job: -acl-replication-token-file is not set by default" { cd `chart_dir` local object=$(helm template \ -s templates/server-acl-init-job.yaml \ --set 'global.acls.manageSystemACLs=true' \ - --set 'global.acls.replicationToken.secretKey=key' \ . | tee /dev/stderr) # Test the flag is not set. diff --git a/charts/consul/test/unit/server-config-configmap.bats b/charts/consul/test/unit/server-config-configmap.bats index 25a08da37c..6fddfe7be2 100755 --- a/charts/consul/test/unit/server-config-configmap.bats +++ b/charts/consul/test/unit/server-config-configmap.bats @@ -86,6 +86,28 @@ load _helpers [ "${actual}" = "true" ] } +@test "server/ConfigMap: does not create ui config when .ui.enabled=false and .ui.metrics.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'ui.enabled=false' \ + --set 'ui.metrics.enabled=false' \ + . | tee /dev/stderr | + yq -r '.data["ui-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: does not create ui config when .ui.enabled=true and .global.metrics.enabled=false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'ui.enabled=true' \ + --set 'global.metrics.enabled=false' \ + . | tee /dev/stderr | + yq -r '.data["ui-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + @test "server/ConfigMap: does not create ui config when .ui.enabled=true and .ui.metrics.enabled=false" { cd `chart_dir` local actual=$(helm template \ @@ -146,36 +168,366 @@ load _helpers [ "${actual}" = "null" ] } -@test "server/ConfigMap: enable_token_replication is not set when acls.replicationToken.secretName is set but secretKey is not" { +@test "server/ConfigMap: enable_token_replication is set when acls.replicationToken.secretKey and secretName are set" { cd `chart_dir` local actual=$(helm template \ -s templates/server-config-configmap.yaml \ --set 'global.acls.manageSystemACLs=true' \ --set 'global.acls.replicationToken.secretName=name' \ + --set 'global.acls.replicationToken.secretKey=key' \ . | tee /dev/stderr | yq -r '.data["acl-config.json"]' | yq -r '.acl.enable_token_replication' | tee /dev/stderr) - [ "${actual}" = "null" ] + [ "${actual}" = "true" ] } -@test "server/ConfigMap: enable_token_replication is not set when acls.replicationToken.secretKey is set but secretName is not" { +#-------------------------------------------------------------------- +# Vault Connect CA + +@test "server/ConfigMap: doesn't add connect CA config by default" { cd `chart_dir` local actual=$(helm template \ -s templates/server-config-configmap.yaml \ - --set 'global.acls.manageSystemACLs=true' \ - --set 'global.acls.replicationToken.secretKey=key' \ . | tee /dev/stderr | - yq -r '.data["acl-config.json"]' | yq -r '.acl.enable_token_replication' | tee /dev/stderr) - [ "${actual}" = "null" ] + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] } -@test "server/ConfigMap: enable_token_replication is set when acls.replicationToken.secretKey and secretName are set" { +@test "server/ConfigMap: doesn't add connect CA config when vault is enabled but vault address, root and int PKI paths are not set" { cd `chart_dir` local actual=$(helm template \ -s templates/server-config-configmap.yaml \ - --set 'global.acls.manageSystemACLs=true' \ - --set 'global.acls.replicationToken.secretName=name' \ - --set 'global.acls.replicationToken.secretKey=key' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ . | tee /dev/stderr | - yq -r '.data["acl-config.json"]' | yq -r '.acl.enable_token_replication' | tee /dev/stderr) + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't add connect CA config when vault is enabled and vault address is set, but root and int PKI paths are not set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't add connect CA config when vault is enabled and root pki path is set, but vault address and int PKI paths are not set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't add connect CA config when vault is enabled and int path is set, but vault address and root PKI paths are not set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't add connect CA config when vault is enabled and root and int paths are set, but vault address is not set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't add connect CA config when vault is enabled and root path and address are set, but int path is not set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't add connect CA config when vault is enabled and int path and address are set, but root path is not set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.intPKIPath=int' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.intPKIPath=int' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: adds connect CA config when vault is enabled and connect CA are configured" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"]' | tee /dev/stderr) + [ "${actual}" = '"{\n \"connect\": [\n {\n \"ca_config\": [\n {\n \"address\": \"example.com\",\n \"intermediate_pki_path\": \"int\",\n \"root_pki_path\": \"root\",\n \"auth_method\": {\n \"type\": \"kubernetes\",\n \"mount_path\": \"kubernetes\",\n \"params\": {\n \"role\": \"foo\"\n }\n }\n }\n ],\n \"ca_provider\": \"vault\"\n }\n ]\n}\n"' ] + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"]' | tee /dev/stderr) + [ "${actual}" = '"{}\n"' ] +} + +@test "server/ConfigMap: can set additional connect CA config" { + cd `chart_dir` + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + --set 'global.secretsBackend.vault.connectCA.additionalConfig="{\"hello\": \"world\"}"' \ + . | tee /dev/stderr | + yq '.data["additional-connect-ca-config.json"]' | tee /dev/stderr) + [ "${actual}" = '"{\"hello\": \"world\"}\n"' ] +} + +@test "server/ConfigMap: can set auth method mount path" { + cd `chart_dir` + + local caConfig=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + --set 'global.secretsBackend.vault.connectCA.authMethodPath=kubernetes2' \ + . | tee /dev/stderr | + yq -r '.data["connect-ca-config.json"]' | tee /dev/stderr) + + local actual=$(echo $caConfig | jq -r .connect[0].ca_config[0].auth_method.mount_path) + [ "${actual}" = "kubernetes2" ] +} + +@test "server/ConfigMap: doesn't set Vault CA cert in connect CA config by default" { + cd `chart_dir` + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | contains("\"ca_file\": \"/consul/vault-ca/tls.crt\"")' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't set Vault CA cert in connect CA config when vault CA secret name is set but secret key is not" { + cd `chart_dir` + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | contains("\"ca_file\": \"/consul/vault-ca/tls.crt\"")' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't set Vault CA cert in connect CA config when vault CA secret key is set but secret name is not" { + cd `chart_dir` + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | contains("\"ca_file\": \"/consul/vault-ca/tls.crt\"")' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: doesn't set Vault CA cert in connect CA config when both vault CA secret name and key are set" { + cd `chart_dir` + + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.connectCA.address=example.com' \ + --set 'global.secretsBackend.vault.connectCA.rootPKIPath=root' \ + --set 'global.secretsBackend.vault.connectCA.intermediatePKIPath=int' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq '.data["connect-ca-config.json"] | contains("\"ca_file\": \"/consul/vault-ca/tls.crt\"")' | tee /dev/stderr) [ "${actual}" = "true" ] } + +@test "server/ConfigMap: doesn't add federation config by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + . | tee /dev/stderr | + yq '.data["federation-config.json"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/ConfigMap: adds empty federation config when global.federation.enabled is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.federation.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq '.data["federation-config.json"]' | tee /dev/stderr) + [ "${actual}" = '"{\n \"primary_datacenter\": \"\",\n \"primary_gateways\": []\n}"' ] +} + +@test "server/ConfigMap: can set primary dc and gateways when global.federation.enabled is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-config-configmap.yaml \ + --set 'global.federation.enabled=true' \ + --set 'global.federation.primaryDatacenter=dc1' \ + --set 'global.federation.primaryGateways[0]=1.1.1.1:443' \ + --set 'global.federation.primaryGateways[1]=2.2.2.2:443' \ + --set 'global.tls.enabled=true' \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq '.data["federation-config.json"]' | tee /dev/stderr) + [ "${actual}" = '"{\n \"primary_datacenter\": \"dc1\",\n \"primary_gateways\": [\"1.1.1.1:443\",\"2.2.2.2:443\"]\n}"' ] +} \ No newline at end of file diff --git a/charts/consul/test/unit/server-disruptionbudget.bats b/charts/consul/test/unit/server-disruptionbudget.bats index db6ae1bca1..eb076ac775 100755 --- a/charts/consul/test/unit/server-disruptionbudget.bats +++ b/charts/consul/test/unit/server-disruptionbudget.bats @@ -127,7 +127,7 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/server-disruptionbudget.yaml \ - --api-versions 'policy/v1' \ + --api-versions 'policy/v1/PodDisruptionBudget' \ . | tee /dev/stderr | yq -r '.apiVersion' | tee /dev/stderr) [ "${actual}" = "policy/v1" ] diff --git a/charts/consul/test/unit/server-podsecuritypolicy.bats b/charts/consul/test/unit/server-podsecuritypolicy.bats index c71c5e9a00..a87980ee80 100644 --- a/charts/consul/test/unit/server-podsecuritypolicy.bats +++ b/charts/consul/test/unit/server-podsecuritypolicy.bats @@ -27,3 +27,29 @@ load _helpers yq -s 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# server.exposeGossipAndRPCPorts + +@test "server/PodSecurityPolicy: hostPort 8300, 8301 and 8302 allowed when exposeGossipAndRPCPorts=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-podsecuritypolicy.yaml \ + --set 'global.enablePodSecurityPolicies=true' \ + --set 'server.exposeGossipAndRPCPorts=true' \ + . | tee /dev/stderr | + yq -c '.spec.hostPorts' | tee /dev/stderr) + [ "${actual}" = '[{"min":8300,"max":8300},{"min":8301,"max":8301},{"min":8302,"max":8302}]' ] +} + +@test "server/PodSecurityPolicy: hostPort 8300, server.ports.serflan.port and 8302 allowed when exposeGossipAndRPCPorts=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-podsecuritypolicy.yaml \ + --set 'global.enablePodSecurityPolicies=true' \ + --set 'server.exposeGossipAndRPCPorts=true' \ + --set 'server.ports.serflan.port=8333' \ + . | tee /dev/stderr | + yq -c '.spec.hostPorts' | tee /dev/stderr) + [ "${actual}" = '[{"min":8300,"max":8300},{"min":8333,"max":8333},{"min":8302,"max":8302}]' ] +} diff --git a/charts/consul/test/unit/server-statefulset.bats b/charts/consul/test/unit/server-statefulset.bats index 78dd6a873e..fc08cdcc3f 100755 --- a/charts/consul/test/unit/server-statefulset.bats +++ b/charts/consul/test/unit/server-statefulset.bats @@ -52,6 +52,21 @@ load _helpers [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# admin-partitions + +@test "server/StatefulSet: federation and admin partitions cannot be enabled together" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.federation.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "If global.federation.enabled is true, global.adminPartitions.enabled must be false because they are mutually exclusive" ]] +} + #-------------------------------------------------------------------- # image @@ -138,6 +153,28 @@ load _helpers [ "${actual}" = "2" ] } +#-------------------------------------------------------------------- +# volumeClaim name + +@test "server/StatefulSet: no truncation for namespace <= 58 chars" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr | + yq -r '.spec.volumeClaimTemplates[0].metadata.name' | tee /dev/stderr) + [ "${actual}" = "data-default" ] +} + +@test "server/StatefulSet: truncation for namespace > 58 chars" { + cd `chart_dir` + local actual=$(helm template \ + -n really-really-really-really-really-really-really-long-namespace \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr | + yq -r '.spec.volumeClaimTemplates[0].metadata.name' | tee /dev/stderr) + [ "${actual}" = "data-really-really-really-really-really-really-really-long-name" ] +} + #-------------------------------------------------------------------- # storageClass @@ -558,6 +595,28 @@ load _helpers [ "${actualBaz}" = "qux" ] } +#-------------------------------------------------------------------- +# DNS + +@test "server/StatefulSet: recursor flags unset by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("$recursor_flags")' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/StatefulSet: add recursor flags if dns.enableRedirection is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'dns.enableRedirection=true' \ + . | tee /dev/stderr | + yq -c -r '.spec.template.spec.containers[0].command | join(" ") | contains("$recursor_flags")' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + #-------------------------------------------------------------------- # annotations @@ -664,7 +723,7 @@ load _helpers -s templates/server-statefulset.yaml \ . | tee /dev/stderr | yq -r '.spec.template.metadata.annotations."consul.hashicorp.com/config-checksum"' | tee /dev/stderr) - [ "${actual}" = df8f3705556144cfb39ae46653965f84faf85001af69306f74d01793503908f4 ] + [ "${actual}" = 2c5397272acdc6fe5b079bf25c846c5a17f474603c794c64e7226ce0690625f7 ] } @test "server/StatefulSet: adds config-checksum annotation when extraConfig is provided" { @@ -674,7 +733,7 @@ load _helpers --set 'server.extraConfig="{\"hello\": \"world\"}"' \ . | tee /dev/stderr | yq -r '.spec.template.metadata.annotations."consul.hashicorp.com/config-checksum"' | tee /dev/stderr) - [ "${actual}" = a97d7f332bb6585541f1eab2d1782f8b00bd16b883c34b2db3dd3ce7d67ba39e ] + [ "${actual}" = b0d22cb051216505edc0e61b57f9eacc0d7e15b24719d815842df88f06f1abe0 ] } @test "server/StatefulSet: adds config-checksum annotation when config is updated" { @@ -684,7 +743,7 @@ load _helpers --set 'global.acls.manageSystemACLs=true' \ . | tee /dev/stderr | yq -r '.spec.template.metadata.annotations."consul.hashicorp.com/config-checksum"' | tee /dev/stderr) - [ "${actual}" = 023154f44972402c58062dbb8ab09095563dd99c23b9dab9d51d705486e767b7 ] + [ "${actual}" = 7772975be982e25cc8df101375374e2ba672a55737f8f1580011e0d88d8752a8 ] } #-------------------------------------------------------------------- @@ -729,15 +788,6 @@ load _helpers . | tee /dev/stderr | yq '.spec.template.spec.topologySpreadConstraints == "foobar"' | tee /dev/stderr) [ "${actual}" = "true" ] - - # todo: test for Kube versions < 1.18 when helm supports --kube-version flag (https://github.com/helm/helm/pull/9040) - # not supported before 1.18 - # run helm template \ - # -s templates/server-statefulset.yaml \ - # --kube-version "1.17" \ - # . - # [ "$status" -eq 1 ] - # [[ "$output" =~ "`topologySpreadConstraints` requires Kubernetes 1.18 and above." ]] } #-------------------------------------------------------------------- @@ -818,7 +868,7 @@ load _helpers #-------------------------------------------------------------------- # global.openshift.enabled & client.containerSecurityContext -@test "client/DaemonSet: container level securityContexts are not set when global.openshift.enabled=true" { +@test "server/StatefulSet: container level securityContexts are not set when global.openshift.enabled=true" { cd `chart_dir` local manifest=$(helm template \ -s templates/server-statefulset.yaml \ @@ -838,28 +888,45 @@ load _helpers local actual=$(helm template \ -s templates/server-statefulset.yaml \ . | tee /dev/stderr | - yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY") | length > 0' | tee /dev/stderr) + yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY")' | tee /dev/stderr) [ "${actual}" = "" ] } -@test "server/StatefulSet: gossip encryption disabled in server StatefulSet when secretName is missing" { +@test "server/StatefulSet: gossip encryption autogeneration properly sets secretName and secretKey" { cd `chart_dir` local actual=$(helm template \ - -s templates/server-statefulset.yaml \ - --set 'global.gossipEncryption.secretKey=bar' \ - . | tee /dev/stderr | - yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY") | length > 0' | tee /dev/stderr) - [ "${actual}" = "" ] + -s templates/server-statefulset.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY") | .valueFrom.secretKeyRef | [.name=="RELEASE-NAME-consul-gossip-encryption-key", .key="key"] | all' | tee /dev/stderr) + [ "${actual}" = "true" ] } -@test "server/StatefulSet: gossip encryption disabled in server StatefulSet when secretKey is missing" { +@test "server/StatefulSet: gossip encryption key is passed via the -encrypt flag" { cd `chart_dir` local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.gossipEncryption.autoGenerate=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[] | select(.name=="consul") | .command | any(contains("-encrypt=\"${GOSSIP_KEY}\""))' \ + | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "server/StatefulSet: fail if global.gossipEncyption.gossipEncryption.secretName is set but not global.gossipEncyption.secretKey" { + cd `chart_dir` + run helm template \ -s templates/server-statefulset.yaml \ - --set 'global.gossipEncryption.secretName=foo' \ - . | tee /dev/stderr | - yq '.spec.template.spec.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY") | length > 0' | tee /dev/stderr) - [ "${actual}" = "" ] + --set 'global.gossipEncryption.secretName=bar' . + [[ "$output" =~ "gossipEncryption.secretKey and secretName must both be specified." ]] +} + +@test "server/StatefulSet: fail if global.gossipEncyption.gossipEncryption.secretKey is set but not global.gossipEncyption.secretName" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.gossipEncryption.secretKey=bar' . + [[ "$output" =~ "gossipEncryption.secretKey and secretName must both be specified." ]] } @test "server/StatefulSet: gossip environment variable present in server StatefulSet when all config is provided" { @@ -1019,16 +1086,6 @@ load _helpers [ "${actual}" = "true" ] } -@test "server/StatefulSet: CA certificate is specified when TLS is enabled" { - cd `chart_dir` - local actual=$(helm template \ - -s templates/server-statefulset.yaml \ - --set 'global.tls.enabled=true' \ - . | tee /dev/stderr | - yq '.spec.template.spec.containers[0].readinessProbe.exec.command | join(" ") | contains("--cacert /consul/tls/ca/tls.crt")' | tee /dev/stderr) - [ "${actual}" = "true" ] -} - @test "server/StatefulSet: HTTP is disabled in agent when httpsOnly is enabled" { cd `chart_dir` local actual=$(helm template \ @@ -1056,6 +1113,25 @@ load _helpers [ "${actual}" = "/consul/tls/ca/tls.crt" ] } +@test "server/StatefulSet: sets Consul environment variables when global.tls.enabled and global.secretsBackend.vault.enabled" { + cd `chart_dir` + local env=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].env[]' | tee /dev/stderr) + + local actual + actual=$(echo $env | jq -r '. | select(.name == "CONSUL_CACERT") | .value' | tee /dev/stderr) + [ "${actual}" = "/vault/secrets/serverca.crt" ] +} + @test "server/StatefulSet: sets verify_* flags to true by default when global.tls.enabled" { cd `chart_dir` local command=$(helm template \ @@ -1279,8 +1355,8 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/server-statefulset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) [ "${actual}" = '{"name":"consul-license","secret":{"secretName":"foo"}}' ] @@ -1290,8 +1366,8 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/server-statefulset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) [ "${actual}" = '{"name":"consul-license","mountPath":"/consul/license","readOnly":true}' ] @@ -1301,14 +1377,13 @@ load _helpers cd `chart_dir` local actual=$(helm template \ -s templates/server-statefulset.yaml \ - --set 'server.enterpriseLicense.secretName=foo' \ - --set 'server.enterpriseLicense.secretKey=bar' \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=bar' \ . | tee /dev/stderr | yq -r -c '.spec.template.spec.containers[0].env[] | select(.name == "CONSUL_LICENSE_PATH")' | tee /dev/stderr) [ "${actual}" = '{"name":"CONSUL_LICENSE_PATH","value":"/consul/license/bar"}' ] } - @test "server/StatefulSet: -recursor can be set by global.recursors" { cd `chart_dir` local actual=$(helm template \ @@ -1318,3 +1393,652 @@ load _helpers yq -r -c '.spec.template.spec.containers[0].command | join(" ") | contains("-recursor=\"1.2.3.4\"")' | tee /dev/stderr) [ "${actual}" = "true" ] } + +@test "server/StatefulSet: when global.enterpriseLicense.secretKey!=null and global.enterpriseLicense.secretName=null, fail" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.enterpriseLicense.secretName=' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "enterpriseLicense.secretKey and secretName must both be specified." ]] +} + +@test "server/StatefulSet: when global.enterpriseLicense.secretName!=null and global.enterpriseLicense.secretKey=null, fail" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.enterpriseLicense.secretName=foo' \ + --set 'global.enterpriseLicense.secretKey=' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "enterpriseLicense.secretKey and secretName must both be specified." ]] +} +#-------------------------------------------------------------------- +# extraContainers + +@test "server/StatefulSet: adds extra container" { + cd `chart_dir` + + # Test that it defines the extra container + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'server.extraContainers[0].image=test-image' \ + --set 'server.extraContainers[0].name=test-container' \ + --set 'server.extraContainers[0].ports[0].name=test-port' \ + --set 'server.extraContainers[0].ports[0].containerPort=9410' \ + --set 'server.extraContainers[0].ports[0].protocol=TCP' \ + --set 'server.extraContainers[0].env[0].name=TEST_ENV' \ + --set 'server.extraContainers[0].env[0].value=test_env_value' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[] | select(.name == "test-container")' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.name' | tee /dev/stderr) + [ "${actual}" = "test-container" ] + + local actual=$(echo $object | + yq -r '.image' | tee /dev/stderr) + [ "${actual}" = "test-image" ] + + local actual=$(echo $object | + yq -r '.ports[0].name' | tee /dev/stderr) + [ "${actual}" = "test-port" ] + + local actual=$(echo $object | + yq -r '.ports[0].containerPort' | tee /dev/stderr) + [ "${actual}" = "9410" ] + + local actual=$(echo $object | + yq -r '.ports[0].protocol' | tee /dev/stderr) + [ "${actual}" = "TCP" ] + + local actual=$(echo $object | + yq -r '.env[0].name' | tee /dev/stderr) + [ "${actual}" = "TEST_ENV" ] + + local actual=$(echo $object | + yq -r '.env[0].value' | tee /dev/stderr) + [ "${actual}" = "test_env_value" ] + +} + +@test "server/StatefulSet: adds two extra containers" { + cd `chart_dir` + + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'server.extraContainers[0].image=test-image' \ + --set 'server.extraContainers[0].name=test-container' \ + --set 'server.extraContainers[1].image=test-image' \ + --set 'server.extraContainers[1].name=test-container-2' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers | length' | tee /dev/stderr) + + [ "${object}" = 3 ] + +} + +@test "server/StatefulSet: no extra containers added by default" { + cd `chart_dir` + + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers | length' | tee /dev/stderr) + + [ "${object}" = 1 ] +} + +#-------------------------------------------------------------------- +# vault integration + +@test "server/StatefulSet: fail when vault is enabled but the consulServerRole is not provided" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.secretsBackend.vault.consulServerRole must be provided if global.secretsBackend.vault.enabled=true" ]] +} + +@test "server/StatefulSet: fail when vault is enabled with tls but autoencrypt is disabled" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.server.serverCert.secretName=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.tls.enableAutoEncrypt must be true if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" ]] +} + +@test "server/StatefulSet: fail when vault, tls are enabled but no caCert provided" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.tls.enabled=true' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.tls.caCert.secretName must be provided if global.tls.enabled=true and global.secretsBackend.vault.enabled=true." ]] +} + +@test "server/StatefulSet: fail when vault, tls are enabled with a serverCert but no autoencrypt" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.enabled=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.tls.enableAutoEncrypt must be true if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" ]] +} + +@test "server/StatefulSet: fail when vault is enabled with tls but no consulCARole is provided" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.server.serverCert.secretName=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.enabled=true' . + [ "$status" -eq 1 ] + [[ "$output" =~ "global.secretsBackend.vault.consulCARole must be provided if global.secretsBackend.vault.enabled=true and global.tls.enabled=true" ]] +} + +@test "server/StatefulSet: vault annotations not set by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/role"] | length > 0 ' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "server/StatefulSet: vault annotations added when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject"] | length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + [ "${actual}" = "test" ] +} + +@test "server/StatefulSet: vault gossip annotations are correct when enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.gossipEncryption.secretName=path/to/secret' \ + --set 'global.gossipEncryption.secretKey=gossip' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-gossip.txt"]' | tee /dev/stderr) + [ "${actual}" = "path/to/secret" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-gossip.txt"]' | tee /dev/stderr) + local actual="$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-gossip.txt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"path/to/secret\" -}}\n{{- .Data.data.gossip -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "server/StatefulSet: vault no GOSSIP_KEY env variable and command defines GOSSIP_KEY" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.gossipEncryption.secretName=a/b/c/d' \ + --set 'global.gossipEncryption.secretKey=gossip' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.containers[] | select(.name=="consul") | .env[] | select(.name == "GOSSIP_KEY")' | tee /dev/stderr) + [ "${actual}" = "" ] + + local actual=$(echo $object | + yq -r '.containers[] | select(.name=="consul") | .command | any(contains("GOSSIP_KEY="))' \ + | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "server/StatefulSet: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check that the volume is defined. + local actual=$(echo $object | + yq -r '.spec.volumes[] | select(.name=="vault-ca")' | tee /dev/stderr) + [ "${actual}" = "" ] + + # Check that the volume mount is added. + local actual=$(echo $object | + yq -r '.spec.containers[] | select(.name=="consul").volumeMounts[] | select(.name=="vault-ca")' \ + | tee /dev/stderr) + [ "${actual}" = "" ] + + # Check that Vault agent annotations are added. + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "server/StatefulSet: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check that the volume is defined. + local actual=$(echo $object | + yq -r '.spec.volumes[] | select(.name=="vault-ca")' | tee /dev/stderr) + [ "${actual}" = "" ] + + # Check that the volume mount is added. + local actual=$(echo $object | + yq -r '.spec.containers[] | select(.name=="consul").volumeMounts[] | select(.name=="vault-ca")' \ + | tee /dev/stderr) + [ "${actual}" = "" ] + + # Check that Vault agent annotations are added. + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "server/StatefulSet: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check that the volume is defined. + local actual=$(echo $object | + yq -r '.spec.volumes[] | select(.name=="vault-ca")' | tee /dev/stderr) + [ "${actual}" = "" ] + + # Check that the volume mount is added. + local actual=$(echo $object | + yq -r '.spec.containers[] | select(.name=="consul").volumeMounts[] | select(.name=="vault-ca")' \ + | tee /dev/stderr) + [ "${actual}" = "" ] + + # Check that Vault agent annotations are added. + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "server/StatefulSet: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check that the volume is defined. + local actual=$(echo $object | + yq -r '.spec.volumes[] | select(.name=="vault-ca").secret.secretName' | tee /dev/stderr) + [ "${actual}" = "ca" ] + + # Check that the volume mount is added. + local actual=$(echo $object | + yq -r '.spec.containers[] | select(.name=="consul").volumeMounts[] | select(.name=="vault-ca")' \ + | tee /dev/stderr) + [ "${actual}" != "" ] + + # Check that Vault agent annotations are added. + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "server/StatefulSet: vault tls annotations are set when tls is enabled and command modified correctly" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc2' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/cert/ca" ] + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/cert/ca\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-servercert.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/issue/test" ] + + local actual=$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-servercert.crt"]' | tee /dev/stderr) + local expected=$'{{- with secret \"pki_int/issue/test\" \"common_name=server.dc2.consul\"\n\"ttl=1h\" \"alt_names=localhost,RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server.default,*.RELEASE-NAME-consul-server.default.svc,*.server.dc2.consul\" \"ip_sans=127.0.0.1\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-servercert.key"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/issue/test" ] + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-servercert.key"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/issue/test\" \"common_name=server.dc2.consul\"\n\"ttl=1h\" \"alt_names=localhost,RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server.default,*.RELEASE-NAME-consul-server.default.svc,*.server.dc2.consul\" \"ip_sans=127.0.0.1\" -}}\n{{- .Data.private_key -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual=$(echo $object | + yq -r '.spec.containers[0].command | any(contains("ca_file = \"/vault/secrets/serverca.crt\""))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "server/StatefulSet: tls related volumes not attached when tls is enabled on vault" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.volumes[] | select(.name == "consul-ca-cert") | length > 0' | tee /dev/stderr) + [ "${actual}" = "" ] + + local actual=$(echo $object | + yq -r '.containers[0].volumeMounts[] | select(.name == "consul-ca-key")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +@test "server/StatefulSet: vault - can set additional alt_names on server cert when tls is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc2' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.serverAdditionalDNSSANs[0]=*.foo.com' \ + --set 'global.tls.serverAdditionalDNSSANs[1]=*.bar.com' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-servercert.crt"]' | tee /dev/stderr) + local expected=$'{{- with secret \"pki_int/issue/test\" \"common_name=server.dc2.consul\"\n\"ttl=1h\" \"alt_names=localhost,RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server.default,*.RELEASE-NAME-consul-server.default.svc,*.server.dc2.consul,*.foo.com,*.bar.com\" \"ip_sans=127.0.0.1\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-servercert.key"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/issue/test\" \"common_name=server.dc2.consul\"\n\"ttl=1h\" \"alt_names=localhost,RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server.default,*.RELEASE-NAME-consul-server.default.svc,*.server.dc2.consul,*.foo.com,*.bar.com\" \"ip_sans=127.0.0.1\" -}}\n{{- .Data.private_key -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "server/StatefulSet: vault - can set additional ip_sans on server cert when tls is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc2' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.serverAdditionalIPSANs[0]=1.1.1.1' \ + --set 'global.tls.serverAdditionalIPSANs[1]=2.2.2.2' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-servercert.crt"]' | tee /dev/stderr) + local expected=$'{{- with secret \"pki_int/issue/test\" \"common_name=server.dc2.consul\"\n\"ttl=1h\" \"alt_names=localhost,RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server.default,*.RELEASE-NAME-consul-server.default.svc,*.server.dc2.consul\" \"ip_sans=127.0.0.1,1.1.1.1,2.2.2.2\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-servercert.key"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/issue/test\" \"common_name=server.dc2.consul\"\n\"ttl=1h\" \"alt_names=localhost,RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server,*.RELEASE-NAME-consul-server.default,*.RELEASE-NAME-consul-server.default.svc,*.server.dc2.consul\" \"ip_sans=127.0.0.1,1.1.1.1,2.2.2.2\" -}}\n{{- .Data.private_key -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "server/StatefulSet: vault enterprise license annotations are correct when enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=path/to/enterpriselicensesecret' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-enterpriselicense.txt"]' | tee /dev/stderr) + [ "${actual}" = "path/to/enterpriselicensesecret" ] + local actual=$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-enterpriselicense.txt"]' | tee /dev/stderr) + local actual="$(echo $object | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-enterpriselicense.txt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"path/to/enterpriselicensesecret\" -}}\n{{- .Data.data.enterpriselicense -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] +} + +@test "server/StatefulSet: vault CONSUL_LICENSE_PATH is set to /vault/secrets/enterpriselicense.txt" { + cd `chart_dir` + local env=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].env[]' | tee /dev/stderr) + + local actual + + local actual=$(echo $env | jq -r '. | select(.name == "CONSUL_LICENSE_PATH") | .value' | tee /dev/stderr) + [ "${actual}" = "/vault/secrets/enterpriselicense.txt" ] +} + +@test "server/StatefulSet: vault does not add volume for license secret" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r -c '.spec.template.spec.volumes[] | select(.name == "consul-license")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +@test "server/StatefulSet: vault does not add volume mount for license secret" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.enterpriseLicense.secretName=a/b/c/d' \ + --set 'global.enterpriseLicense.secretKey=enterpriselicense' \ + . | tee /dev/stderr | + yq -r -c '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-license")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "server/StatefulSet: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."consul.hashicorp.com/config-checksum") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "server/StatefulSet: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + +#-------------------------------------------------------------------- +# Vault replication token + +@test "server/StatefulSet: vault replication token is configured when secret provided and createReplicationToken is false" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.acls.replicationToken.secretName=vault/replication-token' \ + --set 'global.acls.replicationToken.secretKey=token' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check that Vault annotations are set. + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-replication-token-config.hcl"]' | tee /dev/stderr)" + [ "${actual}" = "vault/replication-token" ] + + local actual="$(echo $object | + yq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-replication-token-config.hcl"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"vault/replication-token\" -}}\nacl { tokens { agent = \"{{- .Data.data.token -}}\", replication = \"{{- .Data.data.token -}}\" }}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + # Check that ACL_REPLICATION_TOKEN env var is not provided. + local actual="$(echo $object | yq -r '.spec.containers[] | select(.name=="consul").env[] | select(.name=="ACL_REPLICATION_TOKEN")' | tee /dev/stderr)" + [ "${actual}" = "" ] + + # Check that path to Vault secret config is provided to the command. + local actual="$(echo $object | yq -r '.spec.containers[] | select(.name=="consul").command | any(contains("-config-file=/vault/secrets/replication-token-config.hcl"))' | tee /dev/stderr)" + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# ui.dashboardURLTemplates.service + +@test "server/StatefulSet: dashboard_url_templates not set by default" { + cd `chart_dir` + + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr | + yq -r ".spec.template.spec.containers[0].command | any(contains(\"dashboard_url_templates\"))" | tee /dev/stderr) + + [ "${actual}" = "false" ] +} + +@test "server/StatefulSet: ui.dashboardURLTemplates.service sets the template" { + cd `chart_dir` + + local expected='-hcl='\''ui_config { dashboard_url_templates { service = \"http://localhost:3000/d/WkFEBmF7z/services?orgId=1&var-Service={{Service.Name}}\" } }' + + local actual=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'ui.dashboardURLTemplates.service=http://localhost:3000/d/WkFEBmF7z/services?orgId=1&var-Service={{Service.Name}}' \ + . | tee /dev/stderr | + yq -r ".spec.template.spec.containers[0].command | any(contains(\"$expected\"))" | tee /dev/stderr) + + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/sync-catalog-deployment.bats b/charts/consul/test/unit/sync-catalog-deployment.bats index 904ac699ce..8beead1564 100755 --- a/charts/consul/test/unit/sync-catalog-deployment.bats +++ b/charts/consul/test/unit/sync-catalog-deployment.bats @@ -978,3 +978,198 @@ load _helpers [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# Vault + +@test "syncCatalog/Deployment: configures server CA to come from vault when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check annotations + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + [ "${actual}" = "carole" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = "foo" ] + local actual + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = $'{{- with secret \"foo\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' ] +} + +@test "syncCatalog/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "syncCatalog/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "syncCatalog/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "syncCatalog/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "syncCatalog/Deployment: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "syncCatalog/Deployment: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} + +# consulDestinationNamespace reserved name + +@test "syncCatalog/Deployment: fails when consulDestinationNamespace=system" { + reservedNameTest "system" +} + +@test "syncCatalog/Deployment: fails when consulDestinationNamespace=universal" { + reservedNameTest "universal" +} + +@test "syncCatalog/Deployment: fails when consulDestinationNamespace=consul" { + reservedNameTest "consul" +} + +@test "syncCatalog/Deployment: fails when consulDestinationNamespace=operator" { + reservedNameTest "operator" +} + +@test "syncCatalog/Deployment: fails when consulDestinationNamespace=root" { + reservedNameTest "root" +} + +# reservedNameTest is a helper function that tests if certain Consul destination +# namespace names fail because the name is reserved. +reservedNameTest() { + cd `chart_dir` + local -r name="$1" + run helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set "syncCatalog.consulNamespaces.consulDestinationNamespace=$name" . + + [ "$status" -eq 1 ] + [[ "$output" =~ "The name $name set for key syncCatalog.consulNamespaces.consulDestinationNamespace is reserved by Consul for future use" ]] +} diff --git a/charts/consul/test/unit/terminating-gateways-deployment.bats b/charts/consul/test/unit/terminating-gateways-deployment.bats index b0e82ba4f2..4a23a232ba 100644 --- a/charts/consul/test/unit/terminating-gateways-deployment.bats +++ b/charts/consul/test/unit/terminating-gateways-deployment.bats @@ -83,7 +83,7 @@ load _helpers --set 'connectInject.enabled=true' \ . | tee /dev/stderr | yq -s -r '.[0].spec.template.spec.containers[0].image' | tee /dev/stderr) - [ "${actual}" = "envoyproxy/envoy-alpine:v1.18.4" ] + [ "${actual}" = "envoyproxy/envoy-alpine:v1.20.2" ] } @test "terminatingGateways/Deployment: envoy image can be set using the global value" { @@ -1215,6 +1215,57 @@ EOF [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# partitions + +@test "terminatingGateways/Deployment: partition command flag is not present by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq -s -r '.[0].spec.template.spec.containers[0]' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.command | any(contains("-partition"))' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(echo $object | yq -r '.lifecycle.preStop.exec.command | any(contains("-partition"))' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "terminatingGateways/Deployment: partition command flag is specified through partition name" { + cd `chart_dir` + local object=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.enableConsulNamespaces=true' \ + --set 'global.adminPartitions.enabled=true' \ + --set 'global.adminPartitions.name=default' \ + . | tee /dev/stderr | + yq -s -r '.[0].spec.template.spec.containers[0]' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.command | any(contains("-partition=default"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | yq -r '.lifecycle.preStop.exec.command | any(contains("-partition=default"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "terminatingGateways/Deployment: fails if admin partitions are enabled but namespaces aren't" { + cd `chart_dir` + run helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.enableConsulNamespaces=false' \ + --set 'global.adminPartitions.enabled=true' . + + [ "$status" -eq 1 ] + [[ "$output" =~ "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" ]] +} + #-------------------------------------------------------------------- # multiple gateways @@ -1244,3 +1295,165 @@ EOF local actual=$(echo $object | yq '.[2] | length > 0' | tee /dev/stderr) [ "${actual}" = "false" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "terminatingGateway/Deployment: configures server CA to come from vault when vault is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + # Check annotations + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + [ "${actual}" = "carole" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr) + [ "${actual}" = $'{{- with secret \"foo\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' ] +} + +@test "terminatingGateway/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "terminatingGateway/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "terminatingGateway/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "terminatingGateway/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +#-------------------------------------------------------------------- +# Vault agent annotations + +@test "terminatingGateway/Deployment: no vault agent annotations defined by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/role")' | tee /dev/stderr) + [ "${actual}" = "{}" ] +} + +@test "terminatingGateway/Deployment: vault agent annotations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=test' \ + --set 'global.secretsBackend.vault.consulServerRole=foo' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.consulCARole=carole' \ + --set 'global.secretsBackend.vault.agentAnnotations=foo: bar' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} diff --git a/charts/consul/test/unit/tls-init-cleanup-job.bats b/charts/consul/test/unit/tls-init-cleanup-job.bats index d75abf7d6d..76da65bfe5 100644 --- a/charts/consul/test/unit/tls-init-cleanup-job.bats +++ b/charts/consul/test/unit/tls-init-cleanup-job.bats @@ -58,3 +58,20 @@ load _helpers yq 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInitCleanup/Job: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-cleanup-job.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-cleanup-podsecuritypolicy.bats b/charts/consul/test/unit/tls-init-cleanup-podsecuritypolicy.bats index 34d747aecb..72d1812c69 100644 --- a/charts/consul/test/unit/tls-init-cleanup-podsecuritypolicy.bats +++ b/charts/consul/test/unit/tls-init-cleanup-podsecuritypolicy.bats @@ -47,3 +47,20 @@ load _helpers yq -s 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInitCleanup/PodSecurityPolicy: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-cleanup-podsecuritypolicy.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-cleanup-role.bats b/charts/consul/test/unit/tls-init-cleanup-role.bats index ad3ca7bbe6..cfcf5be7ba 100644 --- a/charts/consul/test/unit/tls-init-cleanup-role.bats +++ b/charts/consul/test/unit/tls-init-cleanup-role.bats @@ -70,3 +70,20 @@ load _helpers [ "${actual}" = "RELEASE-NAME-consul-tls-init-cleanup" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInitCleanup/Role: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-cleanup-role.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-cleanup-rolebinding.bats b/charts/consul/test/unit/tls-init-cleanup-rolebinding.bats index 5fd3632ed4..07cd485852 100644 --- a/charts/consul/test/unit/tls-init-cleanup-rolebinding.bats +++ b/charts/consul/test/unit/tls-init-cleanup-rolebinding.bats @@ -58,3 +58,20 @@ load _helpers yq 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInitCleanup/RoleBinding: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-cleanup-rolebinding.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-cleanup-serviceaccount.bats b/charts/consul/test/unit/tls-init-cleanup-serviceaccount.bats index d2a89e4e48..283c1ad73f 100644 --- a/charts/consul/test/unit/tls-init-cleanup-serviceaccount.bats +++ b/charts/consul/test/unit/tls-init-cleanup-serviceaccount.bats @@ -79,3 +79,20 @@ load _helpers yq -r '.imagePullSecrets[1].name' | tee /dev/stderr) [ "${actual}" = "my-secret2" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInitCleanup/ServiceAccount: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-cleanup-serviceaccount.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-job.bats b/charts/consul/test/unit/tls-init-job.bats index 9c751fa3d8..8db52fcfbd 100644 --- a/charts/consul/test/unit/tls-init-job.bats +++ b/charts/consul/test/unit/tls-init-job.bats @@ -115,3 +115,20 @@ load _helpers actual=$(echo $spec | jq -r '.containers[0].command | join(" ") | contains("consul tls ca create")' | tee /dev/stderr) [ "${actual}" = "false" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInit/Job: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-job.yaml \ + --set 'global.tls.enabled=true' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-podsecuritypolicy.bats b/charts/consul/test/unit/tls-init-podsecuritypolicy.bats index 5ec0729d05..459097a9db 100644 --- a/charts/consul/test/unit/tls-init-podsecuritypolicy.bats +++ b/charts/consul/test/unit/tls-init-podsecuritypolicy.bats @@ -47,3 +47,20 @@ load _helpers yq -s 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInit/PodSecurityPolicy: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-podsecuritypolicy.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-role.bats b/charts/consul/test/unit/tls-init-role.bats index fe601be911..12af5ed2d0 100644 --- a/charts/consul/test/unit/tls-init-role.bats +++ b/charts/consul/test/unit/tls-init-role.bats @@ -70,3 +70,20 @@ load _helpers [ "${actual}" = "RELEASE-NAME-consul-tls-init" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInit/Role: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-role.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-rolebinding.bats b/charts/consul/test/unit/tls-init-rolebinding.bats index cac2a97f53..3085d4b85b 100644 --- a/charts/consul/test/unit/tls-init-rolebinding.bats +++ b/charts/consul/test/unit/tls-init-rolebinding.bats @@ -58,3 +58,20 @@ load _helpers yq 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# Vault + +@test "tlsInit/RoleBinding: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-rolebinding.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/tls-init-serviceaccount.bats b/charts/consul/test/unit/tls-init-serviceaccount.bats index 9eb3560ba7..3acf6f281e 100644 --- a/charts/consul/test/unit/tls-init-serviceaccount.bats +++ b/charts/consul/test/unit/tls-init-serviceaccount.bats @@ -80,3 +80,19 @@ load _helpers [ "${actual}" = "my-secret2" ] } +#-------------------------------------------------------------------- +# Vault + +@test "tlsInit/ServiceAccount: disabled with global.secretsBackend.vault.enabled=true and global.tls.enabled=true" { + cd `chart_dir` + assert_empty helm template \ + -s templates/tls-init-serviceaccount.yaml \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.caCert.secretName=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . +} diff --git a/charts/consul/test/unit/ui-ingress.bats b/charts/consul/test/unit/ui-ingress.bats index 3a4e1b6bd1..e351790aae 100755 --- a/charts/consul/test/unit/ui-ingress.bats +++ b/charts/consul/test/unit/ui-ingress.bats @@ -60,15 +60,7 @@ load _helpers } @test "ui/Ingress: exposes single port 80 when global.tls.enabled=false" { -# todo: test for Kube versions < 1.19 when helm supports --kube-version flag (https://github.com/helm/helm/pull/9040) -# local actual=$(helm template \ -# -s templates/ui-ingress.yaml \ -# --set 'ui.ingress.enabled=true' \ -# --set 'global.tls.enabled=false' \ -# --set 'ui.ingress.hosts[0].host=foo.com' \ -# --kube-version "1.18" \ -# . | tee /dev/stderr | -# yq -r '.spec.rules[0].http.paths[0].backend.servicePort' | tee /dev/stderr) + cd `chart_dir` local actual=$(helm template \ -s templates/ui-ingress.yaml \ --set 'ui.ingress.enabled=true' \ @@ -80,15 +72,7 @@ load _helpers } @test "ui/Ingress: exposes single port 443 when global.tls.enabled=true and global.tls.httpsOnly=true" { -# todo: test for Kube versions < 1.19 when helm supports --kube-version flag (https://github.com/helm/helm/pull/9040) -# local actual=$(helm template \ -# -s templates/ui-ingress.yaml \ -# --set 'ui.ingress.enabled=true' \ -# --set 'global.tls.enabled=true' \ -# --set 'ui.ingress.hosts[0].host=foo.com' \ -# --kube-version "1.18" \ -# . | tee /dev/stderr | -# yq -r '.spec.rules[0].http.paths[0].backend.servicePort' | tee /dev/stderr) + cd `chart_dir` local actual=$(helm template \ -s templates/ui-ingress.yaml \ --set 'ui.ingress.enabled=true' \ @@ -100,16 +84,7 @@ load _helpers } @test "ui/Ingress: exposes the port 80 when global.tls.enabled=true and global.tls.httpsOnly=false" { -# todo: test for Kube versions < 1.19 when helm supports --kube-version flag (https://github.com/helm/helm/pull/9040) -# local actual=$(helm template \ -# -s templates/ui-ingress.yaml \ -# --set 'ui.ingress.enabled=true' \ -# --set 'global.tls.enabled=true' \ -# --set 'global.tls.httpsOnly=false' \ -# --set 'ui.ingress.hosts[0].host=foo.com' \ -# --kube-version "1.18" \ -# . | tee /dev/stderr | -# yq -r '.spec.rules[0].http.paths[0].backend.servicePort' | tee /dev/stderr) + cd `chart_dir` local actual=$(helm template \ -s templates/ui-ingress.yaml \ --set 'ui.ingress.enabled=true' \ @@ -122,16 +97,7 @@ load _helpers } @test "ui/Ingress: exposes the port 443 when global.tls.enabled=true and global.tls.httpsOnly=false" { -# todo: test for Kube versions < 1.19 when helm supports --kube-version flag (https://github.com/helm/helm/pull/9040) -# local actual=$(helm template \ -# -s templates/ui-ingress.yaml \ -# --set 'ui.ingress.enabled=true' \ -# --set 'global.tls.enabled=true' \ -# --set 'global.tls.httpsOnly=false' \ -# --set 'ui.ingress.hosts[0].host=foo.com' \ -# --kube-version "1.18" \ -# . | tee /dev/stderr | -# yq -r '.spec.rules[0].http.paths[1].backend.servicePort' | tee /dev/stderr) + cd `chart_dir` local actual=$(helm template \ -s templates/ui-ingress.yaml \ --set 'ui.ingress.enabled=true' \ @@ -207,6 +173,7 @@ load _helpers # pathtype @test "ui/Ingress: default PathType Prefix" { + cd `chart_dir` local actual=$(helm template \ -s templates/ui-ingress.yaml \ --set 'ui.ingress.enabled=true' \ @@ -218,6 +185,7 @@ load _helpers } @test "ui/Ingress: set PathType ImplementationSpecific" { + cd `chart_dir` local actual=$(helm template \ -s templates/ui-ingress.yaml \ --set 'ui.ingress.enabled=true' \ @@ -228,3 +196,27 @@ load _helpers yq -r '.spec.rules[0].http.paths[0].pathType' | tee /dev/stderr) [ "${actual}" = "ImplementationSpecific" ] } + +#-------------------------------------------------------------------- +# ingressClassName + +@test "ui/Ingress: no ingressClassName by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/ui-ingress.yaml \ + --set 'ui.ingress.enabled=true' \ + . | tee /dev/stderr | + yq -r '.spec.ingressClassName' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "ui/Ingress: can set ingressClassName" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/ui-ingress.yaml \ + --set 'ui.ingress.enabled=true' \ + --set 'ui.ingress.ingressClassName=nginx' \ + . | tee /dev/stderr | + yq -r '.spec.ingressClassName' | tee /dev/stderr) + [ "${actual}" = "nginx" ] +} diff --git a/charts/consul/test/unit/webhook-cert-manager-configmap.bats b/charts/consul/test/unit/webhook-cert-manager-configmap.bats index 62a7a4a5c4..31ec074f8a 100644 --- a/charts/consul/test/unit/webhook-cert-manager-configmap.bats +++ b/charts/consul/test/unit/webhook-cert-manager-configmap.bats @@ -52,7 +52,7 @@ load _helpers local actual=$(echo $cfg | jq '. | length == 1') [ "${actual}" = "true" ] - local actual=$(echo $cfg | jq '.[0].name | contains("controller-mutating-webhook-configuration")') + local actual=$(echo $cfg | jq '.[0].name | contains("controller")') [ "${actual}" = "true" ] } @@ -68,7 +68,7 @@ load _helpers local actual=$(echo $cfg | jq '. | length == 1') [ "${actual}" = "true" ] - local actual=$(echo $cfg | jq '.[0].name | contains("controller-mutating-webhook-configuration")') + local actual=$(echo $cfg | jq '.[0].name | contains("controller")') [ "${actual}" = "false" ] } @@ -85,9 +85,9 @@ load _helpers local actual=$(echo $cfg | jq '. | length == 2') [ "${actual}" = "true" ] - local actual=$(echo $cfg | jq '.[0].name | contains("connect-injector-cfg")') + local actual=$(echo $cfg | jq '.[0].name | contains("connect-injector")') [ "${actual}" = "true" ] - local actual=$(echo $cfg | jq '.[1].name | contains("controller-mutating-webhook-configuration")') + local actual=$(echo $cfg | jq '.[1].name | contains("controller")') [ "${actual}" = "true" ] } \ No newline at end of file diff --git a/charts/consul/test/unit/webhook-cert-manager-deployment.bats b/charts/consul/test/unit/webhook-cert-manager-deployment.bats index d51947aff5..f3118206bd 100644 --- a/charts/consul/test/unit/webhook-cert-manager-deployment.bats +++ b/charts/consul/test/unit/webhook-cert-manager-deployment.bats @@ -39,3 +39,26 @@ load _helpers yq 'length > 0' | tee /dev/stderr) [ "${actual}" = "true" ] } + +@test "webhookCertManager/Deployment: no tolerations by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/webhook-cert-manager-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.tolerations' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "webhookCertManager/Deployment: tolerations can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/webhook-cert-manager-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'webhookCertManager.tolerations=- key: value' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.tolerations[0].key' | tee /dev/stderr) + [ "${actual}" = "value" ] +} diff --git a/charts/consul/values.yaml b/charts/consul/values.yaml index 946c052b09..4d019b36be 100644 --- a/charts/consul/values.yaml +++ b/charts/consul/values.yaml @@ -29,6 +29,49 @@ global: # Consul into Kubernetes will have, e.g. `service-name.service.consul`. domain: consul + # [Enterprise Only] Enabling `adminPartitions` allows creation of Admin Partitions in Kubernetes clusters. + # It additionally indicates that you are running Consul Enterprise v1.11+ with a valid Consul Enterprise + # license. Admin partitions enables deploying services across partitions, while sharing + # a set of Consul servers. + adminPartitions: + # If true, the Helm chart will enable Admin Partitions for the cluster. The clients in the server cluster + # must be installed in the default partition. Creation of Admin Partitions is only supported during installation. + # Admin Partitions cannot be installed via a Helm upgrade operation. Only Helm installs are supported. + enabled: false + # The name of the Admin Partition. The partition name cannot be modified once the partition has been installed. + # Changing the partition name would require an un-install and a re-install with the updated name. + # Must be "default" in the server cluster ie the Kubernetes cluster that the Consul server pods are deployed onto. + name: "default" + # Partition service properties. + service: + type: LoadBalancer + # Optionally set the nodePort value of the partition service if using a NodePort service. + # If not set and using a NodePort service, Kubernetes will automatically assign + # a port. + nodePort: + + # RPC node port + # @type: integer + rpc: null + + # Serf node port + # @type: integer + serf: null + + # HTTPS node port + # @type: integer + https: null + + # Annotations to apply to the partition service. + # + # ```yaml + # annotations: | + # "annotation-key": "annotation-value" + # ``` + # + # @type: string + annotations: null + # The name (and tag) of the Consul Docker image for clients and servers. # This can be overridden per component. This should be pinned to a specific # version tag, otherwise you may inadvertently upgrade your Consul version. @@ -42,7 +85,7 @@ global: # image: "hashicorp/consul-enterprise:1.10.0-ent" # ``` # @default: hashicorp/consul: - image: "hashicorp/consul:1.10.0" + image: "hashicorp/consul:1.11.3" # Array of objects containing image pull secret names that will be applied to each service account. # This can be used to reference image pull secrets if using a custom consul or consul-k8s-control-plane Docker image. @@ -62,7 +105,7 @@ global: # image that is used for functionality such as catalog sync. # This can be overridden per component. # @default: hashicorp/consul-k8s-control-plane: - imageK8S: "hashicorp/consul-k8s-control-plane:0.33.0" + imageK8S: "hashicorp/consul-k8s-control-plane:0.41.1" # The name of the datacenter that the agents should # register as. This can't be changed once the Consul cluster is up and running @@ -74,30 +117,156 @@ global: # created by this chart. See https://kubernetes.io/docs/concepts/policy/pod-security-policy/. enablePodSecurityPolicies: false - # Configures which Kubernetes secret to retrieve Consul's - # gossip encryption key from (see `-encrypt` (https://consul.io/docs/agent/options#_encrypt)). If secretName or - # secretKey are not set, gossip encryption will not be enabled. The secret must - # be in the same namespace that Consul is installed into. - # - # The secret can be created by running: - # - # ```shell + # secretsBackend is used to configure Vault as the secrets backend for the Consul on Kubernetes installation. + # The Vault cluster needs to have the Kubernetes Auth Method, KV2 and PKI secrets engines enabled + # and have necessary secrets, policies and roles created prior to installing Consul. + # See https://www.consul.io/docs/k8s/installation/vault for full instructions. + # + # The Vault cluster _must_ not have the Consul cluster installed by this Helm chart as its storage backend + # as that would cause a circular dependency. + # Vault can have Consul as its storage backend as long as that Consul cluster is not running on this Kubernetes cluster + # and is being managed separately from this Helm installation. + # + # Note: When using Vault KV2 secrets engines the "data" field is implicitly required for Vault API calls, + # secretName should be in the form of "vault-kv2-mount-path/data/secret-name". + # secretKey should be in the form of "key". + secretsBackend: + vault: + # Enabling the Vault secrets backend will replace Kubernetes secrets with referenced Vault secrets. + enabled: false + + # The Vault role for the Consul server. + # The role must be connected to the Consul server's service account and + # have a policy with read capabilities for the following secrets: + # - gossip encryption key defined by `global.gossipEncryption.secretName` + # - certificate issue path defined by `server.serverCert.secretName` + # - CA certificate defined by `global.tls.caCert.secretName` + # - replication token defined by `global.acls.replicationToken.secretName` if `global.federation.enabled` is `true` + # To discover the service account name of the Consul server, run + # ```shell-session + # $ helm template --show-only templates/server-serviceaccount.yaml hashicorp/consul + # ``` + # and check the name of `metadata.name`. + consulServerRole: "" + + # The Vault role for the Consul client. + # The role must be connected to the Consul client's service account and + # have a policy with read capabilities for the following secrets: + # - gossip encryption key defined by `global.gossipEncryption.secretName`. + # To discover the service account name of the Consul client, run + # ```shell-session + # $ helm template --show-only templates/client-serviceaccount.yaml charts/consul + # ``` + # and check the name of `metadata.name`. + consulClientRole: "" + + # A Vault role to allow Kubernetes job that manages ACLs for this Helm chart (`server-acl-init`) + # to read and update Vault secrets for the Consul's bootstrap and replication tokens. + # This role must be bound the `server-acl-init`'s service account. + # To discover the service account name of the `server-acl-init` job, run + # ```shell-session + # $ helm template --show-only templates/server-acl-init-serviceaccount.yaml charts/consul + # ``` + # and check the name of `metadata.name`. + manageSystemACLsRole: "" + + # This value defines additional annotations for + # Vault agent on any pods where it'll be running. + # This should be formatted as a multi-line string. + # + # ```yaml + # annotations: | + # "sample/annotation1": "foo" + # "sample/annotation2": "bar" + # ``` + # + # @type: string + agentAnnotations: null + + # The Vault role for all Consul components to read the Consul's server's CA Certificate (unauthenticated). + # The role should be connected to the service accounts of all Consul components, or alternatively `*` since it + # will be used only against the `pki/cert/ca` endpoint which is unauthenticated. A policy must be created which grants + # read capabilities to `global.tls.caCert.secretName`, which is usually `pki/cert/ca`. + consulCARole: "" + + # Configuration for Vault server CA certificate. This certificate will be mounted + # to any pod where Vault agent needs to run. + ca: + # secretName is the name of the Kubernetes secret that holds the Vault CA certificate. + # A Kubernetes secret must be in the same namespace that Consul is installed into. + secretName: "" + # secretKey is the key within the Kubernetes secret that holds the Vault CA certificate. + secretKey: "" + + # Configuration for the Vault Connect CA provider. + # The provider will be configured to use the Vault Kubernetes auth method + # and therefore requires the role provided by `global.secretsBackend.vault.consulServerRole` + # to have permissions to the root and intermediate PKI paths. + # Please see https://www.consul.io/docs/connect/ca/vault#vault-acl-policies + # for information on how to configure the Vault policies. + connectCA: + # The address of the Vault server. + address: "" + + # The mount path of the Kubernetes auth method in Vault. + authMethodPath: "kubernetes" + + # The path to a PKI secrets engine for the root certificate. + # Please see https://www.consul.io/docs/connect/ca/vault#rootpkipath. + rootPKIPath: "" + + # The path to a PKI secrets engine for the generated intermediate certificate. + # Please see https://www.consul.io/docs/connect/ca/vault#intermediatepkipath. + intermediatePKIPath: "" + + # Additional Connect CA configuration in JSON format. + # Please see https://www.consul.io/docs/connect/ca/vault#common-ca-config-options + # for additional configuration options. + # + # Example: + # + # ```yaml + # additionalConfig: | + # { + # "connect": [{ + # "ca_config": [{ + # "leaf_cert_ttl": "36h" + # }] + # }] + # } + # ``` + additionalConfig: | + {} + + # Configures Consul's gossip encryption key. + # (see `-encrypt` (https://consul.io/docs/agent/options#_encrypt)). + # By default, gossip encryption is not enabled. The gossip encryption key may be set automatically or manually. + # The recommended method is to automatically generate the key. + # To automatically generate and set a gossip encryption key, set autoGenerate to true. + # Values for secretName and secretKey should not be set if autoGenerate is true. + # To manually generate a gossip encryption key, set secretName and secretKey and use Consul to generate + # a key, saving this as a Kubernetes secret or Vault secret path and key. + # If `global.secretsBackend.vault.enabled=true`, be sure to add the "data" component of the secretName path as required by + # the Vault KV-2 secrets engine [see example]. + # + # ```shell-session # $ kubectl create secret generic consul-gossip-encryption-key --from-literal=key=$(consul keygen) # ``` # - # To reference, use: - # - # ```yaml - # global: - # gossipEncryption: - # secretName: consul-gossip-encryption-key - # secretKey: key + # Vault CLI Example: + # ```shell-session + # $ vault kv put consul/secrets/gossip key=$(consul keygen) # ``` + # `gossipEncryption.secretName="consul/data/secrets/gossip"` + # `gossipEncryption.secretKey="key"` + gossipEncryption: - # secretName is the name of the Kubernetes secret that holds the gossip - # encryption key. The secret must be in the same namespace that Consul is installed into. + # Automatically generate a gossip encryption key and save it to a Kubernetes secret. + autoGenerate: false + # secretName is the name of the Kubernetes secret or Vault secret path that holds the gossip + # encryption key. A Kubernetes secret must be in the same namespace that Consul is installed into. secretName: "" - # secretKey is the key within the Kubernetes secret that holds the gossip + # secretKey is the key within the Kubernetes secret or Vault secret key that holds the gossip # encryption key. secretKey: "" @@ -145,15 +314,19 @@ global: # both clients and servers and to only accept HTTPS connections. httpsOnly: true - # A Kubernetes secret containing the certificate of the CA to use for - # TLS communication within the Consul cluster. If you have generated the CA yourself - # with the consul CLI, you could use the following command to create the secret + # A secret containing the certificate of the CA to use for TLS communication within the Consul cluster. + # If you have generated the CA yourself with the consul CLI, you could use the following command to create the secret # in Kubernetes: # - # ```bash - # kubectl create secret generic consul-ca-cert \ + # ```shell-session + # $ kubectl create secret generic consul-ca-cert \ # --from-file='tls.crt=./consul-agent-ca.pem' # ``` + # If you are using Vault as a secrets backend with TLS, `caCert.secretName` must be provided and should reference + # the CA path for your PKI secrets engine. This should be of the form `pki/cert/ca` where `pki` is the mount point of your PKI secrets engine. + # A read policy must be created and associated with the CA cert path for `global.tls.caCert.secretName`. + # This will be consumed by the `global.secretsBackend.vault.consulCARole` role by all Consul components. + # When using Vault the secretKey is not used. caCert: # The name of the Kubernetes secret. secretName: null @@ -165,8 +338,8 @@ global: # with the consul CLI, you could use the following command to create the secret # in Kubernetes: # - # ```bash - # kubectl create secret generic consul-ca-key \ + # ```shell-session + # $ kubectl create secret generic consul-ca-key \ # --from-file='tls.key=./consul-agent-ca-key.pem' # ``` # @@ -219,11 +392,26 @@ global: # and create ACL tokens and policies. # This value is ignored if `bootstrapToken` is also set. replicationToken: - # The name of the Kubernetes secret. + # The name of the Kubernetes secret or the path of the secret in Vault. secretName: null - # The key of the Kubernetes secret. + # The key of the Kubernetes or Vault secret. secretKey: null + # [Enterprise Only] This value refers to a Kubernetes secret that you have created + # that contains your enterprise license. It is required if you are using an + # enterprise binary. Defining it here applies it to your cluster once a leader + # has been elected. If you are not using an enterprise image or if you plan to + # introduce the license key via another route, then set these fields to null. + # Note: the job to apply license runs on both Helm installs and upgrades. + enterpriseLicense: + # secretName is the name of the Kubernetes secret or Vault secret path that holds the enterprise license. + # A Kubernetes secret must be in the same namespace that Consul is installed into. + secretName: null + # secretKey is the key within the Kubernetes secret or Vault secret key that holds the enterprise license. + secretKey: null + # Manages license autoload. Required in Consul 1.10.0+, 1.9.7+ and 1.8.12+. + enableLicenseAutoload: true + # Configure federation. federation: # If enabled, this datacenter will be federation-capable. Only federation @@ -242,6 +430,14 @@ global: # `-consul-federation`. createFederationSecret: false + # The name of the primary datacenter. + primaryDatacenter: "" + + # A list of addresses of the primary mesh gateways in the form `:`. + # (e.g. ["1.1.1.1:443", "2.3.4.5:443"] + # @type: array + primaryGateways: [] + # Configures metrics for Consul service mesh metrics: # Configures the Helm chart’s components @@ -270,9 +466,18 @@ global: # For connect-injected pods, the consul sidecar is responsible for metrics merging. For ingress/mesh/terminating # gateways, it additionally ensures the Consul services are always registered with their local Consul client. - # @recurse: false # @type: map consulSidecarContainer: + # Set default resources for consul sidecar. If null, that resource won't + # be set. + # These settings can be overridden on a per-pod basis via these annotations: + # + # - `consul.hashicorp.com/consul-sidecar-cpu-limit` + # - `consul.hashicorp.com/consul-sidecar-cpu-request` + # - `consul.hashicorp.com/consul-sidecar-memory-limit` + # - `consul.hashicorp.com/consul-sidecar-memory-request` + # @recurse: false + # @type: map resources: requests: memory: "25Mi" @@ -285,7 +490,7 @@ global: # connect-injected sidecar proxies and mesh, terminating, and ingress gateways. # See https://www.consul.io/docs/connect/proxies/envoy for full compatibility matrix between Consul and Envoy. # @default: envoyproxy/envoy-alpine: - imageEnvoy: "envoyproxy/envoy-alpine:v1.18.4" + imageEnvoy: "envoyproxy/envoy-alpine:v1.20.2" # Configuration for running this Helm chart on the Red Hat OpenShift platform. # This Helm chart currently supports OpenShift v4.x+. @@ -326,25 +531,11 @@ server: # @type: int bootstrapExpect: null - # [Enterprise Only] This value refers to a Kubernetes secret that you have created - # that contains your enterprise license. It is required if you are using an - # enterprise binary. Defining it here applies it to your cluster once a leader - # has been elected. If you are not using an enterprise image or if you plan to - # introduce the license key via another route, then set these fields to null. - # Note: the job to apply license runs on both Helm installs and upgrades. - enterpriseLicense: - # The name of the Kubernetes secret that holds the enterprise license. - # The secret must be in the same namespace that Consul is installed into. - secretName: null - # The key within the Kubernetes secret that holds the enterprise license. - secretKey: null - # Manages license autoload. Required in Consul 1.10.0+, 1.9.7+ and 1.8.12+. - enableLicenseAutoload: true - - # A Kubernetes secret containing a certificate & key for the server agents to use + # A secret containing a certificate & key for the server agents to use # for TLS communication within the Consul cluster. Cert needs to be provided with # additional DNS name SANs so that it will work within the Kubernetes cluster: # + # Kubernetes Secrets backend: # ```bash # consul tls cert create -server -days=730 -domain=consul -ca=consul-agent-ca.pem \ # -key=consul-agent-ca-key.pem -dc={{datacenter}} \ @@ -356,8 +547,7 @@ server: # -additional-dnsname="server.{{datacenter}}.{{domain}}" # ``` # - # If you have generated the - # server-cert yourself with the consul CLI, you could use the following command + # If you have generated the server-cert yourself with the consul CLI, you could use the following command # to create the secret in Kubernetes: # # ```bash @@ -365,8 +555,16 @@ server: # --from-file='tls.crt=./dc1-server-consul-0.pem' # --from-file='tls.key=./dc1-server-consul-0-key.pem' # ``` + # + # Vault Secrets backend: + # If you are using Vault as a secrets backend, a Vault Policy must be created which allows `["create", "update"]` + # capabilities on the PKI issuing endpoint, which is usually of the form `pki/issue/consul-server`. + # Please see the following guide for steps to generate a compatible certificate: + # https://learn.hashicorp.com/tutorials/consul/vault-pki-consul-secure-tls + # Note: when using TLS, both the `server.serverCert` and `global.tls.caCert` which points to the CA endpoint of this PKI engine + # must be provided. serverCert: - # The name of the Kubernetes secret. + # The name of the Kubernetes secret or Vault secret path containing the PEM encoded server certificate. secretName: null # Exposes the servers' gossip and RPC ports as hostPorts. To enable a client @@ -404,6 +602,8 @@ server: # storage classes, the PersistentVolumeClaims would need to be manually created. # A `null` value will use the Kubernetes cluster's default StorageClass. If a default # StorageClass does not exist, you will need to create one. + # See https://www.consul.io/docs/install/performance#read-write-tuning for considerations around choosing a + # performant storage class. # @type: string storageClass: null @@ -519,7 +719,7 @@ server: # # This can also be set using Helm's `--set` flag using the following syntax: # - # ```shell + # ```shell-session # --set 'server.extraConfig="{"log_level": "DEBUG"}"' # ``` extraConfig: | @@ -553,6 +753,19 @@ server: # @type: array extraVolumes: [] + # A list of sidecar containers. + # Example: + # + # ```yaml + # extraContainers: + # - name: extra-container + # image: example-image:latest + # command: + # - ... + # ``` + # @type: array + extraContainers: [] + # This value defines the affinity (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity) # for server pods. It defaults to allowing only a single server pod on each node, which # minimizes risk of the cluster becoming unusable if a node is lost. If you need @@ -713,8 +926,8 @@ externalServers: # # You could retrieve this value from your `kubeconfig` by running: # - # ```shell - # kubectl config view \ + # ```shell-session + # $ kubectl config view \ # -o jsonpath="{.clusters[?(@.name=='')].cluster.server}" # ``` # @@ -848,7 +1061,7 @@ client: # # This can also be set using Helm's `--set` flag using the following syntax: # - # ```shell + # ```shell-session # --set 'client.extraConfig="{"log_level": "DEBUG"}"' # ``` extraConfig: | @@ -882,6 +1095,19 @@ client: # @type: array extraVolumes: [] + # A list of sidecar containers. + # Example: + # + # ```yaml + # extraContainers: + # - name: extra-container + # image: example-image:latest + # command: + # - ... + # ``` + # @type: array + extraContainers: [] + # Toleration Settings for Client pods # This should be a multi-line string matching the Toleration array # in a PodSpec. @@ -1058,6 +1284,12 @@ dns: # @type: boolean enabled: "-" + # If true, services using Consul Connect will use Consul DNS + # for default DNS resolution. The DNS lookups fall back to the nameserver IPs + # listed in /etc/resolv.conf if not found in Consul. + # @type: boolean + enableRedirection: false + # Used to control the type of service created. For # example, setting this to "LoadBalancer" will create an external load # balancer (for supported K8S installations) @@ -1152,6 +1384,9 @@ ui: # @type: boolean enabled: false + # Optionally set the ingressClassName. + ingressClassName: "" + # pathType override - see: https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types pathType: Prefix @@ -1209,6 +1444,11 @@ ui: # @type: string baseURL: http://prometheus-server + # Corresponds to https://www.consul.io/docs/agent/options#ui_config_dashboard_url_templates configuration. + dashboardURLTemplates: + # Sets https://www.consul.io/docs/agent/options#ui_config_dashboard_url_templates_service. + service: "" + # Configure the catalog sync process to sync K8S with Consul # services. This can run bidirectional (default) or unidirectionally (Consul # to K8S or K8S to Consul only). @@ -1556,11 +1796,16 @@ connectInject: # This setting can be safely disabled by setting to "Ignore". failurePolicy: "Fail" - # Selector for restricting the webhook to only - # specific namespaces. This should be set to a multiline string. + # Selector for restricting the webhook to only specific namespaces. + # Use with `connectInject.default: true` to automatically inject all pods in namespaces that match the selector. This should be set to a multiline string. # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector # for more details. # + # By default, we exclude the kube-system namespace since usually users won't + # want those pods injected and also the local-path-storage namespace so that + # Kind (Kubernetes In Docker) can provision Pods used to create PVCs. + # Note that this exclusion is only supported in Kubernetes v1.21.1+. + # # Example: # # ```yaml @@ -1569,7 +1814,11 @@ connectInject: # namespace-label: label-value # ``` # @type: string - namespaceSelector: null + namespaceSelector: | + matchExpressions: + - key: "kubernetes.io/metadata.name" + operator: "NotIn" + values: ["kube-system","local-path-storage"] # List of k8s namespaces to allow Connect sidecar # injection in. If a k8s namespace is not included or is listed in `k8sDenyNamespaces`, @@ -1940,6 +2189,18 @@ meshGateway: memory: "150Mi" cpu: "50m" + # Resource settings for the `service-init` init container. + # @recurse: false + # @type: map + initServiceInitContainer: + resources: + requests: + memory: "50Mi" + cpu: "50m" + limits: + memory: "50Mi" + cpu: "50m" + # By default, we set an anti-affinity so that two gateway pods won't be # on the same node. NOTE: Gateways require that Consul client agents are # also running on the nodes alongside each gateway pod. @@ -2094,6 +2355,9 @@ ingressGateways: # Optional priorityClassName. priorityClassName: "" + # Amount of seconds to wait for graceful termination before killing the pod. + terminationGracePeriodSeconds: 10 + # Annotations to apply to the ingress gateway deployment. Annotations defined # here will be applied to all ingress gateway deployments in addition to any # annotations defined for a specific gateway in `ingressGateways.gateways`. @@ -2245,6 +2509,149 @@ terminatingGateways: gateways: - name: terminating-gateway +# Configuration settings for the Consul API Gateway integration +apiGateway: + # When true the helm chart will install the Consul API Gateway controller + enabled: false + + # Image to use for the api-gateway-controller pods and gateway instances + # @type: string + image: null + + # Override global log verbosity level for api-gateway-controller pods. One of "debug", "info", "warn", or "error". + # @type: string + logLevel: info + + # Configuration settings for the optional GatewayClass installed by consul-k8s (enabled by default) + managedGatewayClass: + # When true a GatewayClass is configured to automatically work with Consul as installed by helm. + enabled: true + + # This value defines `nodeSelector` (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) + # labels for gateway pod assignment, formatted as a multi-line string. + # + # Example: + # + # ```yaml + # nodeSelector: | + # beta.kubernetes.io/arch: amd64 + # ``` + # + # @type: string + nodeSelector: null + + # This value defines the type of service created for gateways (e.g. LoadBalancer, ClusterIP) + serviceType: LoadBalancer + + # This value toggles if the gateway ports should be mapped to host ports + useHostPorts: false + + # Configuration settings for annotations to be copied from the Gateway to other child resources. + copyAnnotations: + # This value defines a list of annotations to be copied from the Gateway to the Service created, formatted as a multi-line string. + # + # Example: + # + # ```yaml + # service: | + # - external-dns.alpha.kubernetes.io/hostname + # ``` + # + # @type: string + service: null + + # [Enterprise Only] These settings manage the API Gateway's interaction with + # Consul namespaces (requires consul-ent v1.7+). + # Also, `global.enableConsulNamespaces` must be true. + consulNamespaces: + # Name of the Consul namespace to register all + # k8s services into. If the Consul namespace does not already exist, + # it will be created. This will be ignored if `mirroringK8S` is true. + consulDestinationNamespace: "default" + + # If true, k8s services will be registered into a Consul namespace + # of the same name as their k8s namespace, optionally prefixed if + # `mirroringK8SPrefix` is set below. If the Consul namespace does not + # already exist, it will be created. Turning this on overrides the + # `consulDestinationNamespace` setting. + # `addK8SNamespaceSuffix` may no longer be needed if enabling this option. + mirroringK8S: false + + # If `mirroringK8S` is set to true, `mirroringK8SPrefix` allows each Consul namespace + # to be given a prefix. For example, if `mirroringK8SPrefix` is set to "k8s-", a + # service in the k8s `staging` namespace will be registered into the + # `k8s-staging` Consul namespace. + mirroringK8SPrefix: "" + + # Configuration for the ServiceAccount created for the api-gateway component + serviceAccount: + # This value defines additional annotations for the client service account. This should be formatted as a multi-line + # string. + # + # ```yaml + # annotations: | + # "sample/annotation1": "foo" + # "sample/annotation2": "bar" + # ``` + # + # @type: string + annotations: null + + # Configuration for the api-gateway controller component + controller: + # This value sets the number of controller replicas to deploy. + replicas: 1 + + # Annotations to apply to the api-gateway-controller pods. + # + # ```yaml + # annotations: | + # "annotation-key": "annotation-value" + # ``` + # + # @type: string + annotations: null + + # This value references an existing + # Kubernetes `priorityClassName` (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#pod-priority) + # that can be assigned to api-gateway-controller pods. + priorityClassName: "" + + # This value defines `nodeSelector` (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) + # labels for api-gateway-controller pod assignment, formatted as a multi-line string. + # + # Example: + # + # ```yaml + # nodeSelector: | + # beta.kubernetes.io/arch: amd64 + # ``` + # + # @type: string + nodeSelector: null + + # Configuration for the Service created for the api-gateway-controller + service: + # Annotations to apply to the api-gateway-controller service. + # + # ```yaml + # annotations: | + # "annotation-key": "annotation-value" + # ``` + # + # @type: string + annotations: null + +# Configuration settings for the webhook-cert-manager +# `webhook-cert-manager` ensures that cert bundles are up to date for the mutating webhook. +webhookCertManager: + + # Toleration Settings + # This should be a multi-line string matching the Toleration array + # in a PodSpec. + # @type: string + tolerations: null + # Configures a demo Prometheus installation. prometheus: # When true, the Helm chart will install a demo Prometheus server instance diff --git a/charts/embed_chart.go b/charts/embed_chart.go new file mode 100644 index 0000000000..6393508ebb --- /dev/null +++ b/charts/embed_chart.go @@ -0,0 +1,16 @@ +package charts + +import "embed" + +// ConsulHelmChart embeds the Consul Helm Chart files into an exported variable from this package. Changes to the chart +// files referenced below will be reflected in the embedded templates in the CLI at CLI compile time. +// +// This is currently only meant to be used by the consul-k8s CLI within this repository. Importing this package from the +// CLI allows us to embed the templates at compilation time. Since this is in a monorepo, we can directly reference this +// charts module as relative to the CLI module (with a replace directive), which allows us to not need to bump the +// charts module dependency manually or as part of a Makefile. +// +// The embed directive does not include files with underscores unless explicitly listed, which is why _helpers.tpl is +// explicitly embedded. +//go:embed consul/Chart.yaml consul/values.yaml consul/templates consul/templates/_helpers.tpl +var ConsulHelmChart embed.FS diff --git a/charts/go.mod b/charts/go.mod new file mode 100644 index 0000000000..950a1e55be --- /dev/null +++ b/charts/go.mod @@ -0,0 +1,3 @@ +module github.com/hashicorp/consul-k8s/charts + +go 1.17 diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000000..cad0f51b7e --- /dev/null +++ b/cli/README.md @@ -0,0 +1,127 @@ +# Consul Kubernetes CLI +This repository contains a CLI tool for installing and operating [Consul](https://www.consul.io/) on Kubernetes. +**Warning** this tool is currently experimental. Do not use it on Consul clusters you care about. + +## Installation & Setup +Currently the tool is not available on any releases page. Instead clone the repository and run `go build -o bin/consul-k8s` +from this directory and proceed to run the binary. + +## Commands +* [consul-k8s install](#consul-k8s-install) +* [consul-k8s uninstall](#consul-k8s-uninstall) + +### consul-k8s install +This command installs Consul on a Kubernetes cluster. It allows `demo` and `secure` installations via preset configurations +using the `-preset` flag. The `demo` installation installs just a single replica server with sidecar injection enabled and +is useful to test out service mesh functionality. The `secure` installation is minimal like `demo` but also enables ACLs and TLS. + +Get started with: +```bash +consul-k8s install -preset=demo +``` + +Note that when configuring an installation, the precedence order is as follows from lowest to highest precedence: +1. `-preset` +2. `-f` +3. `-set` +4. `-set-string` +5. `-set-file` + +For example, `-set-file` will override a value provided via `-set`. Additionally, within each of these groups the +rightmost flag value has the highest precedence, i.e `-set foo=bar -set foo=baz` will result in `foo: baz` being set. + +``` +Usage: consul-k8s install [flags] +Install Consul onto a Kubernetes cluster. + +Command Options: + + -auto-approve + Skip confirmation prompt. The default is false. + + -config-file= + Path to a file to customize the installation, such as Consul Helm chart + values file. Can be specified multiple times. This is aliased as "-f". + + -dry-run + Run pre-install checks and display summary of installation. The default + is false. + + -namespace= + Namespace for the Consul installation. The default is consul. + + -preset= + Use an installation preset, one of demo, secure. Defaults to none + + -set= + Set a value to customize. Can be specified multiple times. Supports + Consul Helm chart values. + + -set-file= + Set a value to customize via a file. The contents of the file will be + set as the value. Can be specified multiple times. Supports Consul Helm + chart values. + + -set-string= + Set a string value to customize. Can be specified multiple times. + Supports Consul Helm chart values. + + -timeout= + Timeout to wait for installation to be ready. The default is 10m. + + -wait + Determines whether to wait for resources in installation to be ready + before exiting command. The default is true. + +Global Options: + + -context= + Kubernetes context to use. + + -kubeconfig= + Path to kubeconfig file. This is aliased as "-c". + +``` + +### consul-k8s uninstall +This command uninstalls Consul on Kubernetes, while prompting whether to uninstall the release and whether to delete all +related resources such as PVCs, Secrets, and ServiceAccounts. + +Get started with: +```bash +consul-k8s uninstall +``` + +``` +Usage: consul-k8s uninstall [flags] +Uninstall Consul with options to delete data and resources associated with Consul installation. + +Command Options: + + -auto-approve + Skip approval prompt for uninstalling Consul. The default is false. + + -name= + Name of the installation. This can be used to uninstall and/or delete + the resources of a specific Helm release. + + -namespace= + Namespace for the Consul installation. + + -timeout= + Timeout to wait for uninstall. The default is 10m. + + -wipe-data + When used in combination with -auto-approve, all persisted data (PVCs + and Secrets) from previous installations will be deleted. Only set this + to true when data from previous installations is no longer necessary. + The default is false. + +Global Options: + + -context= + Kubernetes context to use. + + -kubeconfig= + Path to kubeconfig file. This is aliased as "-c". +``` diff --git a/cli/cmd/install/install.go b/cli/cmd/install/install.go new file mode 100644 index 0000000000..f33b044ef8 --- /dev/null +++ b/cli/cmd/install/install.go @@ -0,0 +1,514 @@ +package install + +import ( + "errors" + "fmt" + "os" + "strings" + "sync" + "time" + + consulChart "github.com/hashicorp/consul-k8s/charts" + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/config" + "github.com/hashicorp/consul-k8s/cli/helm" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + helmCLI "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/getter" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "sigs.k8s.io/yaml" +) + +const ( + flagNamePreset = "preset" + defaultPreset = "" + + flagNameConfigFile = "config-file" + flagNameSetStringValues = "set-string" + flagNameSetValues = "set" + flagNameFileValues = "set-file" + + flagNameDryRun = "dry-run" + defaultDryRun = false + + flagNameAutoApprove = "auto-approve" + defaultAutoApprove = false + + flagNameNamespace = "namespace" + + flagNameTimeout = "timeout" + defaultTimeout = "10m" + + flagNameVerbose = "verbose" + defaultVerbose = false + + flagNameWait = "wait" + defaultWait = true +) + +type Command struct { + *common.BaseCommand + + kubernetes kubernetes.Interface + + set *flag.Sets + + flagPreset string + flagNamespace string + flagDryRun bool + flagAutoApprove bool + flagValueFiles []string + flagSetStringValues []string + flagSetValues []string + flagFileValues []string + flagTimeout string + timeoutDuration time.Duration + flagVerbose bool + flagWait bool + + flagKubeConfig string + flagKubeContext string + + once sync.Once + help string +} + +func (c *Command) init() { + // Store all the possible preset values in 'presetList'. Printed in the help message. + var presetList []string + for name := range config.Presets { + presetList = append(presetList, name) + } + + c.set = flag.NewSets() + f := c.set.NewSet("Command Options") + f.BoolVar(&flag.BoolVar{ + Name: flagNameAutoApprove, + Target: &c.flagAutoApprove, + Default: defaultAutoApprove, + Usage: "Skip confirmation prompt.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameDryRun, + Target: &c.flagDryRun, + Default: defaultDryRun, + Usage: "Perform pre-install checks and display a summary of the installation.", + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameConfigFile, + Aliases: []string{"f"}, + Target: &c.flagValueFiles, + Usage: "Set the path to a file to customize the installation, such as Consul Helm chart values file. Can be specified multiple times.", + }) + f.StringVar(&flag.StringVar{ + Name: flagNameNamespace, + Target: &c.flagNamespace, + Default: common.DefaultReleaseNamespace, + Usage: "Set the namespace for the Consul installation.", + }) + f.StringVar(&flag.StringVar{ + Name: flagNamePreset, + Target: &c.flagPreset, + Default: defaultPreset, + Usage: fmt.Sprintf("Use an installation preset, one of %s. Defaults to none", strings.Join(presetList, ", ")), + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameSetValues, + Target: &c.flagSetValues, + Usage: "Set a value to customize. Can be specified multiple times. Supports Consul Helm chart values.", + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameFileValues, + Target: &c.flagFileValues, + Usage: "Set a value to customize using a file. The contents of the file will be set as the value." + + "Can be specified multiple times. Supports Consul Helm chart values.", + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameSetStringValues, + Target: &c.flagSetStringValues, + Usage: "Set a string value to customize. Can be specified multiple times. Supports Consul Helm chart values.", + }) + f.StringVar(&flag.StringVar{ + Name: flagNameTimeout, + Target: &c.flagTimeout, + Default: defaultTimeout, + Usage: "Set a timeout to wait for installation to be ready.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameVerbose, + Aliases: []string{"v"}, + Target: &c.flagVerbose, + Default: defaultVerbose, + Usage: "Output verbose logs from the command with the status of resources being installed.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameWait, + Target: &c.flagWait, + Default: defaultWait, + Usage: "Wait for Kubernetes resources in installation to be ready before exiting command.", + }) + + f = c.set.NewSet("Global Options") + f.StringVar(&flag.StringVar{ + Name: "kubeconfig", + Aliases: []string{"c"}, + Target: &c.flagKubeConfig, + Default: "", + Usage: "Set the path to kubeconfig file.", + }) + f.StringVar(&flag.StringVar{ + Name: "context", + Target: &c.flagKubeContext, + Default: "", + Usage: "Set the Kubernetes context to use.", + }) + + c.help = c.set.Help() + + // c.Init() calls the embedded BaseCommand's initialization function. + c.Init() +} + +type helmValues struct { + Global globalValues `yaml:"global"` +} + +type globalValues struct { + EnterpriseLicense enterpriseLicense `yaml:"enterpriseLicense"` +} + +type enterpriseLicense struct { + SecretName string `yaml:"secretName"` + SecretKey string `yaml:"secretKey"` +} + +// Run installs Consul into a Kubernetes cluster. +func (c *Command) Run(args []string) int { + c.once.Do(c.init) + + // The logger is initialized in main with the name cli. Here, we reset the name to install so log lines would be prefixed with install. + c.Log.ResetNamed("install") + + defer common.CloseWithError(c.BaseCommand) + + if err := c.validateFlags(args); err != nil { + c.UI.Output(err.Error()) + return 1 + } + + if c.flagDryRun { + c.UI.Output("Performing dry run install. No changes will be made to the cluster.", terminal.WithHeaderStyle()) + } + + // helmCLI.New() will create a settings object which is used by the Helm Go SDK calls. + settings := helmCLI.New() + + // Any overrides by our kubeconfig and kubecontext flags is done here. The Kube client that + // is created will use this command's flags first, then the HELM_KUBECONTEXT environment variable, + // then call out to genericclioptions.ConfigFlag + if c.flagKubeConfig != "" { + settings.KubeConfig = c.flagKubeConfig + } + if c.flagKubeContext != "" { + settings.KubeContext = c.flagKubeContext + } + + // Setup logger to stream Helm library logs + var uiLogger = func(s string, args ...interface{}) { + logMsg := fmt.Sprintf(s, args...) + + if c.flagVerbose { + // Only output all logs when verbose is enabled + c.UI.Output(logMsg, terminal.WithLibraryStyle()) + } else { + // When verbose is not enabled, output all logs except not ready messages for resources + if !strings.Contains(logMsg, "not ready") { + c.UI.Output(logMsg, terminal.WithLibraryStyle()) + } + } + } + + // Set up the kubernetes client to use for non Helm SDK calls to the Kubernetes API + // The Helm SDK will use settings.RESTClientGetter for its calls as well, so this will + // use a consistent method to target the right cluster for both Helm SDK and non Helm SDK calls. + if c.kubernetes == nil { + restConfig, err := settings.RESTClientGetter().ToRESTConfig() + if err != nil { + c.UI.Output("Error retrieving Kubernetes authentication:\n%v", err, terminal.WithErrorStyle()) + return 1 + } + c.kubernetes, err = kubernetes.NewForConfig(restConfig) + if err != nil { + c.UI.Output("Error initializing Kubernetes client:\n%v", err, terminal.WithErrorStyle()) + return 1 + } + } + + c.UI.Output("Checking if Consul can be installed", terminal.WithHeaderStyle()) + + // Ensure there is not an existing Consul installation which would cause a conflict. + if name, ns, err := common.CheckForInstallations(settings, uiLogger); err == nil { + c.UI.Output("Cannot install Consul. A Consul cluster is already installed in namespace %s with name %s.", ns, name, terminal.WithErrorStyle()) + c.UI.Output("Use the command `consul-k8s uninstall` to uninstall Consul from the cluster.", terminal.WithInfoStyle()) + return 1 + } + c.UI.Output("No existing Consul installations found.", terminal.WithSuccessStyle()) + + // Ensure there's no previous PVCs lying around. + if err := c.checkForPreviousPVCs(); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + c.UI.Output("No existing Consul persistent volume claims found", terminal.WithSuccessStyle()) + + // Ensure there's no previous bootstrap secret lying around. + if err := c.checkForPreviousSecrets(); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + c.UI.Output("No existing Consul secrets found", terminal.WithSuccessStyle()) + + // Handle preset, value files, and set values logic. + vals, err := c.mergeValuesFlagsWithPrecedence(settings) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + valuesYaml, err := yaml.Marshal(vals) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + var v helmValues + err = yaml.Unmarshal(valuesYaml, &v) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // If an enterprise license secret was provided, check that the secret exists and that the enterprise Consul image is set. + if v.Global.EnterpriseLicense.SecretName != "" { + if err := c.checkValidEnterprise(v.Global.EnterpriseLicense.SecretName); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + c.UI.Output("Valid enterprise Consul secret found.", terminal.WithSuccessStyle()) + } + + // Print out the installation summary. + if !c.flagAutoApprove { + c.UI.Output("Consul Installation Summary", terminal.WithHeaderStyle()) + c.UI.Output("Name: %s", common.DefaultReleaseName, terminal.WithInfoStyle()) + c.UI.Output("Namespace: %s", c.flagNamespace, terminal.WithInfoStyle()) + + if len(vals) == 0 { + c.UI.Output("\nNo overrides provided, using the default Helm values.", terminal.WithInfoStyle()) + } else { + c.UI.Output("\nHelm value overrides\n-------------------\n"+string(valuesYaml), terminal.WithInfoStyle()) + } + } + + // Without informing the user, default global.name to consul if it hasn't been set already. We don't allow setting + // the release name, and since that is hardcoded to "consul", setting global.name to "consul" makes it so resources + // aren't double prefixed with "consul-consul-...". + vals = common.MergeMaps(config.Convert(config.GlobalNameConsul), vals) + + if c.flagDryRun { + c.UI.Output("Dry run complete. No changes were made to the Kubernetes cluster.\n"+ + "Installation can proceed with this configuration.", terminal.WithInfoStyle()) + return 0 + } + + if !c.flagAutoApprove { + confirmation, err := c.UI.Input(&terminal.Input{ + Prompt: "Proceed with installation? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + if common.Abort(confirmation) { + c.UI.Output("Install aborted. Use the command `consul-k8s install -help` to learn how to customize your installation.", + terminal.WithInfoStyle()) + return 1 + } + } + + c.UI.Output("Installing Consul", terminal.WithHeaderStyle()) + + // Setup action configuration for Helm Go SDK function calls. + actionConfig := new(action.Configuration) + actionConfig, err = helm.InitActionConfig(actionConfig, c.flagNamespace, settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // Setup the installation action. + install := action.NewInstall(actionConfig) + install.ReleaseName = common.DefaultReleaseName + install.Namespace = c.flagNamespace + install.CreateNamespace = true + install.Wait = c.flagWait + install.Timeout = c.timeoutDuration + + // Read the embedded chart files into []*loader.BufferedFile. + chartFiles, err := helm.ReadChartFiles(consulChart.ConsulHelmChart, common.TopLevelChartDirName) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // Create a *chart.Chart object from the files to run the installation from. + chart, err := loader.LoadFiles(chartFiles) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + c.UI.Output("Downloaded charts", terminal.WithSuccessStyle()) + + // Run the install. + if _, err = install.Run(chart, vals); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + c.UI.Output("Consul installed in namespace %q.", c.flagNamespace, terminal.WithSuccessStyle()) + return 0 +} + +// Help returns a description of the command and how it is used. +func (c *Command) Help() string { + c.once.Do(c.init) + return c.Synopsis() + "\n\nUsage: consul-k8s install [flags]\n\n" + c.help +} + +// Synopsis returns a one-line command summary. +func (c *Command) Synopsis() string { + return "Install Consul on Kubernetes." +} + +// checkForPreviousPVCs checks for existing Kubernetes persistent volume claims with a name containing "consul-server" +// and returns an error with a list of PVCs it finds if any match. +func (c *Command) checkForPreviousPVCs() error { + pvcs, err := c.kubernetes.CoreV1().PersistentVolumeClaims("").List(c.Ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("error listing persistent volume claims: %s", err) + } + var previousPVCs []string + for _, pvc := range pvcs.Items { + if strings.Contains(pvc.Name, "consul-server") { + previousPVCs = append(previousPVCs, fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name)) + } + } + + if len(previousPVCs) > 0 { + return fmt.Errorf("found persistent volume claims from previous installations, delete before reinstalling: %s", + strings.Join(previousPVCs, ",")) + } + return nil +} + +// checkForPreviousSecrets checks for the bootstrap token and returns an error if found. +func (c *Command) checkForPreviousSecrets() error { + secrets, err := c.kubernetes.CoreV1().Secrets("").List(c.Ctx, metav1.ListOptions{LabelSelector: common.CLILabelKey + "=" + common.CLILabelValue}) + if err != nil { + return fmt.Errorf("error listing secrets: %s", err) + } + for _, secret := range secrets.Items { + // future TODO: also check for federation secret + if secret.ObjectMeta.Labels[common.CLILabelKey] == common.CLILabelValue { + return fmt.Errorf("found Consul secret from previous installation: %q in namespace %q. Use the command `kubectl delete secret %s --namespace %s` to delete", + secret.Name, secret.Namespace, secret.Name, secret.Namespace) + } + } + + return nil +} + +// mergeValuesFlagsWithPrecedence is responsible for merging all the values to determine the values file for the +// installation based on the following precedence order from lowest to highest: +// 1. -preset +// 2. -f values-file +// 3. -set +// 4. -set-string +// 5. -set-file +// For example, -set-file will override a value provided via -set. +// Within each of these groups the rightmost flag value has the highest precedence. +func (c *Command) mergeValuesFlagsWithPrecedence(settings *helmCLI.EnvSettings) (map[string]interface{}, error) { + p := getter.All(settings) + v := &values.Options{ + ValueFiles: c.flagValueFiles, + StringValues: c.flagSetStringValues, + Values: c.flagSetValues, + FileValues: c.flagFileValues, + } + vals, err := v.MergeValues(p) + if err != nil { + return nil, fmt.Errorf("error merging values: %s", err) + } + if c.flagPreset != defaultPreset { + // Note the ordering of the function call, presets have lower precedence than set vals. + presetMap := config.Presets[c.flagPreset].(map[string]interface{}) + vals = common.MergeMaps(presetMap, vals) + } + return vals, err +} + +// validateFlags checks the command line flags and values for errors. +func (c *Command) validateFlags(args []string) error { + if err := c.set.Parse(args); err != nil { + return err + } + if len(c.set.Args()) > 0 { + return errors.New("should have no non-flag arguments") + } + if len(c.flagValueFiles) != 0 && c.flagPreset != defaultPreset { + return fmt.Errorf("cannot set both -%s and -%s", flagNameConfigFile, flagNamePreset) + } + if _, ok := config.Presets[c.flagPreset]; c.flagPreset != defaultPreset && !ok { + return fmt.Errorf("'%s' is not a valid preset", c.flagPreset) + } + if !common.IsValidLabel(c.flagNamespace) { + return fmt.Errorf("'%s' is an invalid namespace. Namespaces follow the RFC 1123 label convention and must "+ + "consist of a lower case alphanumeric character or '-' and must start/end with an alphanumeric character", c.flagNamespace) + } + duration, err := time.ParseDuration(c.flagTimeout) + if err != nil { + return fmt.Errorf("unable to parse -%s: %s", flagNameTimeout, err) + } + c.timeoutDuration = duration + if len(c.flagValueFiles) != 0 { + for _, filename := range c.flagValueFiles { + if _, err := os.Stat(filename); err != nil && os.IsNotExist(err) { + return fmt.Errorf("file '%s' does not exist", filename) + } + } + } + + return nil +} + +// checkValidEnterprise checks and validates an enterprise installation. +// When an enterprise license secret is provided, check that the secret exists in the "consul" namespace. +func (c *Command) checkValidEnterprise(secretName string) error { + + _, err := c.kubernetes.CoreV1().Secrets(c.flagNamespace).Get(c.Ctx, secretName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return fmt.Errorf("enterprise license secret %q is not found in the %q namespace; please make sure that the secret exists in the %q namespace", secretName, c.flagNamespace, c.flagNamespace) + } else if err != nil { + return fmt.Errorf("error getting the enterprise license secret %q in the %q namespace: %s", secretName, c.flagNamespace, err) + } + return nil +} diff --git a/cli/cmd/install/install_test.go b/cli/cmd/install/install_test.go new file mode 100644 index 0000000000..b38af685fc --- /dev/null +++ b/cli/cmd/install/install_test.go @@ -0,0 +1,173 @@ +package install + +import ( + "context" + "os" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestCheckForPreviousPVCs(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + pvc := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-server-test1", + }, + } + pvc2 := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-server-test2", + }, + } + c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc, metav1.CreateOptions{}) + c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc2, metav1.CreateOptions{}) + err := c.checkForPreviousPVCs() + require.Error(t, err) + require.Equal(t, err.Error(), "found persistent volume claims from previous installations, delete before reinstalling: default/consul-server-test1,default/consul-server-test2") + + // Clear out the client and make sure the check now passes. + c.kubernetes = fake.NewSimpleClientset() + err = c.checkForPreviousPVCs() + require.NoError(t, err) + + // Add a new irrelevant PVC and make sure the check continues to pass. + pvc = &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "irrelevant-pvc", + }, + } + c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc, metav1.CreateOptions{}) + err = c.checkForPreviousPVCs() + require.NoError(t, err) +} + +func TestCheckForPreviousSecrets(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-consul-bootstrap-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + } + c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret, metav1.CreateOptions{}) + err := c.checkForPreviousSecrets() + require.Error(t, err) + require.Contains(t, err.Error(), "found Consul secret from previous installation") + + // Clear out the client and make sure the check now passes. + c.kubernetes = fake.NewSimpleClientset() + err = c.checkForPreviousSecrets() + require.NoError(t, err) + + // Add a new irrelevant secret and make sure the check continues to pass. + secret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "irrelevant-secret", + }, + } + c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret, metav1.CreateOptions{}) + err = c.checkForPreviousSecrets() + require.NoError(t, err) +} + +// TestValidateFlags tests the validate flags function. +func TestValidateFlags(t *testing.T) { + // The following cases should all error, if they fail to this test fails. + testCases := []struct { + description string + input []string + }{ + { + "Should disallow non-flag arguments.", + []string{"foo", "-auto-approve"}, + }, + { + "Should disallow specifying both values file AND presets.", + []string{"-f='f.txt'", "-preset=demo"}, + }, + { + "Should error on invalid presets.", + []string{"-preset=foo"}, + }, + { + "Should error on invalid timeout.", + []string{"-timeout=invalid-timeout"}, + }, + { + "Should error on an invalid namespace. If this failed, TestValidLabel() probably did too.", + []string{"-namespace=\" nsWithSpace\""}, + }, + { + "Should have errored on a non-existant file.", + []string{"-f=\"does_not_exist.txt\""}, + }, + } + + for _, testCase := range testCases { + c := getInitializedCommand(t) + t.Run(testCase.description, func(t *testing.T) { + if err := c.validateFlags(testCase.input); err == nil { + t.Errorf("Test case should have failed.") + } + }) + } +} + +// getInitializedCommand sets up a command struct for tests. +func getInitializedCommand(t *testing.T) *Command { + t.Helper() + log := hclog.New(&hclog.LoggerOptions{ + Name: "cli", + Level: hclog.Info, + Output: os.Stdout, + }) + + baseCommand := &common.BaseCommand{ + Log: log, + } + + c := &Command{ + BaseCommand: baseCommand, + } + c.init() + return c +} + +func TestCheckValidEnterprise(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-secret", + }, + } + secret2 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-secret2", + }, + } + + // Enterprise secret is valid. + c.kubernetes.CoreV1().Secrets("consul").Create(context.Background(), secret, metav1.CreateOptions{}) + err := c.checkValidEnterprise(secret.Name) + require.NoError(t, err) + + // Enterprise secret does not exist. + err = c.checkValidEnterprise("consul-unrelated-secret") + require.Error(t, err) + require.Contains(t, err.Error(), "please make sure that the secret exists") + + // Enterprise secret exists in a different namespace. + c.kubernetes.CoreV1().Secrets("unrelated").Create(context.Background(), secret2, metav1.CreateOptions{}) + err = c.checkValidEnterprise(secret2.Name) + require.Error(t, err) + require.Contains(t, err.Error(), "please make sure that the secret exists") +} diff --git a/cli/cmd/status/status.go b/cli/cmd/status/status.go new file mode 100644 index 0000000000..5c148e9921 --- /dev/null +++ b/cli/cmd/status/status.go @@ -0,0 +1,290 @@ +package status + +import ( + "errors" + "fmt" + "strconv" + "sync" + + "helm.sh/helm/v3/pkg/release" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/helm" + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/yaml" +) + +type Command struct { + *common.BaseCommand + + kubernetes kubernetes.Interface + + set *flag.Sets + + flagKubeConfig string + flagKubeContext string + + once sync.Once + help string +} + +func (c *Command) init() { + c.set = flag.NewSets() + + f := c.set.NewSet("Global Options") + f.StringVar(&flag.StringVar{ + Name: "kubeconfig", + Aliases: []string{"c"}, + Target: &c.flagKubeConfig, + Default: "", + Usage: "Path to kubeconfig file.", + }) + f.StringVar(&flag.StringVar{ + Name: "context", + Target: &c.flagKubeContext, + Default: "", + Usage: "Kubernetes context to use.", + }) + + c.help = c.set.Help() + + // c.Init() calls the embedded BaseCommand's initialization function. + c.Init() +} + +// Run checks the status of a Consul installation on Kubernetes. +func (c *Command) Run(args []string) int { + c.once.Do(c.init) + + // The logger is initialized in main with the name cli. Here, we reset the name to status so log lines would be prefixed with status. + c.Log.ResetNamed("status") + + defer common.CloseWithError(c.BaseCommand) + + if err := c.set.Parse(args); err != nil { + c.UI.Output(err.Error()) + return 1 + } + + if err := c.validateFlags(args); err != nil { + c.UI.Output(err.Error()) + return 1 + } + + // helmCLI.New() will create a settings object which is used by the Helm Go SDK calls. + settings := helmCLI.New() + if c.flagKubeConfig != "" { + settings.KubeConfig = c.flagKubeConfig + } + if c.flagKubeContext != "" { + settings.KubeContext = c.flagKubeContext + } + + if err := c.setupKubeClient(settings); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // Setup logger to stream Helm library logs. + var uiLogger = func(s string, args ...interface{}) { + logMsg := fmt.Sprintf(s, args...) + c.UI.Output(logMsg, terminal.WithLibraryStyle()) + } + + c.UI.Output("Consul Status Summary", terminal.WithHeaderStyle()) + + releaseName, namespace, err := common.CheckForInstallations(settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.checkHelmInstallation(settings, uiLogger, releaseName, namespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if s, err := c.checkConsulServers(namespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } else { + c.UI.Output(s, terminal.WithSuccessStyle()) + } + + if s, err := c.checkConsulClients(namespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } else { + c.UI.Output(s, terminal.WithSuccessStyle()) + } + + return 0 +} + +// validateFlags checks the command line flags and values for errors. +func (c *Command) validateFlags(args []string) error { + if len(c.set.Args()) > 0 { + return errors.New("should have no non-flag arguments") + } + return nil +} + +// checkHelmInstallation uses the helm Go SDK to depict the status of a named release. This function then prints +// the version of the release, it's status (unknown, deployed, uninstalled, ...), and the overwritten values. +func (c *Command) checkHelmInstallation(settings *helmCLI.EnvSettings, uiLogger action.DebugLog, releaseName, namespace string) error { + // Need a specific action config to call helm status, where namespace comes from the previous call to list. + statusConfig := new(action.Configuration) + statusConfig, err := helm.InitActionConfig(statusConfig, namespace, settings, uiLogger) + if err != nil { + return err + } + + statuser := action.NewStatus(statusConfig) + rel, err := statuser.Run(releaseName) + if err != nil { + return fmt.Errorf("couldn't check for installations: %s", err) + } + + timezone, _ := rel.Info.LastDeployed.Zone() + + tbl := terminal.NewTable([]string{"Name", "Namespace", "Status", "Chart Version", "AppVersion", "Revision", "Last Updated"}...) + trow := []terminal.TableEntry{ + { + Value: releaseName, + }, + { + Value: namespace, + }, + { + Value: string(rel.Info.Status), + }, + { + Value: rel.Chart.Metadata.Version, + }, + { + Value: rel.Chart.Metadata.AppVersion, + }, + { + Value: strconv.Itoa(rel.Version), + }, + { + Value: rel.Info.LastDeployed.Format("2006/01/02 15:04:05") + " " + timezone, + }, + } + tbl.Rows = [][]terminal.TableEntry{} + tbl.Rows = append(tbl.Rows, trow) + + c.UI.Table(tbl) + + valuesYaml, err := yaml.Marshal(rel.Config) + c.UI.Output("Config:", terminal.WithHeaderStyle()) + if err != nil { + c.UI.Output("%+v", err, terminal.WithInfoStyle()) + } else if len(rel.Config) == 0 { + c.UI.Output(string(valuesYaml), terminal.WithInfoStyle()) + } else { + c.UI.Output(string(valuesYaml), terminal.WithInfoStyle()) + } + + // Check the status of the hooks. + if len(rel.Hooks) > 1 { + c.UI.Output("Status Of Helm Hooks:", terminal.WithHeaderStyle()) + + for _, hook := range rel.Hooks { + // Remember that we only report the status of pre-install or pre-upgrade hooks. + if validEvent(hook.Events) { + c.UI.Output("%s %s: %s", hook.Name, hook.Kind, hook.LastRun.Phase.String()) + } + } + fmt.Println("") + } + + return nil +} + +// validEvent is a helper function that checks if the given hook's events are pre-install or pre-upgrade. +// Only pre-install and pre-upgrade hooks are expected to have run when using the status command against +// a running installation. +func validEvent(events []release.HookEvent) bool { + for _, event := range events { + if event.String() == "pre-install" || event.String() == "pre-upgrade" { + return true + } + } + return false +} + +// checkConsulServers uses the Kubernetes list function to report if the consul servers are healthy. +func (c *Command) checkConsulServers(namespace string) (string, error) { + servers, err := c.kubernetes.AppsV1().StatefulSets(namespace).List(c.Ctx, + metav1.ListOptions{LabelSelector: "app=consul,chart=consul-helm,component=server"}) + if err != nil { + return "", err + } else if len(servers.Items) == 0 { + return "", errors.New("no server stateful set found") + } else if len(servers.Items) > 1 { + return "", errors.New("found multiple server stateful sets") + } + + desiredReplicas := int(*servers.Items[0].Spec.Replicas) + readyReplicas := int(servers.Items[0].Status.ReadyReplicas) + if readyReplicas < desiredReplicas { + return "", fmt.Errorf("%d/%d Consul servers unhealthy", desiredReplicas-readyReplicas, desiredReplicas) + } + return fmt.Sprintf("Consul servers healthy (%d/%d)", readyReplicas, desiredReplicas), nil +} + +// checkConsulClients uses the Kubernetes list function to report if the consul clients are healthy. +func (c *Command) checkConsulClients(namespace string) (string, error) { + clients, err := c.kubernetes.AppsV1().DaemonSets(namespace).List(c.Ctx, + metav1.ListOptions{LabelSelector: "app=consul,chart=consul-helm"}) + if err != nil { + return "", err + } else if len(clients.Items) == 0 { + return "", errors.New("no client daemon set found") + } else if len(clients.Items) > 1 { + return "", errors.New("found multiple client daemon sets") + } + desiredReplicas := int(clients.Items[0].Status.DesiredNumberScheduled) + readyReplicas := int(clients.Items[0].Status.NumberReady) + if readyReplicas < desiredReplicas { + return "", fmt.Errorf("%d/%d Consul clients unhealthy", desiredReplicas-readyReplicas, desiredReplicas) + } + return fmt.Sprintf("Consul clients healthy (%d/%d)", readyReplicas, desiredReplicas), nil +} + +// setupKubeClient to use for non Helm SDK calls to the Kubernetes API The Helm SDK will use +// settings.RESTClientGetter for its calls as well, so this will use a consistent method to +// target the right cluster for both Helm SDK and non Helm SDK calls. +func (c *Command) setupKubeClient(settings *helmCLI.EnvSettings) error { + if c.kubernetes == nil { + restConfig, err := settings.RESTClientGetter().ToRESTConfig() + if err != nil { + c.UI.Output("Error retrieving Kubernetes authentication: %v", err, terminal.WithErrorStyle()) + return err + } + c.kubernetes, err = kubernetes.NewForConfig(restConfig) + if err != nil { + c.UI.Output("Error initializing Kubernetes client: %v", err, terminal.WithErrorStyle()) + return err + } + } + + return nil +} + +// Help returns a description of the command and how it is used. +func (c *Command) Help() string { + c.once.Do(c.init) + return c.Synopsis() + "\n\nUsage: consul-k8s status [flags]\n\n" + c.help +} + +// Synopsis returns a one-line command summary. +func (c *Command) Synopsis() string { + return "Check the status of a Consul installation on Kubernetes." +} diff --git a/cli/cmd/status/status_test.go b/cli/cmd/status/status_test.go new file mode 100644 index 0000000000..79f908654f --- /dev/null +++ b/cli/cmd/status/status_test.go @@ -0,0 +1,186 @@ +package status + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +// TestCheckConsulServers creates a fake stateful set and tests the checkConsulServers function. +func TestCheckConsulServers(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + + // First check that no stateful sets causes an error. + _, err := c.checkConsulServers("default") + require.Error(t, err) + require.Contains(t, err.Error(), "no server stateful set found") + + // Next create a stateful set with 3 desired replicas and 3 ready replicas. + var replicas int32 = 3 + + ss := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-server-test1", + Namespace: "default", + Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "server"}, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: replicas, + ReadyReplicas: replicas, + }, + } + + c.kubernetes.AppsV1().StatefulSets("default").Create(context.Background(), ss, metav1.CreateOptions{}) + + // Now we run the checkConsulServers() function and it should succeed. + s, err := c.checkConsulServers("default") + require.NoError(t, err) + require.Equal(t, "Consul servers healthy (3/3)", s) + + // If you then create another stateful set it should error. + ss2 := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-server-test2", + Namespace: "default", + Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "server"}, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: replicas, + ReadyReplicas: replicas, + }, + } + c.kubernetes.AppsV1().StatefulSets("default").Create(context.Background(), ss2, metav1.CreateOptions{}) + + _, err = c.checkConsulServers("default") + require.Error(t, err) + require.Contains(t, err.Error(), "found multiple server stateful sets") + + // Clear out the client and now run a test where the stateful set isn't ready. + c.kubernetes = fake.NewSimpleClientset() + + ss3 := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-server-test3", + Namespace: "default", + Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "server"}, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: replicas, + ReadyReplicas: replicas - 1, // Let's just set one of the servers to unhealthy + }, + } + c.kubernetes.AppsV1().StatefulSets("default").Create(context.Background(), ss3, metav1.CreateOptions{}) + + _, err = c.checkConsulServers("default") + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("%d/%d Consul servers unhealthy", 1, replicas)) +} + +// TestCheckConsulClients is very similar to TestCheckConsulServers() in structure. +func TestCheckConsulClients(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + + // No client daemon set should cause an error. + _, err := c.checkConsulClients("default") + require.Error(t, err) + require.Contains(t, err.Error(), "no client daemon set found") + + // Next create a daemon set. + var desired int32 = 3 + + ds := &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-client-test1", + Namespace: "default", + Labels: map[string]string{"app": "consul", "chart": "consul-helm"}, + }, + Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: desired, + NumberReady: desired, + }, + } + + c.kubernetes.AppsV1().DaemonSets("default").Create(context.Background(), ds, metav1.CreateOptions{}) + + // Now run checkConsulClients() and make sure it succeeds. + s, err := c.checkConsulClients("default") + require.NoError(t, err) + require.Equal(t, "Consul clients healthy (3/3)", s) + + // Creating another daemon set should cause an error. + ds2 := &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-client-test2", + Namespace: "default", + Labels: map[string]string{"app": "consul", "chart": "consul-helm"}, + }, + Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: desired, + NumberReady: desired, + }, + } + c.kubernetes.AppsV1().DaemonSets("default").Create(context.Background(), ds2, metav1.CreateOptions{}) + + _, err = c.checkConsulClients("default") + require.Error(t, err) + require.Contains(t, err.Error(), "found multiple client daemon sets") + + // Clear out the client and run a test with fewer than desired daemon sets ready. + c.kubernetes = fake.NewSimpleClientset() + + ds3 := &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-client-test2", + Namespace: "default", + Labels: map[string]string{"app": "consul", "chart": "consul-helm"}, + }, + Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: desired, + NumberReady: desired - 1, + }, + } + c.kubernetes.AppsV1().DaemonSets("default").Create(context.Background(), ds3, metav1.CreateOptions{}) + + _, err = c.checkConsulClients("default") + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("%d/%d Consul clients unhealthy", 1, desired)) +} + +// getInitializedCommand sets up a command struct for tests. +func getInitializedCommand(t *testing.T) *Command { + t.Helper() + log := hclog.New(&hclog.LoggerOptions{ + Name: "cli", + Level: hclog.Info, + Output: os.Stdout, + }) + + baseCommand := &common.BaseCommand{ + Log: log, + } + + c := &Command{ + BaseCommand: baseCommand, + } + c.init() + return c +} diff --git a/cli/cmd/uninstall/uninstall.go b/cli/cmd/uninstall/uninstall.go new file mode 100644 index 0000000000..288f97823b --- /dev/null +++ b/cli/cmd/uninstall/uninstall.go @@ -0,0 +1,578 @@ +package uninstall + +import ( + "fmt" + "os" + "sync" + "time" + + "github.com/cenkalti/backoff" + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/helm" + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + flagAutoApprove = "auto-approve" + defaultAutoApprove = false + + flagNamespace = "namespace" + defaultAllNamespaces = "" + + flagReleaseName = "name" + defaultAnyReleaseName = "" + + flagWipeData = "wipe-data" + defaultWipeData = false + + flagTimeout = "timeout" + defaultTimeout = "10m" +) + +type Command struct { + *common.BaseCommand + + kubernetes kubernetes.Interface + + set *flag.Sets + + flagNamespace string + flagReleaseName string + flagAutoApprove bool + flagWipeData bool + flagTimeout string + timeoutDuration time.Duration + + flagKubeConfig string + flagKubeContext string + + once sync.Once + help string +} + +func (c *Command) init() { + c.set = flag.NewSets() + f := c.set.NewSet("Command Options") + f.BoolVar(&flag.BoolVar{ + Name: flagAutoApprove, + Target: &c.flagAutoApprove, + Default: defaultAutoApprove, + Usage: "Skip approval prompt for uninstalling Consul.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagWipeData, + Target: &c.flagWipeData, + Default: defaultWipeData, + Usage: "When used in combination with -auto-approve, all persisted data (PVCs and Secrets) from previous installations will be deleted. Only set this to true when data from previous installations is no longer necessary.", + }) + f.StringVar(&flag.StringVar{ + Name: flagNamespace, + Target: &c.flagNamespace, + Default: defaultAllNamespaces, + Usage: "Namespace for the Consul installation.", + }) + f.StringVar(&flag.StringVar{ + Name: flagReleaseName, + Target: &c.flagReleaseName, + Default: defaultAnyReleaseName, + Usage: "Name of the installation. This can be used to uninstall and/or delete the resources of a specific Helm release.", + }) + f.StringVar(&flag.StringVar{ + Name: flagTimeout, + Target: &c.flagTimeout, + Default: defaultTimeout, + Usage: "Timeout to wait for uninstall.", + }) + + f = c.set.NewSet("Global Options") + f.StringVar(&flag.StringVar{ + Name: "kubeconfig", + Aliases: []string{"c"}, + Target: &c.flagKubeConfig, + Default: "", + Usage: "Path to kubeconfig file.", + }) + f.StringVar(&flag.StringVar{ + Name: "context", + Target: &c.flagKubeContext, + Default: "", + Usage: "Kubernetes context to use.", + }) + + c.help = c.set.Help() + + // c.Init() calls the embedded BaseCommand's initialization function. + c.Init() +} + +func (c *Command) Run(args []string) int { + c.once.Do(c.init) + + // The logger is initialized in main with the name cli. Here, we reset the name to uninstall so log lines would be prefixed with uninstall. + c.Log.ResetNamed("uninstall") + + defer func() { + if err := c.Close(); err != nil { + c.Log.Error(err.Error()) + os.Exit(1) + } + }() + + if err := c.set.Parse(args); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + if len(c.set.Args()) > 0 { + c.UI.Output("Should have no non-flag arguments.", terminal.WithErrorStyle()) + return 1 + } + if c.flagWipeData && !c.flagAutoApprove { + c.UI.Output("Can't set -wipe-data alone. Omit this flag to interactively uninstall, or use it with -auto-approve to wipe all data during the uninstall.", terminal.WithErrorStyle()) + return 1 + } + duration, err := time.ParseDuration(c.flagTimeout) + if err != nil { + c.UI.Output("unable to parse -%s: %s", flagTimeout, err, terminal.WithErrorStyle()) + return 1 + } + c.timeoutDuration = duration + + // helmCLI.New() will create a settings object which is used by the Helm Go SDK calls. + settings := helmCLI.New() + if c.flagKubeConfig != "" { + settings.KubeConfig = c.flagKubeConfig + } + if c.flagKubeContext != "" { + settings.KubeContext = c.flagKubeContext + } + + // Set up the kubernetes client to use for non Helm SDK calls to the Kubernetes API + // The Helm SDK will use settings.RESTClientGetter for its calls as well, so this will + // use a consistent method to target the right cluster for both Helm SDK and non Helm SDK calls. + if c.kubernetes == nil { + restConfig, err := settings.RESTClientGetter().ToRESTConfig() + if err != nil { + c.UI.Output("retrieving Kubernetes auth: %v", err, terminal.WithErrorStyle()) + return 1 + } + c.kubernetes, err = kubernetes.NewForConfig(restConfig) + if err != nil { + c.UI.Output("initializing Kubernetes client: %v", err, terminal.WithErrorStyle()) + return 1 + } + } + + // Setup logger to stream Helm library logs. + var uiLogger = func(s string, args ...interface{}) { + logMsg := fmt.Sprintf(s, args...) + c.UI.Output(logMsg, terminal.WithLibraryStyle()) + } + + c.UI.Output("Existing Installation", terminal.WithHeaderStyle()) + + // Search for Consul installation by calling `helm list`. Depends on what's already specified. + actionConfig := new(action.Configuration) + actionConfig, err = helm.InitActionConfig(actionConfig, c.flagNamespace, settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + found, foundReleaseName, foundReleaseNamespace, err := c.findExistingInstallation(settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } else { + c.UI.Output("Existing Consul installation found.", terminal.WithSuccessStyle()) + c.UI.Output("Consul Uninstall Summary", terminal.WithHeaderStyle()) + c.UI.Output("Name: %s", foundReleaseName, terminal.WithInfoStyle()) + c.UI.Output("Namespace: %s", foundReleaseNamespace, terminal.WithInfoStyle()) + + // Prompt for approval to uninstall Helm release. + if !c.flagAutoApprove { + confirmation, err := c.UI.Input(&terminal.Input{ + Prompt: "Proceed with uninstall? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + if common.Abort(confirmation) { + c.UI.Output("Uninstall aborted. To learn how to customize the uninstall, run:\nconsul-k8s uninstall --help", terminal.WithInfoStyle()) + return 1 + } + } + + // Actually call out to `helm delete`. + actionConfig, err = helm.InitActionConfig(actionConfig, foundReleaseNamespace, settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + uninstaller := action.NewUninstall(actionConfig) + uninstaller.Timeout = c.timeoutDuration + res, err := uninstaller.Run(foundReleaseName) + if err != nil { + c.UI.Output("unable to uninstall: %s", err, terminal.WithErrorStyle()) + return 1 + } + if res != nil && res.Info != "" { + c.UI.Output("Uninstall result: %s", res.Info, terminal.WithInfoStyle()) + } + c.UI.Output("Successfully uninstalled Consul Helm release", terminal.WithSuccessStyle()) + } + + // If -auto-approve=true and -wipe-data=false, we should only uninstall the release, and skip deleting resources. + if c.flagAutoApprove && !c.flagWipeData { + c.UI.Output("Skipping deleting PVCs, secrets, and service accounts.", terminal.WithSuccessStyle()) + return 0 + } + + // At this point, even if no Helm release was found and uninstalled, there could + // still be PVCs, Secrets, and Service Accounts left behind from a previous installation. + // If there isn't a foundReleaseName and foundReleaseNamespace, we'll use the values of the + // flags c.flagReleaseName and c.flagNamespace. If those are empty we'll fall back to defaults "consul" for the + // installation name and "consul" for the namespace. + if !found { + if c.flagReleaseName == "" || c.flagNamespace == "" { + foundReleaseName = common.DefaultReleaseName + foundReleaseNamespace = common.DefaultReleaseNamespace + } else { + foundReleaseName = c.flagReleaseName + foundReleaseNamespace = c.flagNamespace + } + } + + c.UI.Output("Other Consul Resources", terminal.WithHeaderStyle()) + if c.flagAutoApprove { + c.UI.Output("Deleting data for installation: ", terminal.WithInfoStyle()) + c.UI.Output("Name: %s", foundReleaseName, terminal.WithInfoStyle()) + c.UI.Output("Namespace %s", foundReleaseNamespace, terminal.WithInfoStyle()) + } + // Prompt with a warning for approval before deleting PVCs, Secrets, Service Accounts, Roles, Role Bindings, + // Jobs, Cluster Roles, and Cluster Role Bindings. + if !c.flagAutoApprove { + confirmation, err := c.UI.Input(&terminal.Input{ + Prompt: fmt.Sprintf("WARNING: Proceed with deleting PVCs, Secrets, Service Accounts, Roles, Role Bindings, Jobs, Cluster Roles, and Cluster Role Bindings for the following installation? \n\n Name: %s \n Namespace: %s \n\n Only approve if all data from this installation can be deleted. (y/N)", foundReleaseName, foundReleaseNamespace), + Style: terminal.WarningStyle, + Secret: false, + }) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + if common.Abort(confirmation) { + c.UI.Output("Uninstall aborted without deleting PVCs and Secrets.", terminal.WithInfoStyle()) + return 1 + } + } + + if err := c.deletePVCs(foundReleaseName, foundReleaseNamespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteSecrets(foundReleaseName, foundReleaseNamespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteServiceAccounts(foundReleaseName, foundReleaseNamespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteRoles(foundReleaseName, foundReleaseNamespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteRoleBindings(foundReleaseName, foundReleaseNamespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteJobs(foundReleaseName, foundReleaseNamespace); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteClusterRoles(foundReleaseName); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if err := c.deleteClusterRoleBindings(foundReleaseName); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + return 0 +} + +func (c *Command) Help() string { + c.once.Do(c.init) + s := "Usage: consul-k8s uninstall [flags]" + "\n" + "Uninstall Consul with options to delete data and resources associated with Consul installation." + "\n\n" + c.help + return s +} + +func (c *Command) Synopsis() string { + return "Uninstall Consul deployment." +} + +func (c *Command) findExistingInstallation(settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (bool, string, string, error) { + releaseName, namespace, err := common.CheckForInstallations(settings, uiLogger) + if err != nil { + return false, "", "", err + } else if c.flagNamespace == defaultAllNamespaces || c.flagNamespace == namespace { + return true, releaseName, namespace, nil + } else { + return false, "", "", fmt.Errorf("could not find consul installation in namespace %s", c.flagNamespace) + } +} + +// deletePVCs deletes any pvcs that have the label release={{foundReleaseName}} and waits for them to be deleted. +func (c *Command) deletePVCs(foundReleaseName, foundReleaseNamespace string) error { + var pvcNames []string + pvcSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + pvcs, err := c.kubernetes.CoreV1().PersistentVolumeClaims(foundReleaseNamespace).List(c.Ctx, pvcSelector) + if err != nil { + return fmt.Errorf("deletePVCs: %s", err) + } + if len(pvcs.Items) == 0 { + c.UI.Output("No PVCs found.", terminal.WithSuccessStyle()) + return nil + } + for _, pvc := range pvcs.Items { + err := c.kubernetes.CoreV1().PersistentVolumeClaims(foundReleaseNamespace).Delete(c.Ctx, pvc.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deletePVCs: error deleting PVC %q: %s", pvc.Name, err) + } + pvcNames = append(pvcNames, pvc.Name) + } + err = backoff.Retry(func() error { + pvcs, err := c.kubernetes.CoreV1().PersistentVolumeClaims(foundReleaseNamespace).List(c.Ctx, pvcSelector) + if err != nil { + return fmt.Errorf("deletePVCs: %s", err) + } + if len(pvcs.Items) > 0 { + return fmt.Errorf("deletePVCs: pvcs still exist") + } + return nil + }, backoff.WithMaxRetries(backoff.NewConstantBackOff(100*time.Millisecond), 1800)) + if err != nil { + return fmt.Errorf("deletePVCs: timed out waiting for PVCs to be deleted") + } + if len(pvcNames) > 0 { + for _, pvc := range pvcNames { + c.UI.Output("Deleted PVC => %s", pvc, terminal.WithSuccessStyle()) + } + c.UI.Output("PVCs deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteSecrets deletes any secrets that have the label "managed-by" set to "consul-k8s". +func (c *Command) deleteSecrets(foundReleaseName, foundReleaseNamespace string) error { + secrets, err := c.kubernetes.CoreV1().Secrets(foundReleaseNamespace).List(c.Ctx, metav1.ListOptions{ + LabelSelector: common.CLILabelKey + "=" + common.CLILabelValue, + }) + if err != nil { + return fmt.Errorf("deleteSecrets: %s", err) + } + if len(secrets.Items) == 0 { + c.UI.Output("No Consul secrets found.", terminal.WithSuccessStyle()) + return nil + } + var secretNames []string + for _, secret := range secrets.Items { + err := c.kubernetes.CoreV1().Secrets(foundReleaseNamespace).Delete(c.Ctx, secret.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteSecrets: error deleting Secret %q: %s", secret.Name, err) + } + secretNames = append(secretNames, secret.Name) + } + if len(secretNames) > 0 { + for _, secret := range secretNames { + c.UI.Output("Deleted Secret => %s", secret, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul secrets deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteServiceAccounts deletes service accounts that have the label release={{foundReleaseName}}. +func (c *Command) deleteServiceAccounts(foundReleaseName, foundReleaseNamespace string) error { + var serviceAccountNames []string + saSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + sas, err := c.kubernetes.CoreV1().ServiceAccounts(foundReleaseNamespace).List(c.Ctx, saSelector) + if err != nil { + return fmt.Errorf("deleteServiceAccounts: %s", err) + } + if len(sas.Items) == 0 { + c.UI.Output("No Consul service accounts found.", terminal.WithSuccessStyle()) + return nil + } + for _, sa := range sas.Items { + err := c.kubernetes.CoreV1().ServiceAccounts(foundReleaseNamespace).Delete(c.Ctx, sa.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteServiceAccounts: error deleting ServiceAccount %q: %s", sa.Name, err) + } + serviceAccountNames = append(serviceAccountNames, sa.Name) + } + if len(serviceAccountNames) > 0 { + for _, sa := range serviceAccountNames { + c.UI.Output("Deleted Service Account => %s", sa, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul service accounts deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteRoles deletes roles that have the label release={{foundReleaseName}}. +func (c *Command) deleteRoles(foundReleaseName, foundReleaseNamespace string) error { + var roleNames []string + roleSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + roles, err := c.kubernetes.RbacV1().Roles(foundReleaseNamespace).List(c.Ctx, roleSelector) + if err != nil { + return fmt.Errorf("deleteRoles: %s", err) + } + if len(roles.Items) == 0 { + c.UI.Output("No Consul roles found.", terminal.WithSuccessStyle()) + return nil + } + for _, role := range roles.Items { + err := c.kubernetes.RbacV1().Roles(foundReleaseNamespace).Delete(c.Ctx, role.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteRoles: error deleting Role %q: %s", role.Name, err) + } + roleNames = append(roleNames, role.Name) + } + if len(roleNames) > 0 { + for _, role := range roleNames { + c.UI.Output("Deleted Role => %s", role, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul roles deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteRoleBindings deletes rolebindings that have the label release={{foundReleaseName}}. +func (c *Command) deleteRoleBindings(foundReleaseName, foundReleaseNamespace string) error { + var rolebindingNames []string + rolebindingSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + rolebindings, err := c.kubernetes.RbacV1().RoleBindings(foundReleaseNamespace).List(c.Ctx, rolebindingSelector) + if err != nil { + return fmt.Errorf("deleteRoleBindings: %s", err) + } + if len(rolebindings.Items) == 0 { + c.UI.Output("No Consul rolebindings found.", terminal.WithSuccessStyle()) + return nil + } + for _, rolebinding := range rolebindings.Items { + err := c.kubernetes.RbacV1().RoleBindings(foundReleaseNamespace).Delete(c.Ctx, rolebinding.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteRoleBindings: error deleting Role %q: %s", rolebinding.Name, err) + } + rolebindingNames = append(rolebindingNames, rolebinding.Name) + } + if len(rolebindingNames) > 0 { + for _, rolebinding := range rolebindingNames { + c.UI.Output("Deleted Role Binding => %s", rolebinding, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul rolebindings deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteJobs deletes jobs that have the label release={{foundReleaseName}}. +func (c *Command) deleteJobs(foundReleaseName, foundReleaseNamespace string) error { + var jobNames []string + jobSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + jobs, err := c.kubernetes.BatchV1().Jobs(foundReleaseNamespace).List(c.Ctx, jobSelector) + if err != nil { + return fmt.Errorf("deleteJobs: %s", err) + } + if len(jobs.Items) == 0 { + c.UI.Output("No Consul jobs found.", terminal.WithSuccessStyle()) + return nil + } + for _, job := range jobs.Items { + err := c.kubernetes.BatchV1().Jobs(foundReleaseNamespace).Delete(c.Ctx, job.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteJobs: error deleting Job %q: %s", job.Name, err) + } + jobNames = append(jobNames, job.Name) + } + if len(jobNames) > 0 { + for _, job := range jobNames { + c.UI.Output("Deleted Jobs => %s", job, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul jobs deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteClusterRoles deletes clusterRoles that have the label release={{foundReleaseName}}. +func (c *Command) deleteClusterRoles(foundReleaseName string) error { + var clusterRolesNames []string + clusterRolesSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + clusterRoles, err := c.kubernetes.RbacV1().ClusterRoles().List(c.Ctx, clusterRolesSelector) + if err != nil { + return fmt.Errorf("deleteClusterRoles: %s", err) + } + if len(clusterRoles.Items) == 0 { + c.UI.Output("No Consul cluster roles found.", terminal.WithSuccessStyle()) + return nil + } + for _, clusterRole := range clusterRoles.Items { + err := c.kubernetes.RbacV1().ClusterRoles().Delete(c.Ctx, clusterRole.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteClusterRoles: error deleting cluster role %q: %s", clusterRole.Name, err) + } + clusterRolesNames = append(clusterRolesNames, clusterRole.Name) + } + if len(clusterRolesNames) > 0 { + for _, clusterRole := range clusterRolesNames { + c.UI.Output("Deleted cluster role => %s", clusterRole, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul cluster roles deleted.", terminal.WithSuccessStyle()) + } + return nil +} + +// deleteClusterRoleBindings deletes clusterrolebindings that have the label release={{foundReleaseName}}. +func (c *Command) deleteClusterRoleBindings(foundReleaseName string) error { + var clusterRoleBindingsNames []string + clusterRoleBindingsSelector := metav1.ListOptions{LabelSelector: fmt.Sprintf("release=%s", foundReleaseName)} + clusterRoleBindings, err := c.kubernetes.RbacV1().ClusterRoleBindings().List(c.Ctx, clusterRoleBindingsSelector) + if err != nil { + return fmt.Errorf("deleteClusterRoleBindings: %s", err) + } + if len(clusterRoleBindings.Items) == 0 { + c.UI.Output("No Consul cluster role bindings found.", terminal.WithSuccessStyle()) + return nil + } + for _, clusterRoleBinding := range clusterRoleBindings.Items { + err := c.kubernetes.RbacV1().ClusterRoleBindings().Delete(c.Ctx, clusterRoleBinding.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleteClusterRoleBindings: error deleting cluster role binding %q: %s", clusterRoleBinding.Name, err) + } + clusterRoleBindingsNames = append(clusterRoleBindingsNames, clusterRoleBinding.Name) + } + if len(clusterRoleBindingsNames) > 0 { + for _, clusterRoleBinding := range clusterRoleBindingsNames { + c.UI.Output("Deleted cluster role binding => %s", clusterRoleBinding, terminal.WithSuccessStyle()) + } + c.UI.Output("Consul cluster role bindings deleted.", terminal.WithSuccessStyle()) + } + return nil +} diff --git a/cli/cmd/uninstall/uninstall_test.go b/cli/cmd/uninstall/uninstall_test.go new file mode 100644 index 0000000000..8f1df8de80 --- /dev/null +++ b/cli/cmd/uninstall/uninstall_test.go @@ -0,0 +1,366 @@ +package uninstall + +import ( + "context" + "os" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestDeletePVCs(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + pvc := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-server-test1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + pvc2 := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-server-test2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + pvc3 := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unrelated-pvc", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deletePVCs("consul", "default") + require.NoError(t, err) + pvcs, err := c.kubernetes.CoreV1().PersistentVolumeClaims("default").List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, pvcs.Items, 1) + require.Equal(t, pvcs.Items[0].Name, pvc3.Name) +} + +func TestDeleteSecrets(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-secret1", + Labels: map[string]string{ + "release": "consul", + common.CLILabelKey: common.CLILabelValue, + }, + }, + } + secret2 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-secret2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + secret3 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unrelated-test-secret3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.CoreV1().Secrets("default").Create(context.Background(), secret3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteSecrets("consul", "default") + require.NoError(t, err) + secrets, err := c.kubernetes.CoreV1().Secrets("default").List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + + // Only secret1 should have been deleted, secret2 and secret 3 persist since it doesn't have the label. + require.Len(t, secrets.Items, 2) +} + +func TestDeleteServiceAccounts(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + sa := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-sa1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + sa2 := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-sa2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + sa3 := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-sa3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.CoreV1().ServiceAccounts("default").Create(context.Background(), sa, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.CoreV1().ServiceAccounts("default").Create(context.Background(), sa2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.CoreV1().ServiceAccounts("default").Create(context.Background(), sa3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteServiceAccounts("consul", "default") + require.NoError(t, err) + sas, err := c.kubernetes.CoreV1().ServiceAccounts("default").List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, sas.Items, 1) + require.Equal(t, sas.Items[0].Name, sa3.Name) +} + +func TestDeleteRoles(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + role2 := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + role3 := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.RbacV1().Roles("default").Create(context.Background(), role, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().Roles("default").Create(context.Background(), role2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().Roles("default").Create(context.Background(), role3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteRoles("consul", "default") + require.NoError(t, err) + roles, err := c.kubernetes.RbacV1().Roles("default").List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, roles.Items, 1) + require.Equal(t, roles.Items[0].Name, role3.Name) +} + +func TestDeleteRoleBindings(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + rolebinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + rolebinding2 := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + rolebinding3 := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-role3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.RbacV1().RoleBindings("default").Create(context.Background(), rolebinding, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().RoleBindings("default").Create(context.Background(), rolebinding2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().RoleBindings("default").Create(context.Background(), rolebinding3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteRoleBindings("consul", "default") + require.NoError(t, err) + rolebindings, err := c.kubernetes.RbacV1().RoleBindings("default").List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, rolebindings.Items, 1) + require.Equal(t, rolebindings.Items[0].Name, rolebinding3.Name) +} + +func TestDeleteJobs(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-job1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + job2 := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-job2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + job3 := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-job3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.BatchV1().Jobs("default").Create(context.Background(), job, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.BatchV1().Jobs("default").Create(context.Background(), job2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.BatchV1().Jobs("default").Create(context.Background(), job3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteJobs("consul", "default") + require.NoError(t, err) + jobs, err := c.kubernetes.BatchV1().Jobs("default").List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, jobs.Items, 1) + require.Equal(t, jobs.Items[0].Name, job3.Name) +} + +func TestDeleteClusterRoles(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + clusterrole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrole1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrole2 := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrole2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrole3 := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrole3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.RbacV1().ClusterRoles().Create(context.Background(), clusterrole, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoles().Create(context.Background(), clusterrole2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoles().Create(context.Background(), clusterrole3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteClusterRoles("consul") + require.NoError(t, err) + clusterroles, err := c.kubernetes.RbacV1().ClusterRoles().List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, clusterroles.Items, 1) + require.Equal(t, clusterroles.Items[0].Name, clusterrole3.Name) +} + +func TestDeleteClusterRoleBindings(t *testing.T) { + c := getInitializedCommand(t) + c.kubernetes = fake.NewSimpleClientset() + clusterrolebinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrolebinding1", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrolebinding2 := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrolebinding2", + Labels: map[string]string{ + "release": "consul", + }, + }, + } + clusterrolebinding3 := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-test-clusterrolebinding3", + Labels: map[string]string{ + "release": "unrelated", + }, + }, + } + _, err := c.kubernetes.RbacV1().ClusterRoleBindings().Create(context.Background(), clusterrolebinding, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoleBindings().Create(context.Background(), clusterrolebinding2, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = c.kubernetes.RbacV1().ClusterRoleBindings().Create(context.Background(), clusterrolebinding3, metav1.CreateOptions{}) + require.NoError(t, err) + err = c.deleteClusterRoleBindings("consul") + require.NoError(t, err) + clusterrolebindings, err := c.kubernetes.RbacV1().ClusterRoleBindings().List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, clusterrolebindings.Items, 1) + require.Equal(t, clusterrolebindings.Items[0].Name, clusterrolebinding3.Name) +} + +// getInitializedCommand sets up a command struct for tests. +func getInitializedCommand(t *testing.T) *Command { + t.Helper() + log := hclog.New(&hclog.LoggerOptions{ + Name: "cli", + Level: hclog.Info, + Output: os.Stdout, + }) + + baseCommand := &common.BaseCommand{ + Log: log, + } + + c := &Command{ + BaseCommand: baseCommand, + } + c.init() + return c +} diff --git a/cli/cmd/upgrade/upgrade.go b/cli/cmd/upgrade/upgrade.go new file mode 100644 index 0000000000..a0923e3089 --- /dev/null +++ b/cli/cmd/upgrade/upgrade.go @@ -0,0 +1,419 @@ +package upgrade + +import ( + "errors" + "fmt" + "os" + "strings" + "sync" + "time" + + consulChart "github.com/hashicorp/consul-k8s/charts" + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/config" + "github.com/hashicorp/consul-k8s/cli/helm" + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/getter" + "k8s.io/client-go/kubernetes" +) + +const ( + flagNamePreset = "preset" + defaultPreset = "" + + flagNameConfigFile = "config-file" + flagNameSetStringValues = "set-string" + flagNameSetValues = "set" + flagNameFileValues = "set-file" + + flagNameDryRun = "dry-run" + defaultDryRun = false + + flagNameAutoApprove = "auto-approve" + defaultAutoApprove = false + + flagNameTimeout = "timeout" + defaultTimeout = "10m" + + flagNameVerbose = "verbose" + defaultVerbose = false + + flagNameWait = "wait" + defaultWait = true +) + +type Command struct { + *common.BaseCommand + + kubernetes kubernetes.Interface + + set *flag.Sets + + flagPreset string + flagDryRun bool + flagAutoApprove bool + flagValueFiles []string + flagSetStringValues []string + flagSetValues []string + flagFileValues []string + flagTimeout string + timeoutDuration time.Duration + flagVerbose bool + flagWait bool + + flagKubeConfig string + flagKubeContext string + + once sync.Once + help string +} + +func (c *Command) init() { + // Store all the possible preset values in 'presetList'. Printed in the help message. + var presetList []string + for name := range config.Presets { + presetList = append(presetList, name) + } + + c.set = flag.NewSets() + f := c.set.NewSet("Command Options") + f.BoolVar(&flag.BoolVar{ + Name: flagNameAutoApprove, + Target: &c.flagAutoApprove, + Default: defaultAutoApprove, + Usage: "Skip confirmation prompt.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameDryRun, + Target: &c.flagDryRun, + Default: defaultDryRun, + Usage: "Perform pre-upgrade checks and display summary of upgrade.", + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameConfigFile, + Aliases: []string{"f"}, + Target: &c.flagValueFiles, + Usage: "Set the path to a file to customize the upgrade, such as Consul Helm chart values file. Can be specified multiple times.", + }) + f.StringVar(&flag.StringVar{ + Name: flagNamePreset, + Target: &c.flagPreset, + Default: defaultPreset, + Usage: fmt.Sprintf("Use an upgrade preset, one of %s. Defaults to none", strings.Join(presetList, ", ")), + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameSetValues, + Target: &c.flagSetValues, + Usage: "Set a value to customize. Can be specified multiple times. Supports Consul Helm chart values.", + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameFileValues, + Target: &c.flagFileValues, + Usage: "Set a value to customize using a file. The contents of the file will be set as the value." + + "Can be specified multiple times. Supports Consul Helm chart values.", + }) + f.StringSliceVar(&flag.StringSliceVar{ + Name: flagNameSetStringValues, + Target: &c.flagSetStringValues, + Usage: "Set a string value to customize. Can be specified multiple times. Supports Consul Helm chart values.", + }) + f.StringVar(&flag.StringVar{ + Name: flagNameTimeout, + Target: &c.flagTimeout, + Default: defaultTimeout, + Usage: "Set a timeout to wait for upgrade to be ready.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameVerbose, + Aliases: []string{"v"}, + Target: &c.flagVerbose, + Default: defaultVerbose, + Usage: "Output verbose logs from the command with the status of resources being upgraded.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameWait, + Target: &c.flagWait, + Default: defaultWait, + Usage: "Wait for Kubernetes resources in upgrade to be ready before exiting command.", + }) + + f = c.set.NewSet("Global Options") + f.StringVar(&flag.StringVar{ + Name: "kubeconfig", + Aliases: []string{"c"}, + Target: &c.flagKubeConfig, + Default: "", + Usage: "Set the path to kubeconfig file.", + }) + f.StringVar(&flag.StringVar{ + Name: "context", + Target: &c.flagKubeContext, + Default: "", + Usage: "Set the Kubernetes context to use.", + }) + + c.help = c.set.Help() + + // c.Init() calls the embedded BaseCommand's initialization function. + c.Init() +} + +func (c *Command) Run(args []string) int { + c.once.Do(c.init) + c.Log.ResetNamed("upgrade") + + defer common.CloseWithError(c.BaseCommand) + + err := c.validateFlags(args) + if err != nil { + c.UI.Output(err.Error()) + return 1 + } + + if c.flagDryRun { + c.UI.Output("Performing dry run upgrade. No changes will be made to the cluster.", terminal.WithInfoStyle()) + } + + c.timeoutDuration, err = time.ParseDuration(c.flagTimeout) + if err != nil { + c.UI.Output(fmt.Sprintf("Invalid timeout: %s", err)) + return 1 + } + + // helmCLI.New() will create a settings object which is used by the Helm Go SDK calls. + settings := helmCLI.New() + + // Any overrides by our kubeconfig and kubecontext flags is done here. The Kube client that + // is created will use this command's flags first, then the HELM_KUBECONTEXT environment variable, + // then call out to genericclioptions.ConfigFlag + if c.flagKubeConfig != "" { + settings.KubeConfig = c.flagKubeConfig + } + if c.flagKubeContext != "" { + settings.KubeContext = c.flagKubeContext + } + + // Set up the kubernetes client to use for non Helm SDK calls to the Kubernetes API + // The Helm SDK will use settings.RESTClientGetter for its calls as well, so this will + // use a consistent method to target the right cluster for both Helm SDK and non Helm SDK calls. + if c.kubernetes == nil { + restConfig, err := settings.RESTClientGetter().ToRESTConfig() + if err != nil { + c.UI.Output("Error retrieving Kubernetes authentication:\n%v", err, terminal.WithErrorStyle()) + return 1 + } + c.kubernetes, err = kubernetes.NewForConfig(restConfig) + if err != nil { + c.UI.Output("Error initializing Kubernetes client:\n%v", err, terminal.WithErrorStyle()) + return 1 + } + } + + c.UI.Output("Checking if Consul can be upgraded", terminal.WithHeaderStyle()) + uiLogger := c.createUILogger() + name, namespace, err := common.CheckForInstallations(settings, uiLogger) + if err != nil { + c.UI.Output("Cannot upgrade Consul. Existing Consul installation not found. Use the command `consul-k8s install` to install Consul.", terminal.WithErrorStyle()) + return 1 + } + c.UI.Output("Existing Consul installation found to be upgraded.", terminal.WithSuccessStyle()) + c.UI.Output("Name: %s\nNamespace: %s", name, namespace, terminal.WithInfoStyle()) + + chart, err := helm.LoadChart(consulChart.ConsulHelmChart, common.TopLevelChartDirName) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + c.UI.Output("Loaded charts", terminal.WithSuccessStyle()) + + currentChartValues, err := helm.FetchChartValues(namespace, settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // Handle preset, value files, and set values logic. + chartValues, err := c.mergeValuesFlagsWithPrecedence(settings) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // Without informing the user, default global.name to consul if it hasn't been set already. We don't allow setting + // the release name, and since that is hardcoded to "consul", setting global.name to "consul" makes it so resources + // aren't double prefixed with "consul-consul-...". + chartValues = common.MergeMaps(config.Convert(config.GlobalNameConsul), chartValues) + + // Print out the upgrade summary. + if err = c.printDiff(currentChartValues, chartValues); err != nil { + c.UI.Output("Could not print the different between current and upgraded charts: %v", err, terminal.WithErrorStyle()) + return 1 + } + + // Check if the user is OK with the upgrade unless the auto approve or dry run flags are true. + if !c.flagAutoApprove && !c.flagDryRun { + confirmation, err := c.UI.Input(&terminal.Input{ + Prompt: "Proceed with upgrade? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + if common.Abort(confirmation) { + c.UI.Output("Upgrade aborted. Use the command `consul-k8s upgrade -help` to learn how to customize your upgrade.", + terminal.WithInfoStyle()) + return 1 + } + } + + if !c.flagDryRun { + c.UI.Output("Upgrading Consul", terminal.WithHeaderStyle()) + } else { + c.UI.Output("Performing Dry Run Upgrade", terminal.WithHeaderStyle()) + } + + // Setup action configuration for Helm Go SDK function calls. + actionConfig := new(action.Configuration) + actionConfig, err = helm.InitActionConfig(actionConfig, namespace, settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // Setup the upgrade action. + upgrade := action.NewUpgrade(actionConfig) + upgrade.Namespace = namespace + upgrade.DryRun = c.flagDryRun + upgrade.Wait = c.flagWait + upgrade.Timeout = c.timeoutDuration + + // Run the upgrade. Note that the dry run config is passed into the upgrade action, so upgrade.Run is called even during a dry run. + _, err = upgrade.Run(common.DefaultReleaseName, chart, chartValues) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if c.flagDryRun { + c.UI.Output("Dry run complete. No changes were made to the Kubernetes cluster.\n"+ + "Upgrade can proceed with this configuration.", terminal.WithInfoStyle()) + return 0 + } + + c.UI.Output("Consul upgraded in namespace %q.", namespace, terminal.WithSuccessStyle()) + return 0 +} + +// validateFlags checks that the user's provided flags are valid. +func (c *Command) validateFlags(args []string) error { + if err := c.set.Parse(args); err != nil { + return err + } + if len(c.set.Args()) > 0 { + return errors.New("should have no non-flag arguments") + } + if len(c.flagValueFiles) != 0 && c.flagPreset != defaultPreset { + return fmt.Errorf("cannot set both -%s and -%s", flagNameConfigFile, flagNamePreset) + } + if _, ok := config.Presets[c.flagPreset]; c.flagPreset != defaultPreset && !ok { + return fmt.Errorf("'%s' is not a valid preset", c.flagPreset) + } + if _, err := time.ParseDuration(c.flagTimeout); err != nil { + return fmt.Errorf("unable to parse -%s: %s", flagNameTimeout, err) + } + if len(c.flagValueFiles) != 0 { + for _, filename := range c.flagValueFiles { + if _, err := os.Stat(filename); err != nil && os.IsNotExist(err) { + return fmt.Errorf("file '%s' does not exist", filename) + } + } + } + + return nil +} + +// mergeValuesFlagsWithPrecedence is responsible for merging all the values to determine the values file for the +// upgrade based on the following precedence order from lowest to highest: +// 1. -preset +// 2. -f values-file +// 3. -set +// 4. -set-string +// 5. -set-file +// For example, -set-file will override a value provided via -set. +// Within each of these groups the rightmost flag value has the highest precedence. +func (c *Command) mergeValuesFlagsWithPrecedence(settings *helmCLI.EnvSettings) (map[string]interface{}, error) { + p := getter.All(settings) + v := &values.Options{ + ValueFiles: c.flagValueFiles, + StringValues: c.flagSetStringValues, + Values: c.flagSetValues, + FileValues: c.flagFileValues, + } + vals, err := v.MergeValues(p) + if err != nil { + return nil, fmt.Errorf("error merging values: %s", err) + } + if c.flagPreset != defaultPreset { + // Note the ordering of the function call, presets have lower precedence than set vals. + presetMap := config.Presets[c.flagPreset].(map[string]interface{}) + vals = common.MergeMaps(presetMap, vals) + } + return vals, err +} + +// Help returns a description of the command and how it is used. +func (c *Command) Help() string { + c.once.Do(c.init) + return c.Synopsis() + "\n\nUsage: consul-k8s upgrade [flags]\n\n" + c.help +} + +// Synopsis returns a one-line command summary. +func (c *Command) Synopsis() string { + return "Upgrade Consul on Kubernetes from an existing installation." +} + +// createUILogger creates a logger that will write to the UI. +func (c *Command) createUILogger() func(string, ...interface{}) { + return func(s string, args ...interface{}) { + logMsg := fmt.Sprintf(s, args...) + + if c.flagVerbose { + // Only output all logs when verbose is enabled + c.UI.Output(logMsg, terminal.WithLibraryStyle()) + } else { + // When verbose is not enabled, output all logs except not ready messages for resources + if !strings.Contains(logMsg, "not ready") { + c.UI.Output(logMsg, terminal.WithLibraryStyle()) + } + } + } +} + +// printDiff marshals both maps to YAML and prints the diff between the two. +func (c *Command) printDiff(old, new map[string]interface{}) error { + diff, err := common.Diff(old, new) + if err != nil { + return err + } + + c.UI.Output("\nDifference between user overrides for current and upgraded charts"+ + "\n--------------------------------------------------------------", terminal.WithInfoStyle()) + for _, line := range strings.Split(diff, "\n") { + if strings.HasPrefix(line, "+") { + c.UI.Output(line, terminal.WithDiffAddedStyle()) + } else if strings.HasPrefix(line, "-") { + c.UI.Output(line, terminal.WithDiffRemovedStyle()) + } else { + c.UI.Output(line, terminal.WithDiffUnchangedStyle()) + } + } + + return nil +} diff --git a/cli/cmd/upgrade/upgrade_test.go b/cli/cmd/upgrade/upgrade_test.go new file mode 100644 index 0000000000..f70496bda6 --- /dev/null +++ b/cli/cmd/upgrade/upgrade_test.go @@ -0,0 +1,68 @@ +package upgrade + +import ( + "os" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/go-hclog" +) + +// TestValidateFlags tests the validate flags function. +func TestValidateFlags(t *testing.T) { + // The following cases should all error, if they fail to this test fails. + testCases := []struct { + description string + input []string + }{ + { + "Should disallow non-flag arguments.", + []string{"foo", "-auto-approve"}, + }, + { + "Should disallow specifying both values file AND presets.", + []string{"-f='f.txt'", "-preset=demo"}, + }, + { + "Should error on invalid presets.", + []string{"-preset=foo"}, + }, + { + "Should error on invalid timeout.", + []string{"-timeout=invalid-timeout"}, + }, + { + "Should have errored on a non-existant file.", + []string{"-f=\"does_not_exist.txt\""}, + }, + } + + for _, testCase := range testCases { + c := getInitializedCommand(t) + t.Run(testCase.description, func(t *testing.T) { + if err := c.validateFlags(testCase.input); err == nil { + t.Errorf("Test case should have failed.") + } + }) + } +} + +// getInitializedCommand sets up a command struct for tests. +func getInitializedCommand(t *testing.T) *Command { + t.Helper() + log := hclog.New(&hclog.LoggerOptions{ + Name: "cli", + Level: hclog.Info, + Output: os.Stdout, + }) + + baseCommand := &common.BaseCommand{ + Log: log, + } + + c := &Command{ + BaseCommand: baseCommand, + } + c.init() + return c +} diff --git a/cli/cmd/version/version.go b/cli/cmd/version/version.go new file mode 100644 index 0000000000..b8b2235f46 --- /dev/null +++ b/cli/cmd/version/version.go @@ -0,0 +1,38 @@ +package version + +import ( + "sync" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" +) + +type Command struct { + *common.BaseCommand + + // Version is the Consul on Kubernetes CLI version. + Version string + + once sync.Once +} + +func (c *Command) init() { + c.Init() +} + +// Run prints the version of the Consul on Kubernetes CLI. +func (c *Command) Run(_ []string) int { + c.once.Do(c.init) + c.UI.Output("consul-k8s %s", c.Version, terminal.WithInfoStyle()) + return 0 +} + +// Help returns a description of the command and how it is used. +func (c *Command) Help() string { + return "Usage: consul-k8s version\n\n" + c.Synopsis() +} + +// Synopsis returns a one-line command summary. +func (c *Command) Synopsis() string { + return "Print the version of the Consul on Kubernetes CLI." +} diff --git a/cli/commands.go b/cli/commands.go new file mode 100644 index 0000000000..681421c502 --- /dev/null +++ b/cli/commands.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + + "github.com/hashicorp/consul-k8s/cli/cmd/install" + "github.com/hashicorp/consul-k8s/cli/cmd/status" + "github.com/hashicorp/consul-k8s/cli/cmd/uninstall" + "github.com/hashicorp/consul-k8s/cli/cmd/upgrade" + cmdversion "github.com/hashicorp/consul-k8s/cli/cmd/version" + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/version" + "github.com/hashicorp/go-hclog" + "github.com/mitchellh/cli" +) + +func initializeCommands(ctx context.Context, log hclog.Logger) (*common.BaseCommand, map[string]cli.CommandFactory) { + + baseCommand := &common.BaseCommand{ + Ctx: ctx, + Log: log, + } + + commands := map[string]cli.CommandFactory{ + "install": func() (cli.Command, error) { + return &install.Command{ + BaseCommand: baseCommand, + }, nil + }, + "uninstall": func() (cli.Command, error) { + return &uninstall.Command{ + BaseCommand: baseCommand, + }, nil + }, + "status": func() (cli.Command, error) { + return &status.Command{ + BaseCommand: baseCommand, + }, nil + }, + "upgrade": func() (cli.Command, error) { + return &upgrade.Command{ + BaseCommand: baseCommand, + }, nil + }, + "version": func() (cli.Command, error) { + return &cmdversion.Command{ + BaseCommand: baseCommand, + Version: version.GetHumanVersion(), + }, nil + }, + } + + return baseCommand, commands +} diff --git a/cli/common/base.go b/cli/common/base.go new file mode 100644 index 0000000000..81bb360267 --- /dev/null +++ b/cli/common/base.go @@ -0,0 +1,44 @@ +package common + +import ( + "context" + "io" + + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/go-hclog" +) + +// BaseCommand is embedded in all commands to provide common logic and data. +type BaseCommand struct { + // Ctx is the base context for the command. It is up to commands to + // utilize this context so that cancellation works in a timely manner. + Ctx context.Context + + // Log is the logger to use. + Log hclog.Logger + + // UI is used to write to the CLI. + UI terminal.UI +} + +// Close cleans up any resources that the command created. This should be +// defered by any CLI command that embeds baseCommand in the Run command. +func (c *BaseCommand) Close() error { + // Close our UI if it implements it. The glint-based UI does for example + // to finish up all the CLI output. + var err error + if closer, ok := c.UI.(io.Closer); ok && closer != nil { + err = closer.Close() + } + if err != nil { + return err + } + + return nil +} + +// Init should be called FIRST within the Run function implementation. +func (c *BaseCommand) Init() { + ui := terminal.NewBasicUI(c.Ctx) + c.UI = ui +} diff --git a/cli/common/diff.go b/cli/common/diff.go new file mode 100644 index 0000000000..30d03e2968 --- /dev/null +++ b/cli/common/diff.go @@ -0,0 +1,148 @@ +package common + +import ( + "sort" + "strings" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/yaml" +) + +// Diff returns a string representation of the difference between two maps as YAML. +// The returned string is sorted alphabetically by key. If the maps are identical, the returned string is empty. +func Diff(a, b map[string]interface{}) (string, error) { + if len(a) == 0 && len(b) == 0 { + return "", nil + } + + return diffRecursively(a, b, 0) +} + +// diffRecursively iterates over maps `a` and `b` and returns a string representation of the difference between them as YAML. +// The returned string is sorted alphabetically by key with `+` and `-` prefixed to "diffed" lines. +// `c` is a map of the default values for the chart. If a key is present in `a`, but not `b` (i.e. removed), +// the value is compared with the default value in `c` to prevent a false positive "removed" line. +func diffRecursively(a, b map[string]interface{}, recurseDepth int) (string, error) { + buf := new(strings.Builder) + + // Get the union of keys in a and b sorted alphabetically. + keys := collectKeys(a, b) + + for _, key := range keys { + valueInA, inA := a[key] + valueInB, inB := b[key] + + aMapWithKey := map[string]interface{}{ + key: valueInA, + } + bMapWithKey := map[string]interface{}{ + key: valueInB, + } + + // If the key is in both a and b, compare the values. + if inA && inB { + // If the map slices are the same, write as unchanged YAML. + if cmp.Equal(aMapWithKey, bMapWithKey) { + asYaml, err := yaml.Marshal(aMapWithKey) + if err != nil { + return "", err + } + + writeWithPrepend(" ", string(asYaml), recurseDepth, buf) + continue + } + + // If the maps are different and there is another level of depth to the map, recurse. + if !isMaxDepth(aMapWithKey) && !isMaxDepth(bMapWithKey) { + writeWithPrepend(" ", key+":", recurseDepth, buf) + + childDiff, err := diffRecursively(valueInA.(map[string]interface{}), valueInB.(map[string]interface{}), recurseDepth+1) + if err != nil { + return "", err + } + + buf.WriteString(childDiff) + + continue + } + + // If the map slices are different and there is no other level of depth to the map, write as changed YAML. + aSliceAsYaml, err := yaml.Marshal(aMapWithKey) + if err != nil { + return "", err + } + + bSliceAsYaml, err := yaml.Marshal(bMapWithKey) + if err != nil { + return "", err + } + + writeWithPrepend("- ", string(aSliceAsYaml), recurseDepth, buf) + writeWithPrepend("+ ", string(bSliceAsYaml), recurseDepth, buf) + } + + // If the key is in `a` but not in `b`, write as removed unless `a` matches the value in `c`. + if inA && !inB { + asYaml, err := yaml.Marshal(aMapWithKey) + if err != nil { + return "", err + } + + writeWithPrepend("- ", string(asYaml), recurseDepth, buf) + continue + } + + // If the key is in b but not in a, write as added. + if !inA && inB { + asYaml, err := yaml.Marshal(bMapWithKey) + if err != nil { + return "", err + } + + writeWithPrepend("+ ", string(asYaml), recurseDepth, buf) + continue + } + } + + return buf.String(), nil +} + +// collectKeys iterates over both maps and collects all keys sorted alphabetically, ignoring duplicates. +func collectKeys(a, b map[string]interface{}) []string { + keys := make([]string, 0, len(a)+len(b)) + for key := range a { + keys = append(keys, key) + } + for key := range b { + if _, ok := a[key]; !ok { + keys = append(keys, key) + } + } + + sort.Strings(keys) + return keys +} + +// writeWithPrepend writes each line to the buffer with the given prefix and indentation matching the recurse depth. +func writeWithPrepend(prepend, text string, recurseDepth int, buf *strings.Builder) { + lines := strings.Split(strings.TrimSpace(text), "\n") + for _, line := range lines { + buf.WriteString(prepend) + for i := 0; i < recurseDepth; i++ { + buf.WriteString(" ") + } + buf.WriteString(line) + buf.WriteString("\n") + } +} + +// isMaxDepth returns false if any of the values in the map are maps. +func isMaxDepth(m map[string]interface{}) bool { + for _, value := range m { + if _, ok := value.(map[string]interface{}); ok { + return false + } + } + + return true +} diff --git a/cli/common/diff_test.go b/cli/common/diff_test.go new file mode 100644 index 0000000000..31563e0b6b --- /dev/null +++ b/cli/common/diff_test.go @@ -0,0 +1,171 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDiff(t *testing.T) { + type args struct { + a map[string]interface{} + b map[string]interface{} + } + + tests := []struct { + name string + args args + expected string + }{ + { + name: "Two empty maps should return an empty string", + args: args{ + a: map[string]interface{}{}, + b: map[string]interface{}{}, + }, + expected: "", + }, + { + name: "Two equal maps should return the map parsed as YAML", + args: args{ + a: map[string]interface{}{ + "foo": "bar", + "baz": "qux", + "liz": map[string]interface{}{"qux": []string{"quux", "quuz"}}, + }, + b: map[string]interface{}{ + "foo": "bar", + "baz": "qux", + "liz": map[string]interface{}{"qux": []string{"quux", "quuz"}}, + }, + }, + expected: " baz: qux\n foo: bar\n liz:\n qux:\n - quux\n - quuz\n", + }, + { + name: "New elements should be prefixed with a plus sign", + args: args{ + a: map[string]interface{}{ + "foo": "bar", + }, + b: map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + }, + expected: "+ baz: qux\n foo: bar\n", + }, + { + name: "New non-string elements should be prefixed with a plus sign", + args: args{ + a: map[string]interface{}{ + "foo": "bar", + }, + b: map[string]interface{}{ + "foo": "bar", + "baz": []string{"qux"}, + "qux": map[string]string{ + "quux": "corge", + }, + }, + }, + expected: "+ baz:\n+ - qux\n foo: bar\n+ qux:\n+ quux: corge\n", + }, + { + name: "Deleted elements should be prefixed with a minus sign", + args: args{ + a: map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + b: map[string]interface{}{ + "foo": "bar", + }, + }, + expected: "- baz: qux\n foo: bar\n", + }, + { + name: "Deleted non-string elements should be prefixed with a minus sign", + args: args{ + a: map[string]interface{}{ + "foo": "bar", + "baz": []string{"qux"}, + "qux": map[string]string{ + "quux": "corge", + }, + }, + b: map[string]interface{}{ + "foo": "bar", + }, + }, + expected: "- baz:\n- - qux\n foo: bar\n- qux:\n- quux: corge\n", + }, + { + name: "Diff between two complex maps should be correct", + args: args{ + a: map[string]interface{}{ + "global": map[string]interface{}{ + "name": "consul", + "metrics": map[string]interface{}{ + "enabled": true, + "enableAgentMetrics": true, + }, + }, + "connectInject": map[string]interface{}{ + "enabled": true, + "metrics": map[string]interface{}{ + "defaultEnabled": true, + "defaultEnableMerging": true, + "enableGatewayMetrics": true, + }, + }, + "server": map[string]interface{}{ + "replicas": 1, + }, + "controller": map[string]interface{}{ + "enabled": true, + }, + "ui": map[string]interface{}{ + "enabled": true, + "service": map[string]interface{}{ + "enabled": true, + }, + }, + "prometheus": map[string]interface{}{ + "enabled": true, + }, + }, + b: map[string]interface{}{ + "global": map[string]interface{}{ + "name": "consul", + "gossipEncryption": map[string]interface{}{ + "autoGenerate": true, + }, + "tls": map[string]interface{}{ + "enabled": true, + "enableAutoEncrypt": true, + }, + "acls": map[string]interface{}{ + "manageSystemACLs": true, + }, + }, + "server": map[string]interface{}{"replicas": 1}, + "connectInject": map[string]interface{}{ + "enabled": true, + }, + "controller": map[string]interface{}{ + "enabled": true, + }, + }, + }, + expected: " connectInject:\n enabled: true\n- metrics:\n- defaultEnableMerging: true\n- defaultEnabled: true\n- enableGatewayMetrics: true\n controller:\n enabled: true\n global:\n+ acls:\n+ manageSystemACLs: true\n+ gossipEncryption:\n+ autoGenerate: true\n- metrics:\n- enableAgentMetrics: true\n- enabled: true\n name: consul\n+ tls:\n+ enableAutoEncrypt: true\n+ enabled: true\n- prometheus:\n- enabled: true\n server:\n replicas: 1\n- ui:\n- enabled: true\n- service:\n- enabled: true\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := Diff(tt.args.a, tt.args.b) + require.NoError(t, err) + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/cli/common/flag/doc.go b/cli/common/flag/doc.go new file mode 100644 index 0000000000..1e40f0742c --- /dev/null +++ b/cli/common/flag/doc.go @@ -0,0 +1,5 @@ +// Package flag is a thin layer over the stdlib flag package that provides +// some minimal features such as aliasing, autocompletion handling, improved +// defaults, etc. It was created for mitchellh/cli but can work as a standalone +// package. Source: https://github.com/hashicorp/waypoint/tree/348d2c77fce199952618ccef6433c8844b22583b/internal/pkg/flag, or release tag 0.5.1. +package flag diff --git a/cli/common/flag/flag.go b/cli/common/flag/flag.go new file mode 100644 index 0000000000..a0ccf2b239 --- /dev/null +++ b/cli/common/flag/flag.go @@ -0,0 +1,40 @@ +package flag + +import ( + "regexp" + "strings" + + "github.com/kr/text" +) + +// maxLineLength is the maximum width of any line. +const maxLineLength int = 78 + +// reRemoveWhitespace is a regular expression for stripping whitespace from +// a string. +var reRemoveWhitespace = regexp.MustCompile(`[\s]+`) + +// FlagExample is an interface which declares an example value. This is +// used in help generation to provide better help text. +type FlagExample interface { + Example() string +} + +// FlagVisibility is an interface which declares whether a flag should be +// hidden from help and completions. This is usually used for deprecations +// on "internal-only" flags. +type FlagVisibility interface { + Hidden() bool +} + +// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking +// into account any provided left padding. +func wrapAtLengthWithPadding(s string, pad int) string { + wrapped := text.Wrap(s, maxLineLength-pad) + lines := strings.Split(wrapped, "\n") + for i, line := range lines { + lines[i] = strings.Repeat(" ", pad) + line + } + + return strings.Join(lines, "\n") +} diff --git a/cli/common/flag/flag_bool.go b/cli/common/flag/flag_bool.go new file mode 100644 index 0000000000..2862c2fe40 --- /dev/null +++ b/cli/common/flag/flag_bool.go @@ -0,0 +1,77 @@ +package flag + +import ( + "os" + "strconv" + + "github.com/posener/complete" +) + +// -- BoolVar and boolValue. +type BoolVar struct { + Name string + Aliases []string + Usage string + Default bool + Hidden bool + EnvVar string + Target *bool + Completion complete.Predictor + SetHook func(val bool) +} + +func (f *Set) BoolVar(i *BoolVar) { + def := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if b, err := strconv.ParseBool(v); err == nil { + def = b + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: strconv.FormatBool(i.Default), + EnvVar: i.EnvVar, + Value: newBoolValue(i, def, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type boolValue struct { + v *BoolVar + hidden bool + target *bool +} + +func newBoolValue(v *BoolVar, def bool, target *bool, hidden bool) *boolValue { + *target = def + + return &boolValue{ + v: v, + hidden: hidden, + target: target, + } +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + + *b.target = v + + if b.v.SetHook != nil { + b.v.SetHook(v) + } + + return nil +} + +func (b *boolValue) Get() interface{} { return *b.target } +func (b *boolValue) String() string { return strconv.FormatBool(*b.target) } +func (b *boolValue) Example() string { return "" } +func (b *boolValue) Hidden() bool { return b.hidden } +func (b *boolValue) IsBoolFlag() bool { return true } diff --git a/cli/common/flag/flag_enum.go b/cli/common/flag/flag_enum.go new file mode 100644 index 0000000000..ac9ecaedc1 --- /dev/null +++ b/cli/common/flag/flag_enum.go @@ -0,0 +1,90 @@ +package flag + +import ( + "fmt" + "os" + "strings" + + "github.com/posener/complete" +) + +// -- EnumVar and enumValue. +type EnumVar struct { + Name string + Aliases []string + Usage string + Values []string + Default []string + Hidden bool + EnvVar string + Target *[]string + Completion complete.Predictor +} + +func (f *Set) EnumVar(i *EnumVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + parts := strings.Split(v, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + initial = parts + } + + def := "" + if i.Default != nil { + def = strings.Join(i.Default, ",") + } + + possible := strings.Join(i.Values, ", ") + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: strings.TrimRight(i.Usage, ". \t") + ". One possible value from: " + possible + ".", + Default: def, + EnvVar: i.EnvVar, + Value: newEnumValue(i, initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type enumValue struct { + ev *EnumVar + hidden bool + target *[]string +} + +func newEnumValue(ev *EnumVar, def []string, target *[]string, hidden bool) *enumValue { + *target = def + return &enumValue{ + ev: ev, + hidden: hidden, + target: target, + } +} + +func (s *enumValue) Set(vals string) error { + parts := strings.Split(vals, ",") + +parts: + for _, val := range parts { + val = strings.TrimSpace(val) + + for _, p := range s.ev.Values { + if p == val { + *s.target = append(*s.target, strings.TrimSpace(val)) + continue parts + } + } + + return fmt.Errorf("'%s' not valid. Must be one of: %s", val, strings.Join(s.ev.Values, ", ")) + } + + return nil +} + +func (s *enumValue) Get() interface{} { return *s.target } +func (s *enumValue) String() string { return strings.Join(*s.target, ",") } +func (s *enumValue) Example() string { return "string" } +func (s *enumValue) Hidden() bool { return s.hidden } diff --git a/cli/common/flag/flag_enum_single.go b/cli/common/flag/flag_enum_single.go new file mode 100644 index 0000000000..ddef52796b --- /dev/null +++ b/cli/common/flag/flag_enum_single.go @@ -0,0 +1,80 @@ +package flag + +import ( + "fmt" + "os" + "strings" + + "github.com/posener/complete" +) + +// -- EnumVar and enumValue. +type EnumSingleVar struct { + Name string + Aliases []string + Usage string + Values []string + Default string + Hidden bool + EnvVar string + Target *string + SetHook func(val string) + Completion complete.Predictor +} + +func (f *Set) EnumSingleVar(i *EnumSingleVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + initial = v + } + + def := i.Default + + possible := strings.Join(i.Values, ", ") + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: strings.TrimRight(i.Usage, ". \t") + ". One possible value from: " + possible + ".", + Default: def, + EnvVar: i.EnvVar, + Value: newEnumSingleValue(i, initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type enumSingleValue struct { + ev *EnumSingleVar + hidden bool + target *string +} + +func newEnumSingleValue(ev *EnumSingleVar, def string, target *string, hidden bool) *enumSingleValue { + *target = def + return &enumSingleValue{ + ev: ev, + hidden: hidden, + target: target, + } +} + +func (s *enumSingleValue) Set(val string) error { + for _, p := range s.ev.Values { + if p == val { + *s.target = val + + if s.ev.SetHook != nil { + s.ev.SetHook(val) + } + + return nil + } + } + + return fmt.Errorf("'%s' not valid. Must be one of: %s", val, strings.Join(s.ev.Values, ", ")) +} + +func (s *enumSingleValue) Get() interface{} { return *s.target } +func (s *enumSingleValue) String() string { return *s.target } +func (s *enumSingleValue) Example() string { return "string" } +func (s *enumSingleValue) Hidden() bool { return s.hidden } diff --git a/cli/common/flag/flag_float.go b/cli/common/flag/flag_float.go new file mode 100644 index 0000000000..1f7fcfad67 --- /dev/null +++ b/cli/common/flag/flag_float.go @@ -0,0 +1,72 @@ +package flag + +import ( + "os" + "strconv" + + "github.com/posener/complete" +) + +// -- Float64Var and float64Value. +type Float64Var struct { + Name string + Aliases []string + Usage string + Default float64 + Hidden bool + EnvVar string + Target *float64 + Completion complete.Predictor +} + +func (f *Set) Float64Var(i *Float64Var) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if i, err := strconv.ParseFloat(v, 64); err == nil { + initial = i + } + } + + def := "" + if i.Default != 0 { + def = strconv.FormatFloat(i.Default, 'e', -1, 64) + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newFloat64Value(initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type float64Value struct { + hidden bool + target *float64 +} + +func newFloat64Value(def float64, target *float64, hidden bool) *float64Value { + *target = def + return &float64Value{ + hidden: hidden, + target: target, + } +} + +func (f *float64Value) Set(s string) error { + v, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + + *f.target = v + return nil +} + +func (f *float64Value) Get() interface{} { return float64(*f.target) } +func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f.target), 'g', -1, 64) } +func (f *float64Value) Example() string { return "float" } +func (f *float64Value) Hidden() bool { return f.hidden } diff --git a/cli/common/flag/flag_int.go b/cli/common/flag/flag_int.go new file mode 100644 index 0000000000..419dabaa0e --- /dev/null +++ b/cli/common/flag/flag_int.go @@ -0,0 +1,296 @@ +package flag + +import ( + "os" + "strconv" + + "github.com/posener/complete" +) + +// -- IntVar and intValue. +type IntVar struct { + Name string + Aliases []string + Usage string + Default int + Hidden bool + EnvVar string + Target *int + Completion complete.Predictor + SetHook func(val int) +} + +func (f *Set) IntVar(i *IntVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + initial = int(i) + } + } + + def := "" + if i.Default != 0 { + def = strconv.FormatInt(int64(i.Default), 10) + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newIntValue(i, initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type intValue struct { + v *IntVar + hidden bool + target *int +} + +func newIntValue(v *IntVar, def int, target *int, hidden bool) *intValue { + *target = def + return &intValue{ + v: v, + hidden: hidden, + target: target, + } +} + +func (i *intValue) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + + *i.target = int(v) + + if i.v.SetHook != nil { + i.v.SetHook(int(v)) + } + + return nil +} + +func (i *intValue) Get() interface{} { return int(*i.target) } +func (i *intValue) String() string { return strconv.Itoa(int(*i.target)) } +func (i *intValue) Example() string { return "int" } +func (i *intValue) Hidden() bool { return i.hidden } + +// -- Int64Var and int64Value. +type Int64Var struct { + Name string + Aliases []string + Usage string + Default int64 + Hidden bool + EnvVar string + Target *int64 + Completion complete.Predictor + SetHook func(val int64) +} + +func (f *Set) Int64Var(i *Int64Var) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + initial = i + } + } + + def := "" + if i.Default != 0 { + def = strconv.FormatInt(int64(i.Default), 10) + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newInt64Value(i, initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type int64Value struct { + v *Int64Var + hidden bool + target *int64 +} + +func newInt64Value(v *Int64Var, def int64, target *int64, hidden bool) *int64Value { + *target = def + return &int64Value{ + v: v, + hidden: hidden, + target: target, + } +} + +func (i *int64Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + + *i.target = v + + if i.v.SetHook != nil { + i.v.SetHook(v) + } + + return nil +} + +func (i *int64Value) Get() interface{} { return int64(*i.target) } +func (i *int64Value) String() string { return strconv.FormatInt(int64(*i.target), 10) } +func (i *int64Value) Example() string { return "int" } +func (i *int64Value) Hidden() bool { return i.hidden } + +// -- UintVar && uintValue. +type UintVar struct { + Name string + Aliases []string + Usage string + Default uint + Hidden bool + EnvVar string + Target *uint + Completion complete.Predictor + SetHook func(val uint) +} + +func (f *Set) UintVar(i *UintVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if i, err := strconv.ParseUint(v, 0, 64); err == nil { + initial = uint(i) + } + } + + def := "" + if i.Default != 0 { + def = strconv.FormatUint(uint64(i.Default), 10) + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newUintValue(i, initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type uintValue struct { + v *UintVar + hidden bool + target *uint +} + +func newUintValue(v *UintVar, def uint, target *uint, hidden bool) *uintValue { + *target = def + return &uintValue{ + v: v, + hidden: hidden, + target: target, + } +} + +func (i *uintValue) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + if err != nil { + return err + } + + *i.target = uint(v) + + if i.v.SetHook != nil { + i.v.SetHook(uint(v)) + } + + return nil +} + +func (i *uintValue) Get() interface{} { return uint(*i.target) } +func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i.target), 10) } +func (i *uintValue) Example() string { return "uint" } +func (i *uintValue) Hidden() bool { return i.hidden } + +// -- Uint64Var and uint64Value. +type Uint64Var struct { + Name string + Aliases []string + Usage string + Default uint64 + Hidden bool + EnvVar string + Target *uint64 + Completion complete.Predictor + SetHook func(val uint64) +} + +func (f *Set) Uint64Var(i *Uint64Var) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if i, err := strconv.ParseUint(v, 0, 64); err == nil { + initial = i + } + } + + def := "" + if i.Default != 0 { + strconv.FormatUint(i.Default, 10) + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newUint64Value(i, initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type uint64Value struct { + v *Uint64Var + hidden bool + target *uint64 +} + +func newUint64Value(v *Uint64Var, def uint64, target *uint64, hidden bool) *uint64Value { + *target = def + return &uint64Value{ + v: v, + hidden: hidden, + target: target, + } +} + +func (i *uint64Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + if err != nil { + return err + } + + *i.target = v + + if i.v.SetHook != nil { + i.v.SetHook(v) + } + + return nil +} + +func (i *uint64Value) Get() interface{} { return uint64(*i.target) } +func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i.target), 10) } +func (i *uint64Value) Example() string { return "uint" } +func (i *uint64Value) Hidden() bool { return i.hidden } diff --git a/cli/common/flag/flag_string.go b/cli/common/flag/flag_string.go new file mode 100644 index 0000000000..b16a1be16d --- /dev/null +++ b/cli/common/flag/flag_string.go @@ -0,0 +1,72 @@ +package flag + +import ( + "os" + + "github.com/posener/complete" +) + +// -- StringVar and stringValue. +type StringVar struct { + Name string + Aliases []string + Usage string + Default string + Hidden bool + EnvVar string + Target *string + Completion complete.Predictor + SetHook func(val string) +} + +func (f *Set) StringVar(i *StringVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + initial = v + } + + def := "" + if i.Default != "" { + def = i.Default + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newStringValue(i, initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type stringValue struct { + v *StringVar + hidden bool + target *string +} + +func newStringValue(v *StringVar, def string, target *string, hidden bool) *stringValue { + *target = def + return &stringValue{ + v: v, + hidden: hidden, + target: target, + } +} + +func (s *stringValue) Set(val string) error { + *s.target = val + + if s.v.SetHook != nil { + s.v.SetHook(val) + } + + return nil +} + +func (s *stringValue) Get() interface{} { return *s.target } +func (s *stringValue) String() string { return *s.target } +func (s *stringValue) Example() string { return "string" } +func (s *stringValue) Hidden() bool { return s.hidden } diff --git a/cli/common/flag/flag_string_map.go b/cli/common/flag/flag_string_map.go new file mode 100644 index 0000000000..5314a66655 --- /dev/null +++ b/cli/common/flag/flag_string_map.go @@ -0,0 +1,83 @@ +package flag + +import ( + "fmt" + "sort" + "strings" + + "github.com/posener/complete" +) + +// -- StringMapVar and stringMapValue. +type StringMapVar struct { + Name string + Aliases []string + Usage string + Default map[string]string + Hidden bool + Target *map[string]string + Completion complete.Predictor +} + +func (f *Set) StringMapVar(i *StringMapVar) { + def := "" + if i.Default != nil { + def = mapToKV(i.Default) + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + Value: newStringMapValue(i.Default, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type stringMapValue struct { + hidden bool + target *map[string]string +} + +func newStringMapValue(def map[string]string, target *map[string]string, hidden bool) *stringMapValue { + *target = def + return &stringMapValue{ + hidden: hidden, + target: target, + } +} + +func (s *stringMapValue) Set(val string) error { + idx := strings.Index(val, "=") + if idx == -1 { + return fmt.Errorf("missing = in KV pair: %q", val) + } + + if *s.target == nil { + *s.target = make(map[string]string) + } + + k, v := val[0:idx], val[idx+1:] + (*s.target)[k] = v + return nil +} + +func (s *stringMapValue) Get() interface{} { return *s.target } +func (s *stringMapValue) String() string { return mapToKV(*s.target) } +func (s *stringMapValue) Example() string { return "key=value" } +func (s *stringMapValue) Hidden() bool { return s.hidden } + +func mapToKV(m map[string]string) string { + list := make([]string, 0, len(m)) + for k := range m { + list = append(list, k) + } + sort.Strings(list) + + for i, k := range list { + list[i] = k + "=" + m[k] + } + + return strings.Join(list, ",") +} diff --git a/cli/common/flag/flag_string_slice.go b/cli/common/flag/flag_string_slice.go new file mode 100644 index 0000000000..b567c64936 --- /dev/null +++ b/cli/common/flag/flag_string_slice.go @@ -0,0 +1,75 @@ +package flag + +import ( + "os" + "strings" + + "github.com/posener/complete" +) + +// -- StringSliceVar and stringSliceValue. +type StringSliceVar struct { + Name string + Aliases []string + Usage string + Default []string + Hidden bool + EnvVar string + Target *[]string + Completion complete.Predictor +} + +func (f *Set) StringSliceVar(i *StringSliceVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + parts := strings.Split(v, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + initial = parts + } + + def := "" + if i.Default != nil { + def = strings.Join(i.Default, ",") + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newStringSliceValue(initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type stringSliceValue struct { + hidden bool + target *[]string + set bool +} + +func newStringSliceValue(def []string, target *[]string, hidden bool) *stringSliceValue { + *target = def + return &stringSliceValue{ + hidden: hidden, + target: target, + } +} + +func (s *stringSliceValue) Set(val string) error { + if !s.set { + s.set = true + *s.target = nil + } + + *s.target = append(*s.target, strings.Split(strings.TrimSpace(val), ",")...) + return nil +} + +func (s *stringSliceValue) Get() interface{} { return *s.target } +func (s *stringSliceValue) String() string { return strings.Join(*s.target, ",") } +func (s *stringSliceValue) Example() string { return "string" } +func (s *stringSliceValue) Hidden() bool { return s.hidden } diff --git a/cli/common/flag/flag_string_slice_test.go b/cli/common/flag/flag_string_slice_test.go new file mode 100644 index 0000000000..5bea43386f --- /dev/null +++ b/cli/common/flag/flag_string_slice_test.go @@ -0,0 +1,38 @@ +package flag + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStringSlice(t *testing.T) { + require := require.New(t) + + var valA, valB []string + sets := NewSets() + { + set := sets.NewSet("A") + set.StringSliceVar(&StringSliceVar{ + Name: "a", + Target: &valA, + }) + } + + { + set := sets.NewSet("B") + set.StringSliceVar(&StringSliceVar{ + Name: "b", + Target: &valB, + }) + } + + err := sets.Parse([]string{ + "-b", "somevalueB", + "-a", "somevalueA,somevalueB", + }) + require.NoError(err) + + require.Equal([]string{"somevalueB"}, valB) + require.Equal([]string{"somevalueA", "somevalueB"}, valA) +} diff --git a/cli/common/flag/flag_time.go b/cli/common/flag/flag_time.go new file mode 100644 index 0000000000..45dc297f3d --- /dev/null +++ b/cli/common/flag/flag_time.go @@ -0,0 +1,81 @@ +package flag + +import ( + "os" + "strings" + "time" + + "github.com/posener/complete" +) + +// -- DurationVar and durationValue. +type DurationVar struct { + Name string + Aliases []string + Usage string + Default time.Duration + Hidden bool + EnvVar string + Target *time.Duration + Completion complete.Predictor +} + +func (f *Set) DurationVar(i *DurationVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if d, err := time.ParseDuration(appendDurationSuffix(v)); err == nil { + initial = d + } + } + + def := "" + if i.Default != 0 { + def = i.Default.String() + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newDurationValue(initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type durationValue struct { + hidden bool + target *time.Duration +} + +func newDurationValue(def time.Duration, target *time.Duration, hidden bool) *durationValue { + *target = def + return &durationValue{ + hidden: hidden, + target: target, + } +} + +func (d *durationValue) Set(s string) error { + v, err := time.ParseDuration(appendDurationSuffix(s)) + if err != nil { + return err + } + *d.target = v + return nil +} + +func (d *durationValue) Get() interface{} { return time.Duration(*d.target) } +func (d *durationValue) String() string { return (*d.target).String() } +func (d *durationValue) Example() string { return "duration" } +func (d *durationValue) Hidden() bool { return d.hidden } + +// appendDurationSuffix is used as a backwards-compat tool for assuming users +// meant "seconds" when they do not provide a suffixed duration value. +func appendDurationSuffix(s string) string { + if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "m") || strings.HasSuffix(s, "h") { + return s + } + return s + "s" +} diff --git a/cli/common/flag/flag_var.go b/cli/common/flag/flag_var.go new file mode 100644 index 0000000000..1b4114f16f --- /dev/null +++ b/cli/common/flag/flag_var.go @@ -0,0 +1,81 @@ +package flag + +import ( + "flag" + "fmt" + "strings" + + "github.com/posener/complete" +) + +// -- VarFlag. +type VarFlag struct { + Name string + Aliases []string + Usage string + Default string + EnvVar string + Value flag.Value + Completion complete.Predictor +} + +func (f *Set) VarFlag(i *VarFlag) { + f.vars = append(f.vars, i) + + // If the flag is marked as hidden, just add it to the set and return to + // avoid unnecessary computations here. We do not want to add completions or + // generate help output for hidden flags. + if v, ok := i.Value.(FlagVisibility); ok && v.Hidden() { + f.Var(i.Value, i.Name, "") + return + } + + // Calculate the full usage + usage := i.Usage + + if len(i.Aliases) > 0 { + sentence := make([]string, len(i.Aliases)) + for i, a := range i.Aliases { + sentence[i] = fmt.Sprintf(`"-%s"`, a) + } + + aliases := "" + switch len(sentence) { + case 0: + // impossible... + case 1: + aliases = sentence[0] + case 2: + aliases = sentence[0] + " and " + sentence[1] + default: + sentence[len(sentence)-1] = "and " + sentence[len(sentence)-1] + aliases = strings.Join(sentence, ", ") + } + + usage += fmt.Sprintf(" This is aliased as %s.", aliases) + } + + if i.Default != "" { + usage += fmt.Sprintf(" The default is %s.", i.Default) + } + + if i.EnvVar != "" { + usage += fmt.Sprintf(" This can also be specified via the %s "+ + "environment variable.", i.EnvVar) + } + + // Add aliases to the main set + for _, a := range i.Aliases { + f.unionSet.Var(i.Value, a, "") + } + + f.Var(i.Value, i.Name, usage) + f.completions["-"+i.Name] = i.Completion +} + +// Var is a lower-level API for adding something to the flags. It should be used +// wtih caution, since it bypasses all validation. Consider VarFlag instead. +func (f *Set) Var(value flag.Value, name, usage string) { + f.unionSet.Var(value, name, usage) + f.flagSet.Var(value, name, usage) +} diff --git a/cli/common/flag/set.go b/cli/common/flag/set.go new file mode 100644 index 0000000000..df44e7031c --- /dev/null +++ b/cli/common/flag/set.go @@ -0,0 +1,175 @@ +package flag + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/posener/complete" +) + +// Sets is a group of flag sets. +type Sets struct { + // unionSet is the set that is the union of all other sets. This + // has ALL flags defined on it and is the set that is parsed. But + // we maintain the other list of sets so that we can generate proper help. + unionSet *flag.FlagSet + + // flagSets is the list of sets that we have. We don't parse these + // directly but use them for help generation and autocompletion. + flagSets []*Set + + // completions is our set of autocompletion handlers. This is also + // the union of all available flags similar to unionSet. + completions complete.Flags +} + +// NewSets creates a new flag sets. +func NewSets() *Sets { + unionSet := flag.NewFlagSet("", flag.ContinueOnError) + + // Errors and usage are expected to be controlled externally by + // checking on the result of Parse. + unionSet.Usage = func() {} + unionSet.SetOutput(ioutil.Discard) + + return &Sets{ + unionSet: unionSet, + completions: complete.Flags{}, + } +} + +// NewSet creates a new single flag set. A set should be created for +// any grouping of flags, for example "Common Options", "Auth Options", etc. +func (f *Sets) NewSet(name string) *Set { + flagSet := NewSet(name) + + // The union and completions are pointers to our own values + flagSet.unionSet = f.unionSet + flagSet.completions = f.completions + + // Keep track of it for help generation + f.flagSets = append(f.flagSets, flagSet) + return flagSet +} + +// Completions returns the completions for this flag set. +func (f *Sets) Completions() complete.Flags { + return f.completions +} + +// Parse parses the given flags, returning any errors. +func (f *Sets) Parse(args []string) error { + return f.unionSet.Parse(args) +} + +// Parsed reports whether the command-line flags have been parsed. +func (f *Sets) Parsed() bool { + return f.unionSet.Parsed() +} + +// Args returns the remaining args after parsing. +func (f *Sets) Args() []string { + return f.unionSet.Args() +} + +// Visit visits the flags in lexicographical order, calling fn for each. It +// visits only those flags that have been set. +func (f *Sets) Visit(fn func(*flag.Flag)) { + f.unionSet.Visit(fn) +} + +// Help builds custom help for this command, grouping by flag set. +func (fs *Sets) Help() string { + var out bytes.Buffer + + for _, set := range fs.flagSets { + printFlagTitle(&out, set.name+":") + set.VisitAll(func(f *flag.Flag) { + // Skip any hidden flags + if v, ok := f.Value.(FlagVisibility); ok && v.Hidden() { + return + } + printFlagDetail(&out, f) + }) + } + + return strings.TrimRight(out.String(), "\n") +} + +// Help builds custom help for this command, grouping by flag set. +func (fs *Sets) VisitSets(fn func(name string, set *Set)) { + for _, set := range fs.flagSets { + fn(set.name, set) + } +} + +// Set is a grouped wrapper around a real flag set and a grouped flag set. +type Set struct { + name string + flagSet *flag.FlagSet + unionSet *flag.FlagSet + completions complete.Flags + + vars []*VarFlag +} + +// NewSet creates a new flag set. +func NewSet(name string) *Set { + return &Set{ + name: name, + flagSet: flag.NewFlagSet(name, flag.ContinueOnError), + } +} + +// Name returns the name of this flag set. +func (f *Set) Name() string { + return f.name +} + +func (f *Set) Visit(fn func(*flag.Flag)) { + f.flagSet.Visit(fn) +} + +func (f *Set) VisitAll(fn func(*flag.Flag)) { + f.flagSet.VisitAll(fn) +} + +func (f *Set) VisitVars(fn func(*VarFlag)) { + for _, v := range f.vars { + fn(v) + } +} + +// printFlagTitle prints a consistently-formatted title to the given writer. +func printFlagTitle(w io.Writer, s string) { + fmt.Fprintf(w, "%s\n\n", s) +} + +// printFlagDetail prints a single flag to the given writer. +func printFlagDetail(w io.Writer, f *flag.Flag) { + // Check if the flag is hidden - do not print any flag detail or help output + // if it is hidden. + if h, ok := f.Value.(FlagVisibility); ok && h.Hidden() { + return + } + + // Check for a detailed example + example := "" + if t, ok := f.Value.(FlagExample); ok { + example = t.Example() + } + + if example != "" { + fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example) + } else { + fmt.Fprintf(w, " -%s\n", f.Name) + } + + usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ") + indented := wrapAtLengthWithPadding(usage, 6) + fmt.Fprintf(w, "%s\n\n", indented) +} diff --git a/cli/common/flag/set_test.go b/cli/common/flag/set_test.go new file mode 100644 index 0000000000..9bb4ef4a7a --- /dev/null +++ b/cli/common/flag/set_test.go @@ -0,0 +1,35 @@ +package flag + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSets(t *testing.T) { + require := require.New(t) + + var valA, valB int + sets := NewSets() + { + set := sets.NewSet("A") + set.IntVar(&IntVar{ + Name: "a", + Target: &valA, + }) + } + + { + set := sets.NewSet("B") + set.IntVar(&IntVar{ + Name: "b", + Target: &valB, + }) + } + + err := sets.Parse([]string{"-b", "42", "-a", "21"}) + require.NoError(err) + + require.Equal(int(21), valA) + require.Equal(int(42), valB) +} diff --git a/cli/common/terminal/basic.go b/cli/common/terminal/basic.go new file mode 100644 index 0000000000..d06815b8f5 --- /dev/null +++ b/cli/common/terminal/basic.go @@ -0,0 +1,149 @@ +package terminal + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/bgentry/speakeasy" + "github.com/fatih/color" + "github.com/mattn/go-isatty" +) + +// basicUI. +type basicUI struct { + ctx context.Context +} + +func NewBasicUI(ctx context.Context) *basicUI { + return &basicUI{ + ctx: ctx, + } +} + +// Input implements UI. +func (ui *basicUI) Input(input *Input) (string, error) { + var buf bytes.Buffer + + // Write the prompt, add a space. + ui.Output(input.Prompt, WithStyle(input.Style), WithWriter(&buf)) + fmt.Fprint(color.Output, strings.TrimRight(buf.String(), "\r\n")) + fmt.Fprint(color.Output, " ") + + // Ask for input in a go-routine so that we can ignore it. + errCh := make(chan error, 1) + lineCh := make(chan string, 1) + go func() { + var line string + var err error + if input.Secret && isatty.IsTerminal(os.Stdin.Fd()) { + line, err = speakeasy.Ask("") + } else { + r := bufio.NewReader(os.Stdin) + line, err = r.ReadString('\n') + } + if err != nil { + errCh <- err + return + } + + lineCh <- strings.TrimRight(line, "\r\n") + }() + + select { + case err := <-errCh: + return "", err + case line := <-lineCh: + return line, nil + case <-ui.ctx.Done(): + // Print newline so that any further output starts properly + fmt.Fprintln(color.Output) + return "", ui.ctx.Err() + } +} + +// Interactive implements UI. +func (ui *basicUI) Interactive() bool { + return isatty.IsTerminal(os.Stdin.Fd()) +} + +// Output implements UI. +func (ui *basicUI) Output(msg string, raw ...interface{}) { + msg, style, w := Interpret(msg, raw...) + + switch style { + case HeaderStyle: + msg = colorHeader.Sprintf("\n==> %s", msg) + case ErrorStyle: + msg = colorError.Sprintf(" ! %s", msg) + case ErrorBoldStyle: + msg = colorErrorBold.Sprintf(" ! %s", msg) + case WarningStyle: + msg = colorWarning.Sprintf(" * %s", msg) + case WarningBoldStyle: + msg = colorWarningBold.Sprintf(" * %s", msg) + case SuccessStyle: + msg = colorSuccess.Sprintf(" ✓ %s", msg) + case SuccessBoldStyle: + msg = colorSuccessBold.Sprintf(" ✓ %s", msg) + case LibraryStyle: + msg = colorLibrary.Sprintf(" --> %s", msg) + case DiffUnchangedStyle: + msg = colorDiffUnchanged.Sprintf(" %s", msg) + case DiffAddedStyle: + msg = colorDiffAdded.Sprintf(" %s", msg) + case DiffRemovedStyle: + msg = colorDiffRemoved.Sprintf(" %s", msg) + case InfoStyle: + lines := strings.Split(msg, "\n") + for i, line := range lines { + lines[i] = colorInfo.Sprintf(" %s", line) + } + + msg = strings.Join(lines, "\n") + } + + // Write it + fmt.Fprintln(w, msg) +} + +// NamedValues implements UI. +func (ui *basicUI) NamedValues(rows []NamedValue, opts ...Option) { + cfg := &config{Writer: color.Output} + for _, opt := range opts { + opt(cfg) + } + + var buf bytes.Buffer + tr := tabwriter.NewWriter(&buf, 1, 8, 0, ' ', tabwriter.AlignRight) + for _, row := range rows { + switch v := row.Value.(type) { + case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64: + fmt.Fprintf(tr, " %s: \t%d\n", row.Name, row.Value) + case float32, float64: + fmt.Fprintf(tr, " %s: \t%f\n", row.Name, row.Value) + case bool: + fmt.Fprintf(tr, " %s: \t%v\n", row.Name, row.Value) + case string: + if v == "" { + continue + } + fmt.Fprintf(tr, " %s: \t%s\n", row.Name, row.Value) + default: + fmt.Fprintf(tr, " %s: \t%s\n", row.Name, row.Value) + } + } + + _ = tr.Flush() + _, _ = colorInfo.Fprintln(cfg.Writer, buf.String()) +} + +// OutputWriters implements UI. +func (ui *basicUI) OutputWriters() (io.Writer, io.Writer, error) { + return os.Stdout, os.Stderr, nil +} diff --git a/cli/common/terminal/doc.go b/cli/common/terminal/doc.go new file mode 100644 index 0000000000..2935fa9db5 --- /dev/null +++ b/cli/common/terminal/doc.go @@ -0,0 +1,10 @@ +// Package terminal is a modified version of +// https://github.com/hashicorp/waypoint-plugin-sdk/tree/74d9328929293551499078da388b8d057f3b2341/terminal. +// +// This terminal package only contains the basic UI implementation and excludes the glint UI and noninteractive UI +// implementations as they do not yet have an Input implementation which we leverage in commands. +// +// This terminal package does not contain the Status, Step, and StepGroup interface and implementation from the original +// terminal package since using the spinner package does not allow streaming outputs of a given Step or Status. Instead, +// we only use the UI interface to standardize Input and Output formatting. +package terminal diff --git a/cli/common/terminal/table.go b/cli/common/terminal/table.go new file mode 100644 index 0000000000..c8e8c2c67b --- /dev/null +++ b/cli/common/terminal/table.go @@ -0,0 +1,99 @@ +package terminal + +import ( + "github.com/fatih/color" + "github.com/olekukonko/tablewriter" +) + +// Passed to UI.Table to provide a nicely formatted table. +type Table struct { + Headers []string + Rows [][]TableEntry +} + +// Table creates a new Table structure that can be used with UI.Table. +func NewTable(headers ...string) *Table { + return &Table{ + Headers: headers, + } +} + +// TableEntry is a single entry for a table. +type TableEntry struct { + Value string + Color string +} + +// Rich adds a row to the table. +func (t *Table) Rich(cols []string, colors []string) { + var row []TableEntry + + for i, col := range cols { + if i < len(colors) { + row = append(row, TableEntry{Value: col, Color: colors[i]}) + } else { + row = append(row, TableEntry{Value: col}) + } + } + + t.Rows = append(t.Rows, row) +} + +// Table implements UI. +func (u *basicUI) Table(tbl *Table, opts ...Option) { + // Build our config and set our options + cfg := &config{Writer: color.Output} + for _, opt := range opts { + opt(cfg) + } + + table := tablewriter.NewWriter(cfg.Writer) + + table.SetHeader(tbl.Headers) + table.SetBorder(false) + table.SetAutoWrapText(false) + + if cfg.Style == "Simple" { + // Format the table without borders, simple output + + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + } + + for _, row := range tbl.Rows { + colors := make([]tablewriter.Colors, len(row)) + entries := make([]string, len(row)) + + for i, ent := range row { + entries[i] = ent.Value + + color, ok := colorMapping[ent.Color] + if ok { + colors[i] = tablewriter.Colors{color} + } + } + + table.Rich(entries, colors) + } + + table.Render() +} + +const ( + Yellow = "yellow" + Green = "green" + Red = "red" +) + +var colorMapping = map[string]int{ + Green: tablewriter.FgGreenColor, + Yellow: tablewriter.FgYellowColor, + Red: tablewriter.FgRedColor, +} diff --git a/cli/common/terminal/ui.go b/cli/common/terminal/ui.go new file mode 100644 index 0000000000..a9baa7aa87 --- /dev/null +++ b/cli/common/terminal/ui.go @@ -0,0 +1,210 @@ +package terminal + +import ( + "errors" + "fmt" + "io" + + "github.com/fatih/color" +) + +// ErrNonInteractive is returned when Input is called on a non-Interactive UI. +var ErrNonInteractive = errors.New("noninteractive UI doesn't support this operation") + +// Passed to UI.NamedValues to provide a nicely formatted key: value output. +type NamedValue struct { + Name string + Value interface{} +} + +// UI is the primary interface for interacting with a user via the CLI. +// +// Some of the methods on this interface return values that have a lifetime +// such as Status and StepGroup. While these are still active (haven't called +// the close or equivalent method on these values), no other method on the +// UI should be called. +type UI interface { + // Input asks the user for input. This will immediately return an error + // if the UI doesn't support interaction. You can test for interaction + // ahead of time with Interactive(). + Input(*Input) (string, error) + + // Interactive returns true if this prompt supports user interaction. + // If this is false, Input will always error. + Interactive() bool + + // Output outputs a message directly to the terminal. The remaining + // arguments should be interpolations for the format string. After the + // interpolations you may add Options. + Output(string, ...interface{}) + + // Output data as a table of data. Each entry is a row which will be output + // with the columns lined up nicely. + NamedValues([]NamedValue, ...Option) + + // OutputWriters returns stdout and stderr writers. These are usually + // but not always TTYs. This is useful for subprocesses, network requests, + // etc. Note that writing to these is not thread-safe by default so + // you must take care that there is only ever one writer. + OutputWriters() (stdout, stderr io.Writer, err error) + + // Table outputs the information formatted into a Table structure. + Table(*Table, ...Option) +} + +// Input is the configuration for an input. +type Input struct { + // Prompt is a single-line prompt to give the user such as "Continue?" + // The user will input their answer after this prompt. + Prompt string + + // Style is the style to apply to the input. If this is blank, + // the output won't be colorized in any way. + Style string + + // True if this input is a secret. The input will be masked. + Secret bool +} + +// Interpret decomposes the msg and arguments into the message, style, and writer. +func Interpret(msg string, raw ...interface{}) (string, string, io.Writer) { + // Build our args and options + var args []interface{} + var opts []Option + for _, r := range raw { + if opt, ok := r.(Option); ok { + opts = append(opts, opt) + } else { + args = append(args, r) + } + } + + // Build our message + msg = fmt.Sprintf(msg, args...) + + // Build our config and set our options + cfg := &config{Writer: color.Output} + for _, opt := range opts { + opt(cfg) + } + + return msg, cfg.Style, cfg.Writer +} + +const ( + HeaderStyle = "header" + ErrorStyle = "error" + ErrorBoldStyle = "error-bold" + WarningStyle = "warning" + WarningBoldStyle = "warning-bold" + InfoStyle = "info" + LibraryStyle = "library" + SuccessStyle = "success" + SuccessBoldStyle = "success-bold" + DiffUnchangedStyle = "diff-unchanged" + DiffAddedStyle = "diff-added" + DiffRemovedStyle = "diff-removed" +) + +type config struct { + // Writer is where the message will be written to. + Writer io.Writer + + // The style the output should take on + Style string +} + +// Option controls output styling. +type Option func(*config) + +// WithHeaderStyle styles the output like a header denoting a new section +// of execution. This should only be used with single-line output. Multi-line +// output will not look correct. +func WithHeaderStyle() Option { + return func(c *config) { + c.Style = HeaderStyle + } +} + +// WithInfoStyle styles the output like it's formatted information. +func WithInfoStyle() Option { + return func(c *config) { + c.Style = InfoStyle + } +} + +// WithErrorStyle styles the output as an error message. +func WithErrorStyle() Option { + return func(c *config) { + c.Style = ErrorStyle + } +} + +// WithWarningStyle styles the output as an warning message. +func WithWarningStyle() Option { + return func(c *config) { + c.Style = WarningStyle + } +} + +// WithSuccessStyle styles the output as a success message. +func WithSuccessStyle() Option { + return func(c *config) { + c.Style = SuccessStyle + } +} + +// WithLibraryStyle styles the output with an arrow pointing to a section. +func WithLibraryStyle() Option { + return func(c *config) { + c.Style = LibraryStyle + } +} + +// WithDiffUnchangedStyle colors the diff style in white. +func WithDiffUnchangedStyle() Option { + return func(c *config) { + c.Style = DiffUnchangedStyle + } +} + +// WithDiffAddedStyle colors the output in green. +func WithDiffAddedStyle() Option { + return func(c *config) { + c.Style = DiffAddedStyle + } +} + +// WithDiffRemovedStyle colors the output in red. +func WithDiffRemovedStyle() Option { + return func(c *config) { + c.Style = DiffRemovedStyle + } +} + +// WithStyle allows for setting a style by passing a string. +func WithStyle(style string) Option { + return func(c *config) { + c.Style = style + } +} + +// WithWriter specifies the writer for the output. +func WithWriter(w io.Writer) Option { + return func(c *config) { c.Writer = w } +} + +var ( + colorHeader = color.New(color.Bold) + colorInfo = color.New() + colorError = color.New(color.FgRed) + colorErrorBold = color.New(color.FgRed, color.Bold) + colorLibrary = color.New(color.FgCyan) + colorSuccess = color.New(color.FgGreen) + colorSuccessBold = color.New(color.FgGreen, color.Bold) + colorWarning = color.New(color.FgYellow) + colorWarningBold = color.New(color.FgYellow, color.Bold) + colorDiffUnchanged = color.New() + colorDiffAdded = color.New(color.FgGreen) + colorDiffRemoved = color.New(color.FgRed) +) diff --git a/cli/common/usage.go b/cli/common/usage.go new file mode 100644 index 0000000000..b4b4cdf780 --- /dev/null +++ b/cli/common/usage.go @@ -0,0 +1,86 @@ +package common + +import ( + "bytes" + "flag" + "fmt" + "io" + "strings" + + "github.com/kr/text" +) + +// Taken from https://github.com/hashicorp/consul/blob/b5b9c8d953cd3c79c6b795946839f4cf5012f507/command/flags/usage.go +// This was done so we don't depend on internal Consul implementation. + +// Usage returns a usage string nicely formatted. +func Usage(txt string, flags *flag.FlagSet) string { + u := &Usager{ + Usage: txt, + Flags: flags, + } + return u.String() +} + +type Usager struct { + Usage string + Flags *flag.FlagSet +} + +func (u *Usager) String() string { + out := new(bytes.Buffer) + out.WriteString(strings.TrimSpace(u.Usage)) + out.WriteString("\n") + out.WriteString("\n") + + if u.Flags != nil { + var cmdFlags *flag.FlagSet + u.Flags.VisitAll(func(f *flag.Flag) { + if cmdFlags == nil { + cmdFlags = flag.NewFlagSet("", flag.ContinueOnError) + } + cmdFlags.Var(f.Value, f.Name, f.Usage) + }) + + if cmdFlags != nil { + printTitle(out, "Command Options") + cmdFlags.VisitAll(func(f *flag.Flag) { + printFlag(out, f) + }) + } + } + + return strings.TrimRight(out.String(), "\n") +} + +// printTitle prints a consistently-formatted title to the given writer. +func printTitle(w io.Writer, s string) { + fmt.Fprintf(w, "%s\n\n", s) +} + +// printFlag prints a single flag to the given writer. +func printFlag(w io.Writer, f *flag.Flag) { + example, _ := flag.UnquoteUsage(f) + if example != "" { + fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example) + } else { + fmt.Fprintf(w, " -%s\n", f.Name) + } + + indented := wrapAtLength(f.Usage, 5) + fmt.Fprintf(w, "%s\n\n", indented) +} + +// maxLineLength is the maximum width of any line. +const maxLineLength int = 72 + +// wrapAtLength wraps the given text at the maxLineLength, taking into account +// any provided left padding. +func wrapAtLength(s string, pad int) string { + wrapped := text.Wrap(s, maxLineLength-pad) + lines := strings.Split(wrapped, "\n") + for i, line := range lines { + lines[i] = strings.Repeat(" ", pad) + line + } + return strings.Join(lines, "\n") +} diff --git a/cli/common/utils.go b/cli/common/utils.go new file mode 100644 index 0000000000..3d7df71433 --- /dev/null +++ b/cli/common/utils.go @@ -0,0 +1,108 @@ +package common + +import ( + "errors" + "fmt" + "os" + "strings" + + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" +) + +const ( + DefaultReleaseName = "consul" + DefaultReleaseNamespace = "consul" + TopLevelChartDirName = "consul" + + // CLILabelKey and CLILabelValue are added to each secret on creation so the CLI knows + // which key to delete on an uninstall. + CLILabelKey = "managed-by" + CLILabelValue = "consul-k8s" +) + +// Abort returns true if the raw input string is not equal to "y" or "yes". +func Abort(raw string) bool { + confirmation := strings.TrimSuffix(raw, "\n") + return !(strings.ToLower(confirmation) == "y" || strings.ToLower(confirmation) == "yes") +} + +// CheckForInstallations uses the helm Go SDK to find helm releases in all namespaces where the chart name is +// "consul", and returns the release name and namespace if found, or an error if not found. +func CheckForInstallations(settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (string, string, error) { + // Need a specific action config to call helm list, where namespace is NOT specified. + listConfig := new(action.Configuration) + if err := listConfig.Init(settings.RESTClientGetter(), "", + os.Getenv("HELM_DRIVER"), uiLogger); err != nil { + return "", "", fmt.Errorf("couldn't initialize helm config: %s", err) + } + + lister := action.NewList(listConfig) + lister.AllNamespaces = true + lister.StateMask = action.ListAll + res, err := lister.Run() + if err != nil { + return "", "", fmt.Errorf("couldn't check for installations: %s", err) + } + + for _, rel := range res { + if rel.Chart.Metadata.Name == "consul" { + return rel.Name, rel.Namespace, nil + } + } + return "", "", errors.New("couldn't find consul installation") +} + +// MergeMaps merges two maps giving b precedent. +// @source: https://github.com/helm/helm/blob/main/pkg/cli/values/options.go +func MergeMaps(a, b map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(a)) + for k, v := range a { + out[k] = v + } + for k, v := range b { + if v, ok := v.(map[string]interface{}); ok { + if bv, ok := out[k]; ok { + if bv, ok := bv.(map[string]interface{}); ok { + out[k] = MergeMaps(bv, v) + continue + } + } + } + out[k] = v + } + return out +} + +func CloseWithError(c *BaseCommand) { + if err := c.Close(); err != nil { + c.Log.Error(err.Error()) + os.Exit(1) + } +} + +// IsValidLabel checks if a given label conforms to RFC 1123 https://datatracker.ietf.org/doc/html/rfc1123. +// This standard requires that the label begins and ends with an alphanumeric character, does not exceed 63 characters, +// and contains only alphanumeric characters and '-'. +func IsValidLabel(label string) bool { + if len(label) > 63 || len(label) == 0 { + return false + } + + for i, c := range label { + isAlphaNumeric := c >= '0' && c <= '9' || c >= 'a' && c <= 'z' + isTerminal := i == 0 || i == len(label)-1 + + // First and last character must be alphanumeric. + if isTerminal && !isAlphaNumeric { + return false + } + + // All other characters must be alphanumeric or '-'. + if !isAlphaNumeric && c != '-' { + return false + } + } + + return true +} diff --git a/cli/common/utils_test.go b/cli/common/utils_test.go new file mode 100644 index 0000000000..c15397e4a3 --- /dev/null +++ b/cli/common/utils_test.go @@ -0,0 +1,67 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMergeMaps(t *testing.T) { + cases := map[string]struct { + a map[string]interface{} + b map[string]interface{} + expected map[string]interface{} + }{ + "a is empty": { + a: map[string]interface{}{}, + b: map[string]interface{}{"foo": "bar"}, + expected: map[string]interface{}{"foo": "bar"}, + }, + "b is empty": { + a: map[string]interface{}{"foo": "bar"}, + b: map[string]interface{}{}, + expected: map[string]interface{}{"foo": "bar"}, + }, + "b overrides a": { + a: map[string]interface{}{"foo": "bar"}, + b: map[string]interface{}{"foo": "baz"}, + expected: map[string]interface{}{"foo": "baz"}, + }, + "b partially overrides a": { + a: map[string]interface{}{"foo": "bar", "baz": "qux"}, + b: map[string]interface{}{"foo": "baz"}, + expected: map[string]interface{}{"foo": "baz", "baz": "qux"}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + actual := MergeMaps(tc.a, tc.b) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestIsValidLabel(t *testing.T) { + cases := []struct { + name string + label string + expected bool + }{ + {"Valid label", "such-a-good-label", true}, + {"Valid label with leading numbers", "123-such-a-good-label", true}, + {"Invalid label empty", "", false}, + {"Invalid label contains capital letters", "Peppertrout", false}, + {"Invalid label contains underscores", "this_is_not_python", false}, + {"Invalid label too long", "a-very-very-very-long-label-that-is-more-than-63-characters-long", false}, + {"Invalid label starts with a dash", "-invalid-label", false}, + {"Invalid label ends with a dash", "invalid-label-", false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual := IsValidLabel(tc.label) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/cli/config/presets.go b/cli/config/presets.go new file mode 100644 index 0000000000..06b91ce8ce --- /dev/null +++ b/cli/config/presets.go @@ -0,0 +1,71 @@ +package config + +import "sigs.k8s.io/yaml" + +const ( + PresetDemo = "demo" + PresetSecure = "secure" +) + +// Presets is a map of pre-configured helm values. +var Presets = map[string]interface{}{ + PresetDemo: Convert(demo), + PresetSecure: Convert(secure), +} + +// demo is a preset of common values for setting up Consul. +const demo = ` +global: + name: consul + metrics: + enabled: true + enableAgentMetrics: true +connectInject: + enabled: true + metrics: + defaultEnabled: true + defaultEnableMerging: true + enableGatewayMetrics: true +server: + replicas: 1 +controller: + enabled: true +ui: + enabled: true + service: + enabled: true +prometheus: + enabled: true +` + +// secure is a preset of common values for setting up Consul in a secure manner. +const secure = ` +global: + name: consul + gossipEncryption: + autoGenerate: true + tls: + enabled: true + enableAutoEncrypt: true + acls: + manageSystemACLs: true +server: + replicas: 1 +connectInject: + enabled: true +controller: + enabled: true +` + +// GlobalNameConsul is used to set the global name of an install to consul. +const GlobalNameConsul = ` +global: + name: consul +` + +// convert is a helper function that converts a YAML string to a map. +func Convert(s string) map[string]interface{} { + var m map[string]interface{} + _ = yaml.Unmarshal([]byte(s), &m) + return m +} diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 0000000000..fa8cdf886a --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,166 @@ +module github.com/hashicorp/consul-k8s/cli + +go 1.17 + +require ( + github.com/bgentry/speakeasy v0.1.0 + github.com/cenkalti/backoff v2.2.1+incompatible + github.com/fatih/color v1.9.0 + github.com/google/go-cmp v0.5.5 + github.com/hashicorp/consul-k8s/charts v0.0.0-00010101000000-000000000000 + github.com/hashicorp/go-hclog v0.16.2 + github.com/kr/text v0.2.0 + github.com/mattn/go-isatty v0.0.12 + github.com/mitchellh/cli v1.1.2 + github.com/olekukonko/tablewriter v0.0.4 + github.com/posener/complete v1.1.1 + github.com/stretchr/testify v1.7.0 + helm.sh/helm/v3 v3.6.1 + k8s.io/api v0.22.2 + k8s.io/apimachinery v0.22.2 + k8s.io/cli-runtime v0.21.0 + k8s.io/client-go v0.22.2 + sigs.k8s.io/yaml v1.2.0 +) + +require ( + cloud.google.com/go v0.54.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.18 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect + github.com/Masterminds/sprig/v3 v3.2.2 // indirect + github.com/Masterminds/squirrel v1.5.0 // indirect + github.com/Microsoft/go-winio v0.4.16 // indirect + github.com/Microsoft/hcsshim v0.8.14 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect + github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 // indirect + github.com/containerd/containerd v1.4.4 // indirect + github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7 // indirect + github.com/cyphar/filepath-securejoin v0.2.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deislabs/oras v0.11.1 // indirect + github.com/docker/cli v20.10.5+incompatible // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible // indirect + github.com/docker/docker-credential-helpers v0.6.3 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/evanphx/json-patch v4.11.0+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect + github.com/go-errors/errors v1.0.1 // indirect + github.com/go-logr/logr v0.4.0 // indirect + github.com/go-openapi/jsonpointer v0.19.3 // indirect + github.com/go-openapi/jsonreference v0.19.3 // indirect + github.com/go-openapi/spec v0.19.5 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/googleapis/gnostic v0.5.5 // indirect + github.com/gorilla/mux v1.7.3 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmoiron/sqlx v1.3.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-runewidth v0.0.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mitchellh/copystructure v1.1.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.1 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/opencontainers/runc v0.1.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.11.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.26.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 // indirect + github.com/russross/blackfriday v1.5.2 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/cobra v1.1.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect + go.opencensus.io v0.22.3 // indirect + go.starlark.net v0.0.0-20200707032745-474f21a9602d // indirect + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect + golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect + golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + google.golang.org/appengine v1.6.5 // indirect + google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect + google.golang.org/grpc v1.33.1 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/gorp.v1 v1.7.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/apiextensions-apiserver v0.21.0 // indirect + k8s.io/apiserver v0.21.0 // indirect + k8s.io/component-base v0.21.0 // indirect + k8s.io/klog/v2 v2.9.0 // indirect + k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect + k8s.io/kubectl v0.21.0 // indirect + k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect + rsc.io/letsencrypt v0.0.3 // indirect + sigs.k8s.io/kustomize/api v0.8.5 // indirect + sigs.k8s.io/kustomize/kyaml v0.10.15 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect +) + +// This replace directive is to avoid having to manually bump the version of the charts module upon changes to the Helm +// chart. When the CLI compiles, all changes to the local charts directory are picked up automatically. This directive +// works because of the monorepo setup, where the charts module and CLI module are in the same repository. Otherwise, +// this won't work. +replace github.com/hashicorp/consul-k8s/charts => ../charts diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 0000000000..d6ada483c0 --- /dev/null +++ b/cli/go.sum @@ -0,0 +1,1250 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +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= +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/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= +github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +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/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +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= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +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/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +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= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.4 h1:rtRG4N6Ct7GNssATwgpvMGfnjnwfjnu/Zs9W3Ikzq+M= +github.com/containerd/containerd v1.4.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7 h1:6ejg6Lkk8dskcM7wQ28gONkukbQkM4qpj4RnYbpFzrI= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/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= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/deislabs/oras v0.11.1 h1:oo2J/3vXdcti8cjFi8ghMOkx0OacONxHC8dhJ17NdJ0= +github.com/deislabs/oras v0.11.1/go.mod h1:39lCtf8Q6WDC7ul9cnyWXONNzKvabEKk+AX+L0ImnQk= +github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v20.10.5+incompatible h1:bjflayQbWg+xOkF2WPEAOi4Y7zWhR7ptoPhV/VqLVDE= +github.com/docker/cli v20.10.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8= +github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 h1:yWHOI+vFjEsAakUTSrtqc/SAHrhSkmn48pqjidZX3QA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= +github.com/evanphx/json-patch v4.11.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/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+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/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= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-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/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/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +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= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= +github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +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/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= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +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/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.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/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +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= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/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/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= +github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/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/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4= +github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +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/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/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/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk= +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= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +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= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +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/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +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/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/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= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.starlark.net v0.0.0-20200707032745-474f21a9602d h1:uFqwFYlX7d5ZSp+IqhXxct0SybXrTzEBDvb2CkEhPBs= +go.starlark.net v0.0.0-20200707032745-474f21a9602d/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU= +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/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +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/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/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-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-20190617133340-57b3e21c3d56/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/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/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/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/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/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= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-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/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190321052220-f7bb7a8bee54/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/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= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +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/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= +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.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= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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.6.1 h1:TQ6q4pAatXr7qh2fbLcb0oNd0I3J7kv26oo5cExKTtc= +helm.sh/helm/v3 v3.6.1/go.mod h1:mIIus8EOqj+obtycw3sidsR4ORr2aFDmXMSI3k+oeVY= +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= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +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.21.0/go.mod h1:+YbrhBBGgsxbF6o6Kj4KJPJnBmAKuXDeS3E18bgHNVU= +k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= +k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= +k8s.io/apiextensions-apiserver v0.21.0 h1:Nd4uBuweg6ImzbxkC1W7xUNZcCV/8Vt10iTdTIVF3hw= +k8s.io/apiextensions-apiserver v0.21.0/go.mod h1:gsQGNtGkc/YoDG9loKI0V+oLZM4ljRPjc/sql5tmvzc= +k8s.io/apimachinery v0.21.0/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= +k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= +k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apiserver v0.21.0 h1:1hWMfsz+cXxB77k6/y0XxWxwl6l9OF26PC9QneUVn1Q= +k8s.io/apiserver v0.21.0/go.mod h1:w2YSn4/WIwYuxG5zJmcqtRdtqgW/J2JRgFAqps3bBpg= +k8s.io/cli-runtime v0.21.0 h1:/V2Kkxtf6x5NI2z+Sd/mIrq4FQyQ8jzZAUD6N5RnN7Y= +k8s.io/cli-runtime v0.21.0/go.mod h1:XoaHP93mGPF37MkLbjGVYqg3S1MnsFdKtiA/RZzzxOo= +k8s.io/client-go v0.21.0/go.mod h1:nNBytTF9qPFDEhoqgEPaarobC8QPae13bElIVHzIglA= +k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= +k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= +k8s.io/code-generator v0.21.0/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= +k8s.io/component-base v0.21.0 h1:tLLGp4BBjQaCpS/KiuWh7m2xqvAdsxLm4ATxHSe5Zpg= +k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= +k8s.io/component-helpers v0.21.0/go.mod h1:tezqefP7lxfvJyR+0a+6QtVrkZ/wIkyMLK4WcQ3Cj8U= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kubectl v0.21.0 h1:WZXlnG/yjcE4LWO2g6ULjFxtzK6H1TKzsfaBFuVIhNg= +k8s.io/kubectl v0.21.0/go.mod h1:EU37NukZRXn1TpAkMUoy8Z/B2u6wjHDS4aInsDzVvks= +k8s.io/metrics v0.21.0/go.mod h1:L3Ji9EGPP1YBbfm9sPfEXSpnj8i24bfQbAFAsW0NueQ= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/letsencrypt v0.0.3 h1:H7xDfhkaFFSYEJlKeq38RwX2jYcnTeHuDQyT+mMNMwM= +rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/kustomize/api v0.8.5 h1:bfCXGXDAbFbb/Jv5AhMj2BB8a5VAJuuQ5/KU69WtDjQ= +sigs.k8s.io/kustomize/api v0.8.5/go.mod h1:M377apnKT5ZHJS++6H4rQoCHmWtt6qTpp3mbe7p6OLY= +sigs.k8s.io/kustomize/cmd/config v0.9.7/go.mod h1:MvXCpHs77cfyxRmCNUQjIqCmZyYsbn5PyQpWiq44nW0= +sigs.k8s.io/kustomize/kustomize/v4 v4.0.5/go.mod h1:C7rYla7sI8EnxHE/xEhRBSHMNfcL91fx0uKmUlUhrBk= +sigs.k8s.io/kustomize/kyaml v0.10.15 h1:dSLgG78KyaxN4HylPXdK+7zB3k7sW6q3IcCmcfKA+aI= +sigs.k8s.io/kustomize/kyaml v0.10.15/go.mod h1:mlQFagmkm1P+W4lZJbJ/yaxMd8PqMRSC4cPcfUVt5Hg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +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= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/cli/helm/action.go b/cli/helm/action.go new file mode 100644 index 0000000000..c4d9de9650 --- /dev/null +++ b/cli/helm/action.go @@ -0,0 +1,24 @@ +package helm + +import ( + "fmt" + "os" + + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// InitActionConfig initializes a Helm Go SDK action configuration. This function currently uses a hack to override the +// namespace field that gets set in the K8s client set up by the SDK. +func InitActionConfig(actionConfig *action.Configuration, namespace string, settings *helmCLI.EnvSettings, logger action.DebugLog) (*action.Configuration, error) { + getter := settings.RESTClientGetter() + configFlags := getter.(*genericclioptions.ConfigFlags) + configFlags.Namespace = &namespace + err := actionConfig.Init(settings.RESTClientGetter(), namespace, + os.Getenv("HELM_DRIVER"), logger) + if err != nil { + return nil, fmt.Errorf("error setting up helm action configuration to find existing installations: %s", err) + } + return actionConfig, nil +} diff --git a/cli/helm/chart.go b/cli/helm/chart.go new file mode 100644 index 0000000000..3558e688ca --- /dev/null +++ b/cli/helm/chart.go @@ -0,0 +1,98 @@ +package helm + +import ( + "embed" + "path/filepath" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + helmCLI "helm.sh/helm/v3/pkg/cli" +) + +const ( + chartFileName = "Chart.yaml" + valuesFileName = "values.yaml" + templatesDirName = "templates" +) + +// LoadChart will attempt to load a chart from the embedded file system. +func LoadChart(chart embed.FS, chartDirName string) (*chart.Chart, error) { + chartFiles, err := ReadChartFiles(chart, chartDirName) + if err != nil { + return nil, err + } + + return loader.LoadFiles(chartFiles) +} + +// ReadChartFiles reads the chart files from the embedded file system, and loads their contents into +// []*loader.BufferedFile. This is a format that the Helm Go SDK functions can read from to create a chart to install +// from. The names of these files are important, as there are case statements in the Helm Go SDK looking for files named +// "Chart.yaml" or "templates/.yaml", which is why even though the embedded file system has them named +// "consul/Chart.yaml" we have to strip the "consul" prefix out, which is done by the call to the helper method readFile. +func ReadChartFiles(chart embed.FS, chartDirName string) ([]*loader.BufferedFile, error) { + var chartFiles []*loader.BufferedFile + + // Load Chart.yaml and values.yaml first. + for _, f := range []string{chartFileName, valuesFileName} { + file, err := readFile(chart, filepath.Join(chartDirName, f), chartDirName) + if err != nil { + return nil, err + } + chartFiles = append(chartFiles, file) + } + + // Now load everything under templates/. + dirs, err := chart.ReadDir(filepath.Join(chartDirName, templatesDirName)) + if err != nil { + return nil, err + } + for _, f := range dirs { + if f.IsDir() { + // We only need to include files in the templates directory. + continue + } + + file, err := readFile(chart, filepath.Join(chartDirName, templatesDirName, f.Name()), chartDirName) + if err != nil { + return nil, err + } + chartFiles = append(chartFiles, file) + } + + return chartFiles, nil +} + +// FetchChartValues will attempt to fetch the values from the currently installed Helm chart. +func FetchChartValues(namespace string, settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (map[string]interface{}, error) { + cfg := new(action.Configuration) + cfg, err := InitActionConfig(cfg, namespace, settings, uiLogger) + if err != nil { + return nil, err + } + + status := action.NewStatus(cfg) + release, err := status.Run(namespace) + if err != nil { + return nil, err + } + + return release.Config, nil +} + +func readFile(chart embed.FS, f string, pathPrefix string) (*loader.BufferedFile, error) { + bytes, err := chart.ReadFile(f) + if err != nil { + return nil, err + } + // Remove the path prefix. + rel, err := filepath.Rel(pathPrefix, f) + if err != nil { + return nil, err + } + return &loader.BufferedFile{ + Name: rel, + Data: bytes, + }, nil +} diff --git a/cli/helm/chart_test.go b/cli/helm/chart_test.go new file mode 100644 index 0000000000..1bed8bbdbe --- /dev/null +++ b/cli/helm/chart_test.go @@ -0,0 +1,56 @@ +package helm + +import ( + "embed" + "testing" + + "github.com/stretchr/testify/require" +) + +// Embed a test chart to test against. +//go:embed fixtures/consul/* fixtures/consul/templates/_helpers.tpl +var testChartFiles embed.FS + +func TestLoadChart(t *testing.T) { + directory := "fixtures/consul" + + expectedApiVersion := "v2" + expectedName := "Foo" + expectedVersion := "0.1.0" + expectedDescription := "Mock Helm Chart for testing." + expectedValues := map[string]interface{}{ + "key": "value", + } + + actual, err := LoadChart(testChartFiles, directory) + require.NoError(t, err) + require.Equal(t, expectedApiVersion, actual.Metadata.APIVersion) + require.Equal(t, expectedName, actual.Metadata.Name) + require.Equal(t, expectedVersion, actual.Metadata.Version) + require.Equal(t, expectedDescription, actual.Metadata.Description) + require.Equal(t, expectedValues, actual.Values) +} + +func TestReadChartFiles(t *testing.T) { + directory := "fixtures/consul" + expectedFiles := map[string]string{ + "Chart.yaml": "# This is a mock Helm Chart.yaml file used for testing.\napiVersion: v2\nname: Foo\nversion: 0.1.0\ndescription: Mock Helm Chart for testing.", + "values.yaml": "# This is a mock Helm values.yaml file used for testing.\nkey: value", + "templates/_helpers.tpl": "helpers", + "templates/foo.yaml": "foo: bar\n", + } + + files, err := ReadChartFiles(testChartFiles, directory) + require.NoError(t, err) + + actualFiles := make(map[string]string, len(files)) + for _, f := range files { + actualFiles[f.Name] = string(f.Data) + } + + for expectedName, expectedContents := range expectedFiles { + actualContents, ok := actualFiles[expectedName] + require.True(t, ok, "Expected file %s not found", expectedName) + require.Equal(t, expectedContents, actualContents) + } +} diff --git a/cli/helm/fixtures/consul/Chart.yaml b/cli/helm/fixtures/consul/Chart.yaml new file mode 100644 index 0000000000..72cdfea793 --- /dev/null +++ b/cli/helm/fixtures/consul/Chart.yaml @@ -0,0 +1,5 @@ +# This is a mock Helm Chart.yaml file used for testing. +apiVersion: v2 +name: Foo +version: 0.1.0 +description: Mock Helm Chart for testing. \ No newline at end of file diff --git a/cli/helm/fixtures/consul/templates/_helpers.tpl b/cli/helm/fixtures/consul/templates/_helpers.tpl new file mode 100644 index 0000000000..bdf8746bdb --- /dev/null +++ b/cli/helm/fixtures/consul/templates/_helpers.tpl @@ -0,0 +1 @@ +helpers \ No newline at end of file diff --git a/cli/helm/fixtures/consul/templates/foo.yaml b/cli/helm/fixtures/consul/templates/foo.yaml new file mode 100644 index 0000000000..20e9ff3fea --- /dev/null +++ b/cli/helm/fixtures/consul/templates/foo.yaml @@ -0,0 +1 @@ +foo: bar diff --git a/cli/helm/fixtures/consul/values.yaml b/cli/helm/fixtures/consul/values.yaml new file mode 100644 index 0000000000..e8062923bc --- /dev/null +++ b/cli/helm/fixtures/consul/values.yaml @@ -0,0 +1,2 @@ +# This is a mock Helm values.yaml file used for testing. +key: value \ No newline at end of file diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000000..c979ec625a --- /dev/null +++ b/cli/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/hashicorp/consul-k8s/cli/version" + "github.com/hashicorp/go-hclog" + "github.com/mitchellh/cli" +) + +func main() { + c := cli.NewCLI("consul-k8s", version.GetHumanVersion()) + c.Args = os.Args[1:] + + log := hclog.New(&hclog.LoggerOptions{ + Name: "cli", + Level: hclog.Info, + Output: os.Stdout, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + basecmd, commands := initializeCommands(ctx, log) + c.Commands = commands + defer func() { + _ = basecmd.Close() + }() + + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-ch + // Any cleanups, such as cancelling contexts + cancel() + _ = basecmd.Close() + os.Exit(1) + }() + + c.HelpFunc = cli.BasicHelpFunc("consul-k8s") + + exitStatus, err := c.Run() + if err != nil { + log.Info(err.Error()) + } + os.Exit(exitStatus) +} diff --git a/cli/version/version.go b/cli/version/version.go new file mode 100644 index 0000000000..b3a31b883f --- /dev/null +++ b/cli/version/version.go @@ -0,0 +1,50 @@ +package version + +import ( + "fmt" + "strings" +) + +var ( + // The git commit that was compiled. These will be filled in by the compiler. + GitCommit string + GitDescribe string + + // The main version number that is being run at the moment. + // + // Version must conform to the format expected by + // github.com/hashicorp/go-version for tests to work. + Version = "0.41.1" + + // A pre-release marker for the version. If this is "" (empty string) + // then it means that it is a final release. Otherwise, this is a pre-release + // such as "dev" (in development), "beta", "rc1", etc. + VersionPrerelease = "dev" +) + +// GetHumanVersion composes the parts of the version in a way that's suitable +// for displaying to humans. +func GetHumanVersion() string { + version := Version + if GitDescribe != "" { + version = GitDescribe + } + + release := VersionPrerelease + if GitDescribe == "" && release == "" { + release = "dev" + } + + if release != "" { + if !strings.HasSuffix(version, "-"+release) { + // if we tagged a prerelease version then the release is in the version already + version += fmt.Sprintf("-%s", release) + } + if GitCommit != "" { + version += fmt.Sprintf(" (%s)", GitCommit) + } + } + + // Strip off any single quotes added by the git information. + return strings.Replace(version, "'", "", -1) +} diff --git a/control-plane/.gitignore b/control-plane/.gitignore deleted file mode 100644 index 14f1aa298b..0000000000 --- a/control-plane/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -/bin -/pkg diff --git a/control-plane/Makefile b/control-plane/Makefile index 3560b22a55..eb174b3c1c 100644 --- a/control-plane/Makefile +++ b/control-plane/Makefile @@ -1,8 +1,8 @@ SHELL = bash -GOOS?=$(shell go env GOOS) -GOARCH?=$(shell go env GOARCH) -DEV_IMAGE?=consul-k8s-control-plane-dev +################ +# CI Variables # +################ GIT_COMMIT?=$(shell git rev-parse --short HEAD) GIT_DIRTY?=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true) GIT_DESCRIBE?=$(shell git describe --tags --always) @@ -18,88 +18,16 @@ export GIT_COMMIT export GIT_DIRTY export GIT_DESCRIBE -CRD_OPTIONS ?= "crd:trivialVersions=true,allowDangerousTypes=true" - -################ -# CI Variables # -################ CI_DEV_DOCKER_NAMESPACE?=hashicorpdev CI_DEV_DOCKER_IMAGE_NAME?=consul-k8s-control-plane CI_DEV_DOCKER_WORKDIR?=. CONSUL_K8S_IMAGE_VERSION?=latest ################ -dev: - @$(SHELL) $(CURDIR)/build-support/scripts/build-local.sh -o $(GOOS) -a $(GOARCH) - -dev-docker: - @$(SHELL) $(CURDIR)/build-support/scripts/build-local.sh -o linux -a amd64 - @docker build -t '$(DEV_IMAGE)' --build-arg 'GIT_COMMIT=$(GIT_COMMIT)' --build-arg 'GIT_DIRTY=$(GIT_DIRTY)' --build-arg 'GIT_DESCRIBE=$(GIT_DESCRIBE)' -f $(CURDIR)/build-support/docker/Dev.dockerfile $(CURDIR) - -dev-tree: +ci.dev-tree: @$(SHELL) $(CURDIR)/build-support/scripts/dev.sh $(DEV_PUSH_ARG) -test: - go test ./... - -# requires a consul enterprise binary on the path -ent-test: - go test ./... -tags=enterprise - -cov: - go test ./... -coverprofile=coverage.out - go tool cover -html=coverage.out - -clean: - @rm -rf \ - $(CURDIR)/bin \ - $(CURDIR)/pkg - -# Generate manifests e.g. CRD, RBAC etc. -ctrl-manifests: get-controller-gen - $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases - -# Generate code -ctrl-generate: get-controller-gen - $(CONTROLLER_GEN) object:headerFile="build-support/controller/boilerplate.go.txt" paths="./..." - -# Copy CRD YAML to consul-helm. -# Usage: make ctrl-crd-copy helm= -ctrl-crd-copy: - @cd hack/crds-to-consul-helm; go run ./... $(helm) - -# find or download controller-gen -# download controller-gen if necessary -get-controller-gen: -ifeq (, $(shell which controller-gen)) - @{ \ - set -e ;\ - CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ - cd $$CONTROLLER_GEN_TMP_DIR ;\ - go mod init tmp ;\ - go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.6.0 ;\ - rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ - } -CONTROLLER_GEN=$(GOBIN)/controller-gen -else -CONTROLLER_GEN=$(shell which controller-gen) -endif - -get-kustomize: -ifeq (, $(shell which kustomize)) - @{ \ - set -e ;\ - KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\ - cd $$KUSTOMIZE_GEN_TMP_DIR ;\ - go mod init tmp ;\ - go get sigs.k8s.io/kustomize/kustomize/v3@v3.5.4 ;\ - rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\ - } -KUSTOMIZE=$(GOBIN)/kustomize -else -KUSTOMIZE=$(shell which kustomize) -endif - +# TODO: Remove this ci.dev-docker target once we move the acceptance tests to Github Actions. # In CircleCI, the linux binary will be attached from a previous step at pkg/bin/linux_amd64/. This make target # should only run in CI and not locally. ci.dev-docker: @@ -119,13 +47,25 @@ ifeq ($(CIRCLE_BRANCH), main) @docker tag $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):$(GIT_COMMIT) $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):latest @docker push $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):latest endif -ifeq ($(CIRCLE_BRANCH), crd-controller-base) - @docker tag $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):$(GIT_COMMIT) $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):crd-controller-base-latest - @docker push $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):crd-controller-base-latest -endif -ifeq ($(CIRCLE_BRANCH), monorepo) - @docker tag $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):$(GIT_COMMIT) $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):monorepo - @docker push $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):monorepo + +# In Github Actions, the linux binary will be attached from a previous step at pkg/bin/linux_amd64/. This make target +# should only run in CI and not locally. +ci.dev-docker-github: + @echo "Pulling consul-k8s container image - $(CONSUL_K8S_IMAGE_VERSION)" + @docker pull hashicorp/consul-k8s:$(CONSUL_K8S_IMAGE_VERSION) >/dev/null #todo change this back after pulling it the first time since the dockerfile is FROM this image + @echo "Building consul-k8s Development container - $(CI_DEV_DOCKER_IMAGE_NAME)" + @docker build -t '$(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):$(GIT_COMMIT)' \ + --build-arg CONSUL_K8S_IMAGE_VERSION=$(CONSUL_K8S_IMAGE_VERSION) \ + --label COMMIT_SHA=$(GITHUB_SHA) \ + --label PULL_REQUEST=$(GITHIB_PULL_REQUEST) \ + --label GITHUB_BUILD_URL=$(GITHUB_SERVER_URL)/$(GITHUB_REPOSITORY)/actions/runs/$(GITHUB_RUN_ID) \ + $(CI_DEV_DOCKER_WORKDIR) -f $(CURDIR)/build-support/docker/Dev.dockerfile + @echo $(DOCKER_PASS) | docker login -u="$(DOCKER_USER)" --password-stdin + @echo "Pushing dev image to: https://cloud.docker.com/u/$(CI_DEV_DOCKER_NAMESPACE)/repository/docker/$(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME)" + @docker push $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):$(GIT_COMMIT) +ifeq ($(GITHUB_REF_NAME), main) + @docker tag $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):$(GIT_COMMIT) $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):latest + @docker push $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):latest endif -.PHONY: all bin clean dev dist docker-images go-build-image test tools ci.dev-docker +.PHONY: ci.dev-tree ci.dev-docker ci.dev-docker-github diff --git a/control-plane/PROJECT b/control-plane/PROJECT index 9df8efbbe7..16d303458c 100644 --- a/control-plane/PROJECT +++ b/control-plane/PROJECT @@ -1,63 +1,62 @@ domain: hashicorp.com -layout: go.kubebuilder.io/v2 +layout: +- go.kubebuilder.io/v2 +plugins: + go.operator-sdk.io/v2-alpha: {} repo: github.com/hashicorp/consul-k8s resources: -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: IngressGateway path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: ProxyDefaults path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: ServiceIntentions path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: ServiceDefaults path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: ServiceResolver path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: ServiceRouter path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: ServiceSplitter path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 -- - controller: true +- controller: true domain: hashicorp.com group: consul kind: TerminatingGateway path: github.com/hashicorp/consul-k8s/api/v1alpha1 version: v1alpha1 +- controller: true + domain: hashicorp.com + group: consul + kind: ExportedServices + path: github.com/hashicorp/consul-k8s/api/v1alpha1 + version: v1alpha1 version: "3" -plugins: - go.operator-sdk.io/v2-alpha: {} diff --git a/control-plane/api/common/common.go b/control-plane/api/common/common.go index 3d0ae3f6e7..7c761b6477 100644 --- a/control-plane/api/common/common.go +++ b/control-plane/api/common/common.go @@ -8,12 +8,14 @@ const ( ServiceRouter string = "servicerouter" ServiceSplitter string = "servicesplitter" ServiceIntentions string = "serviceintentions" + ExportedServices string = "exportedservices" IngressGateway string = "ingressgateway" TerminatingGateway string = "terminatinggateway" Global string = "global" Mesh string = "mesh" DefaultConsulNamespace string = "default" + DefaultConsulPartition string = "default" WildcardNamespace string = "*" SourceKey string = "external-source" diff --git a/control-plane/api/common/configentry.go b/control-plane/api/common/configentry.go index eefd52635f..2d83ce05b0 100644 --- a/control-plane/api/common/configentry.go +++ b/control-plane/api/common/configentry.go @@ -58,13 +58,42 @@ type ConfigEntryResource interface { // DeepCopyObject should be implemented by the generated code. DeepCopyObject() runtime.Object // Validate returns an error if the resource is invalid. - Validate(namespacesEnabled bool) error + Validate(consulMeta ConsulMeta) error // DefaultNamespaceFields sets Consul namespace fields on the config entry // spec to their default values if namespaces are enabled. - DefaultNamespaceFields(consulNamespacesEnabled bool, destinationNamespace string, mirroring bool, prefix string) + DefaultNamespaceFields(consulMeta ConsulMeta) // ConfigEntryResource has to implement metav1.Object so that structs // that implement it effectively implement client.Object which is // the interface supported by controller-runtime reconcile-able resources. metav1.Object } + +// ConsulMeta contains metadata which represents installation specific +// information about Consul. +type ConsulMeta struct { + // PartitionsEnabled indicates that a user is running Consul Enterprise + // with version 1.11+ which supports Admin Partitions. + PartitionsEnabled bool + // Partition is the name of the Admin Partition in Consul that the config + // entry will be created in. + Partition string + + // NamespacesEnabled indicates that a user is running Consul Enterprise + // with version 1.7+ which supports namespaces. + NamespacesEnabled bool + // DestinationNamespace is the namespace in Consul that the config entry created + // in k8s will get mapped into. If the Consul namespace does not already exist, it will + // be created. + DestinationNamespace string + // Mirroring causes Consul namespaces to be created to match the + // k8s namespace of any config entry custom resource. Config entries will + // be created in the matching Consul namespace. + Mirroring bool + // Prefix works in conjunction with Mirroring. + // It is the prefix added to the Consul namespace to map to a specific. + // k8s namespace. For example, if `mirroringK8SPrefix` is set to "k8s-", a + // service in the k8s `staging` namespace will be registered into the + // `k8s-staging` Consul namespace. + Prefix string +} diff --git a/control-plane/api/common/configentry_webhook.go b/control-plane/api/common/configentry_webhook.go index fe62b29754..4028a5e3cb 100644 --- a/control-plane/api/common/configentry_webhook.go +++ b/control-plane/api/common/configentry_webhook.go @@ -29,12 +29,9 @@ func ValidateConfigEntry( logger logr.Logger, configEntryLister ConfigEntryLister, cfgEntry ConfigEntryResource, - enableConsulNamespaces bool, - nsMirroring bool, - consulDestinationNamespace string, - nsMirroringPrefix string) admission.Response { + consulMeta ConsulMeta) admission.Response { - defaultingPatches, err := DefaultingPatches(cfgEntry, enableConsulNamespaces, nsMirroring, consulDestinationNamespace, nsMirroringPrefix) + defaultingPatches, err := DefaultingPatches(cfgEntry, consulMeta) if err != nil { return admission.Errored(http.StatusInternalServerError, err) } @@ -43,7 +40,7 @@ func ValidateConfigEntry( // resources to a single Consul namespace. The only case where we're not // mapping all kube resources to a single Consul namespace is when we // are running Consul enterprise with namespace mirroring. - singleConsulDestNS := !(enableConsulNamespaces && nsMirroring) + singleConsulDestNS := !(consulMeta.NamespacesEnabled && consulMeta.Mirroring) if req.Operation == admissionv1.Create && singleConsulDestNS { logger.Info("validate create", "name", cfgEntry.KubernetesName()) @@ -61,7 +58,7 @@ func ValidateConfigEntry( } } } - if err := cfgEntry.Validate(enableConsulNamespaces); err != nil { + if err := cfgEntry.Validate(consulMeta); err != nil { return admission.Errored(http.StatusBadRequest, err) } return admission.Patched(fmt.Sprintf("valid %s request", cfgEntry.KubeKind()), defaultingPatches...) @@ -69,12 +66,12 @@ func ValidateConfigEntry( // DefaultingPatches returns the patches needed to set fields to their // defaults. -func DefaultingPatches(cfgEntry ConfigEntryResource, enableConsulNamespaces bool, nsMirroring bool, consulDestinationNamespace string, nsMirroringPrefix string) ([]jsonpatch.Operation, error) { +func DefaultingPatches(cfgEntry ConfigEntryResource, consulMeta ConsulMeta) ([]jsonpatch.Operation, error) { beforeDefaulting, err := json.Marshal(cfgEntry) if err != nil { return nil, fmt.Errorf("marshalling input: %s", err) } - cfgEntry.DefaultNamespaceFields(enableConsulNamespaces, consulDestinationNamespace, nsMirroring, nsMirroringPrefix) + cfgEntry.DefaultNamespaceFields(consulMeta) afterDefaulting, err := json.Marshal(cfgEntry) if err != nil { return nil, fmt.Errorf("marshalling after defaulting: %s", err) diff --git a/control-plane/api/common/configentry_webhook_test.go b/control-plane/api/common/configentry_webhook_test.go index 258f4886de..cf79efea85 100644 --- a/control-plane/api/common/configentry_webhook_test.go +++ b/control-plane/api/common/configentry_webhook_test.go @@ -115,10 +115,12 @@ func TestValidateConfigEntry(t *testing.T) { logrtest.TestLogger{T: t}, lister, c.newResource, - c.enableNamespaces, - c.nsMirroring, - c.consulDestinationNS, - c.nsMirroringPrefix) + ConsulMeta{ + NamespacesEnabled: c.enableNamespaces, + DestinationNamespace: c.consulDestinationNS, + Mirroring: c.nsMirroring, + Prefix: c.nsMirroringPrefix, + }) require.Equal(t, c.expAllow, response.Allowed) if c.expErrMessage != "" { require.Equal(t, c.expErrMessage, response.AdmissionResponse.Result.Message) @@ -134,7 +136,7 @@ func TestDefaultingPatches(t *testing.T) { } // This test validates that DefaultingPatches invokes DefaultNamespaceFields on the Config Entry. - patches, err := DefaultingPatches(cfgEntry, false, false, "", "") + patches, err := DefaultingPatches(cfgEntry, ConsulMeta{}) require.NoError(t, err) require.Equal(t, []jsonpatch.Operation{ @@ -320,14 +322,14 @@ func (in *mockConfigEntry) ToConsul(string) capi.ConfigEntry { return &capi.ServiceConfigEntry{} } -func (in *mockConfigEntry) Validate(bool) error { +func (in *mockConfigEntry) Validate(_ ConsulMeta) error { if !in.Valid { return errors.New("invalid") } return nil } -func (in *mockConfigEntry) DefaultNamespaceFields(consulNamespacesEnabled bool, destinationNamespace string, mirroring bool, prefix string) { +func (in *mockConfigEntry) DefaultNamespaceFields(_ ConsulMeta) { in.MockNamespace = "bar" } diff --git a/control-plane/api/v1alpha1/exportedservices_types.go b/control-plane/api/v1alpha1/exportedservices_types.go new file mode 100644 index 0000000000..01c54dbe17 --- /dev/null +++ b/control-plane/api/v1alpha1/exportedservices_types.go @@ -0,0 +1,223 @@ +package v1alpha1 + +import ( + "errors" + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/control-plane/api/common" + "github.com/hashicorp/consul/api" + capi "github.com/hashicorp/consul/api" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +const ExportedServicesKubeKind = "exportedservices" + +func init() { + SchemeBuilder.Register(&ExportedServices{}, &ExportedServicesList{}) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// ExportedServices is the Schema for the exportedservices API +// +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" +// +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="exported-services" +type ExportedServices struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ExportedServicesSpec `json:"spec,omitempty"` + Status `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ExportedServicesList contains a list of ExportedServices. +type ExportedServicesList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ExportedServices `json:"items"` +} + +// ExportedServicesSpec defines the desired state of ExportedServices. +type ExportedServicesSpec struct { + // Services is a list of services to be exported and the list of partitions + // to expose them to. + Services []ExportedService `json:"services,omitempty"` +} + +// ExportedService manages the exporting of a service in the local partition to +// other partitions. +type ExportedService struct { + // Name is the name of the service to be exported. + Name string `json:"name,omitempty"` + + // Namespace is the namespace to export the service from. + Namespace string `json:"namespace,omitempty"` + + // Consumers is a list of downstream consumers of the service to be exported. + Consumers []ServiceConsumer `json:"consumers,omitempty"` +} + +// ServiceConsumer represents a downstream consumer of the service to be exported. +type ServiceConsumer struct { + // Partition is the admin partition to export the service to. + Partition string `json:"partition,omitempty"` +} + +func (in *ExportedServices) GetObjectMeta() metav1.ObjectMeta { + return in.ObjectMeta +} + +func (in *ExportedServices) AddFinalizer(name string) { + in.ObjectMeta.Finalizers = append(in.Finalizers(), name) +} + +func (in *ExportedServices) RemoveFinalizer(name string) { + var newFinalizers []string + for _, oldF := range in.Finalizers() { + if oldF != name { + newFinalizers = append(newFinalizers, oldF) + } + } + in.ObjectMeta.Finalizers = newFinalizers +} + +func (in *ExportedServices) Finalizers() []string { + return in.ObjectMeta.Finalizers +} + +func (in *ExportedServices) ConsulKind() string { + return capi.ExportedServices +} + +func (in *ExportedServices) ConsulGlobalResource() bool { + return true +} + +func (in *ExportedServices) ConsulMirroringNS() string { + return common.DefaultConsulNamespace +} + +func (in *ExportedServices) KubeKind() string { + return ExportedServicesKubeKind +} + +func (in *ExportedServices) ConsulName() string { + return in.ObjectMeta.Name +} + +func (in *ExportedServices) KubernetesName() string { + return in.ObjectMeta.Name +} + +func (in *ExportedServices) SetSyncedCondition(status corev1.ConditionStatus, reason, message string) { + in.Status.Conditions = Conditions{ + { + Type: ConditionSynced, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + }, + } +} + +func (in *ExportedServices) SetLastSyncedTime(time *metav1.Time) { + in.Status.LastSyncedTime = time +} + +func (in *ExportedServices) SyncedCondition() (status corev1.ConditionStatus, reason, message string) { + cond := in.Status.GetCondition(ConditionSynced) + if cond == nil { + return corev1.ConditionUnknown, "", "" + } + return cond.Status, cond.Reason, cond.Message +} + +func (in *ExportedServices) SyncedConditionStatus() corev1.ConditionStatus { + cond := in.Status.GetCondition(ConditionSynced) + if cond == nil { + return corev1.ConditionUnknown + } + return cond.Status +} + +func (in *ExportedServices) ToConsul(datacenter string) api.ConfigEntry { + var services []capi.ExportedService + for _, service := range in.Spec.Services { + services = append(services, service.toConsul()) + } + return &capi.ExportedServicesConfigEntry{ + Name: in.Name, + Services: services, + Meta: meta(datacenter), + } +} + +func (in *ExportedService) toConsul() capi.ExportedService { + var consumers []capi.ServiceConsumer + for _, consumer := range in.Consumers { + consumers = append(consumers, capi.ServiceConsumer{Partition: consumer.Partition}) + } + return capi.ExportedService{ + Name: in.Name, + Namespace: in.Namespace, + Consumers: consumers, + } +} + +func (in *ExportedServices) MatchesConsul(candidate api.ConfigEntry) bool { + configEntry, ok := candidate.(*capi.ExportedServicesConfigEntry) + if !ok { + return false + } + // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ExportedServicesConfigEntry{}, "Partition", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) + +} + +func (in *ExportedServices) Validate(consulMeta common.ConsulMeta) error { + var errs field.ErrorList + if !consulMeta.PartitionsEnabled { + return apierrors.NewForbidden( + schema.GroupResource{Group: ConsulHashicorpGroup, Resource: common.ExportedServices}, + in.KubernetesName(), + errors.New("Consul Enterprise Admin Partitions must be enabled to create ExportedServices")) + } + if in.Name != consulMeta.Partition { + errs = append(errs, field.Invalid(field.NewPath("name"), in.Name, fmt.Sprintf(`%s resource name must be the same name as the partition, "%s"`, in.KubeKind(), consulMeta.Partition))) + } + if len(in.Spec.Services) == 0 { + errs = append(errs, field.Invalid(field.NewPath("spec").Child("services"), in.Spec.Services, "at least one service must be exported")) + } + for i, service := range in.Spec.Services { + if err := service.validate(field.NewPath("spec").Child("services").Index(i)); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return apierrors.NewInvalid( + schema.GroupKind{Group: ConsulHashicorpGroup, Kind: ExportedServicesKubeKind}, + in.KubernetesName(), errs) + } + return nil +} + +func (in *ExportedService) validate(path *field.Path) *field.Error { + if len(in.Consumers) == 0 { + return field.Invalid(path, in.Consumers, "service must have at least 1 consumer.") + } + return nil +} + +func (in *ExportedServices) DefaultNamespaceFields(_ common.ConsulMeta) { +} diff --git a/control-plane/api/v1alpha1/exportedservices_types_test.go b/control-plane/api/v1alpha1/exportedservices_types_test.go new file mode 100644 index 0000000000..b35f27cca2 --- /dev/null +++ b/control-plane/api/v1alpha1/exportedservices_types_test.go @@ -0,0 +1,335 @@ +package v1alpha1 + +import ( + "testing" + "time" + + "github.com/hashicorp/consul-k8s/control-plane/api/common" + capi "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Test MatchesConsul for cases that should return true. +func TestExportedServices_MatchesConsul(t *testing.T) { + cases := map[string]struct { + Ours ExportedServices + Theirs capi.ConfigEntry + Matches bool + }{ + "empty fields matches": { + Ours: ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{}, + }, + Theirs: &capi.ExportedServicesConfigEntry{ + Name: common.DefaultConsulPartition, + CreateIndex: 1, + ModifyIndex: 2, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + Matches: true, + }, + "all fields set matches": { + Ours: ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []ServiceConsumer{ + { + Partition: "second", + }, + { + Partition: "third", + }, + }, + }, + { + Name: "service-backend", + Namespace: "backend", + Consumers: []ServiceConsumer{ + { + Partition: "fourth", + }, + { + Partition: "fifth", + }, + }, + }, + }, + }, + }, + Theirs: &capi.ExportedServicesConfigEntry{ + Name: common.DefaultConsulPartition, + Services: []capi.ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []capi.ServiceConsumer{ + { + Partition: "second", + }, + { + Partition: "third", + }, + }, + }, + { + Name: "service-backend", + Namespace: "backend", + Consumers: []capi.ServiceConsumer{ + { + Partition: "fourth", + }, + { + Partition: "fifth", + }, + }, + }, + }, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + CreateIndex: 1, + ModifyIndex: 2, + }, + Matches: true, + }, + "mismatched types does not match": { + Ours: ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{}, + }, + Theirs: &capi.ServiceConfigEntry{ + Name: common.DefaultConsulPartition, + Kind: capi.ExportedServices, + }, + Matches: false, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + require.Equal(t, c.Matches, c.Ours.MatchesConsul(c.Theirs)) + }) + } +} + +func TestExportedServices_ToConsul(t *testing.T) { + cases := map[string]struct { + Ours ExportedServices + Exp *capi.ExportedServicesConfigEntry + }{ + "empty fields": { + Ours: ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{}, + }, + Exp: &capi.ExportedServicesConfigEntry{ + Name: common.DefaultConsulPartition, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + }, + "every field set": { + Ours: ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []ServiceConsumer{ + { + Partition: "second", + }, + { + Partition: "third", + }, + }, + }, + { + Name: "service-backend", + Namespace: "backend", + Consumers: []ServiceConsumer{ + { + Partition: "fourth", + }, + { + Partition: "fifth", + }, + }, + }, + }, + }, + }, + Exp: &capi.ExportedServicesConfigEntry{ + Name: common.DefaultConsulPartition, + Services: []capi.ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []capi.ServiceConsumer{ + { + Partition: "second", + }, + { + Partition: "third", + }, + }, + }, + { + Name: "service-backend", + Namespace: "backend", + Consumers: []capi.ServiceConsumer{ + { + Partition: "fourth", + }, + { + Partition: "fifth", + }, + }, + }, + }, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + act := c.Ours.ToConsul("datacenter") + exportedServices, ok := act.(*capi.ExportedServicesConfigEntry) + require.True(t, ok, "could not cast") + require.Equal(t, c.Exp, exportedServices) + }) + } +} + +func TestExportedServices_AddFinalizer(t *testing.T) { + exportedServices := &ExportedServices{} + exportedServices.AddFinalizer("finalizer") + require.Equal(t, []string{"finalizer"}, exportedServices.ObjectMeta.Finalizers) +} + +func TestExportedServices_RemoveFinalizer(t *testing.T) { + exportedServices := &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{"f1", "f2"}, + }, + } + exportedServices.RemoveFinalizer("f1") + require.Equal(t, []string{"f2"}, exportedServices.ObjectMeta.Finalizers) +} + +func TestExportedServices_SetSyncedCondition(t *testing.T) { + exportedServices := &ExportedServices{} + exportedServices.SetSyncedCondition(corev1.ConditionTrue, "reason", "message") + + require.Equal(t, corev1.ConditionTrue, exportedServices.Status.Conditions[0].Status) + require.Equal(t, "reason", exportedServices.Status.Conditions[0].Reason) + require.Equal(t, "message", exportedServices.Status.Conditions[0].Message) + now := metav1.Now() + require.True(t, exportedServices.Status.Conditions[0].LastTransitionTime.Before(&now)) +} + +func TestExportedServices_SetLastSyncedTime(t *testing.T) { + exportedServices := &ExportedServices{} + syncedTime := metav1.NewTime(time.Now()) + exportedServices.SetLastSyncedTime(&syncedTime) + + require.Equal(t, &syncedTime, exportedServices.Status.LastSyncedTime) +} + +func TestExportedServices_GetSyncedConditionStatus(t *testing.T) { + cases := []corev1.ConditionStatus{ + corev1.ConditionUnknown, + corev1.ConditionFalse, + corev1.ConditionTrue, + } + for _, status := range cases { + t.Run(string(status), func(t *testing.T) { + exportedServices := &ExportedServices{ + Status: Status{ + Conditions: []Condition{{ + Type: ConditionSynced, + Status: status, + }}, + }, + } + + require.Equal(t, status, exportedServices.SyncedConditionStatus()) + }) + } +} + +func TestExportedServices_GetConditionWhenStatusNil(t *testing.T) { + require.Nil(t, (&ExportedServices{}).GetCondition(ConditionSynced)) +} + +func TestExportedServices_SyncedConditionStatusWhenStatusNil(t *testing.T) { + require.Equal(t, corev1.ConditionUnknown, (&ExportedServices{}).SyncedConditionStatus()) +} + +func TestExportedServices_SyncedConditionWhenStatusNil(t *testing.T) { + status, reason, message := (&ExportedServices{}).SyncedCondition() + require.Equal(t, corev1.ConditionUnknown, status) + require.Equal(t, "", reason) + require.Equal(t, "", message) +} + +func TestExportedServices_ConsulKind(t *testing.T) { + require.Equal(t, capi.ExportedServices, (&ExportedServices{}).ConsulKind()) +} + +func TestExportedServices_KubeKind(t *testing.T) { + require.Equal(t, "exportedservices", (&ExportedServices{}).KubeKind()) +} + +func TestExportedServices_ConsulName(t *testing.T) { + require.Equal(t, "foo", (&ExportedServices{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).ConsulName()) +} + +func TestExportedServices_KubernetesName(t *testing.T) { + require.Equal(t, "foo", (&ExportedServices{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).KubernetesName()) +} + +func TestExportedServices_ConsulNamespace(t *testing.T) { + require.Equal(t, common.DefaultConsulNamespace, (&ExportedServices{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}}).ConsulMirroringNS()) +} + +func TestExportedServices_ConsulGlobalResource(t *testing.T) { + require.True(t, (&ExportedServices{}).ConsulGlobalResource()) +} + +func TestExportedServices_ObjectMeta(t *testing.T) { + meta := metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + } + exportedServices := &ExportedServices{ + ObjectMeta: meta, + } + require.Equal(t, meta, exportedServices.GetObjectMeta()) +} diff --git a/control-plane/api/v1alpha1/exportedservices_webhook.go b/control-plane/api/v1alpha1/exportedservices_webhook.go new file mode 100644 index 0000000000..d80062e958 --- /dev/null +++ b/control-plane/api/v1alpha1/exportedservices_webhook.go @@ -0,0 +1,67 @@ +package v1alpha1 + +import ( + "context" + "fmt" + "net/http" + + "github.com/go-logr/logr" + "github.com/hashicorp/consul-k8s/control-plane/api/common" + capi "github.com/hashicorp/consul/api" + admissionv1 "k8s.io/api/admission/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// +kubebuilder:object:generate=false + +type ExportedServicesWebhook struct { + client.Client + ConsulClient *capi.Client + Logger logr.Logger + decoder *admission.Decoder + ConsulMeta common.ConsulMeta +} + +// NOTE: The path value in the below line is the path to the webhook. +// If it is updated, run code-gen, update subcommand/controller/command.go +// and the consul-helm value for the path to the webhook. +// +// NOTE: The below line cannot be combined with any other comment. If it is +// it will break the code generation. +// +// +kubebuilder:webhook:verbs=create;update,path=/mutate-v1alpha1-exportedservices,mutating=true,failurePolicy=fail,groups=consul.hashicorp.com,resources=exportedservices,versions=v1alpha1,name=mutate-exportedservices.consul.hashicorp.com,sideEffects=None,admissionReviewVersions=v1beta1;v1 + +func (v *ExportedServicesWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { + var exports ExportedServices + var exportsList ExportedServicesList + err := v.decoder.Decode(req, &exports) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if req.Operation == admissionv1.Create { + v.Logger.Info("validate create", "name", exports.KubernetesName()) + + if err := v.Client.List(ctx, &exportsList); err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + if len(exportsList.Items) > 0 { + return admission.Errored(http.StatusBadRequest, + fmt.Errorf("%s resource already defined - only one exportedservices entry is supported per Kubernetes cluster", + exports.KubeKind())) + } + } + + if err := exports.Validate(v.ConsulMeta); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + return admission.Allowed(fmt.Sprintf("valid %s request", exports.KubeKind())) +} + +func (v *ExportedServicesWebhook) InjectDecoder(d *admission.Decoder) error { + v.decoder = d + return nil +} diff --git a/control-plane/api/v1alpha1/exportedservices_webhook_test.go b/control-plane/api/v1alpha1/exportedservices_webhook_test.go new file mode 100644 index 0000000000..9c4b79b13d --- /dev/null +++ b/control-plane/api/v1alpha1/exportedservices_webhook_test.go @@ -0,0 +1,200 @@ +package v1alpha1 + +import ( + "context" + "encoding/json" + "testing" + + logrtest "github.com/go-logr/logr/testing" + "github.com/hashicorp/consul-k8s/control-plane/api/common" + "github.com/stretchr/testify/require" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestValidateExportedServices(t *testing.T) { + otherNS := "other" + otherPartition := "other" + + cases := map[string]struct { + existingResources []runtime.Object + newResource *ExportedServices + consulMeta common.ConsulMeta + expAllow bool + expErrMessage string + }{ + "no duplicates, valid": { + existingResources: nil, + newResource: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service", + Namespace: "service-ns", + Consumers: []ServiceConsumer{{Partition: "other"}}, + }, + }, + }, + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + Partition: otherPartition, + }, + expAllow: true, + }, + "exportedservices exists": { + existingResources: []runtime.Object{&ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherPartition, + }, + }}, + newResource: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service", + Namespace: "service-ns", + Consumers: []ServiceConsumer{{Partition: "other"}}, + }, + }, + }, + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + Partition: otherPartition, + }, + expAllow: false, + expErrMessage: "exportedservices resource already defined - only one exportedservices entry is supported per Kubernetes cluster", + }, + "name not partition name": { + existingResources: []runtime.Object{}, + newResource: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local", + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service", + Namespace: "service-ns", + Consumers: []ServiceConsumer{{Partition: "other"}}, + }, + }, + }, + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + Partition: otherPartition, + }, + expAllow: false, + expErrMessage: "exportedservices.consul.hashicorp.com \"local\" is invalid: name: Invalid value: \"local\": exportedservices resource name must be the same name as the partition, \"other\"", + }, + "partitions disabled": { + existingResources: []runtime.Object{}, + newResource: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service", + Namespace: "service-ns", + Consumers: []ServiceConsumer{{Partition: "other"}}, + }, + }, + }, + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: false, + Partition: "", + }, + expAllow: false, + expErrMessage: "exportedservices.consul.hashicorp.com \"other\" is forbidden: Consul Enterprise Admin Partitions must be enabled to create ExportedServices", + }, + "no services": { + existingResources: []runtime.Object{}, + newResource: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{}, + }, + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + Partition: otherPartition, + }, + expAllow: false, + expErrMessage: "exportedservices.consul.hashicorp.com \"other\" is invalid: spec.services: Invalid value: []v1alpha1.ExportedService(nil): at least one service must be exported", + }, + "service with no consumers": { + existingResources: []runtime.Object{}, + newResource: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service", + Namespace: "service-ns", + Consumers: []ServiceConsumer{}, + }, + }, + }, + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + Partition: otherPartition, + }, + expAllow: false, + expErrMessage: "exportedservices.consul.hashicorp.com \"other\" is invalid: spec.services[0]: Invalid value: []v1alpha1.ServiceConsumer(nil): service must have at least 1 consumer.", + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + marshalledRequestObject, err := json.Marshal(c.newResource) + require.NoError(t, err) + s := runtime.NewScheme() + s.AddKnownTypes(GroupVersion, &ExportedServices{}, &ExportedServicesList{}) + client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(c.existingResources...).Build() + decoder, err := admission.NewDecoder(s) + require.NoError(t, err) + + validator := &ExportedServicesWebhook{ + Client: client, + ConsulClient: nil, + Logger: logrtest.TestLogger{T: t}, + decoder: decoder, + ConsulMeta: c.consulMeta, + } + response := validator.Handle(ctx, admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: c.newResource.KubernetesName(), + Namespace: otherNS, + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: marshalledRequestObject, + }, + }, + }) + + require.Equal(t, c.expAllow, response.Allowed) + if c.expErrMessage != "" { + require.Equal(t, c.expErrMessage, response.AdmissionResponse.Result.Message) + } + }) + } +} diff --git a/control-plane/api/v1alpha1/groupversion_info.go b/control-plane/api/v1alpha1/groupversion_info.go index b6054efb6f..cdbe085af4 100644 --- a/control-plane/api/v1alpha1/groupversion_info.go +++ b/control-plane/api/v1alpha1/groupversion_info.go @@ -11,10 +11,10 @@ import ( const ConsulHashicorpGroup string = "consul.hashicorp.com" var ( - // GroupVersion is group version used to register these objects + // GroupVersion is group version used to register these objects. GroupVersion = schema.GroupVersion{Group: "consul.hashicorp.com", Version: "v1alpha1"} - // SchemeBuilder is used to add go types to the GroupVersionKind scheme + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. diff --git a/control-plane/api/v1alpha1/ingressgateway_types.go b/control-plane/api/v1alpha1/ingressgateway_types.go index 6813b51f35..aef9917b49 100644 --- a/control-plane/api/v1alpha1/ingressgateway_types.go +++ b/control-plane/api/v1alpha1/ingressgateway_types.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/hashicorp/consul-k8s/control-plane/namespaces" capi "github.com/hashicorp/consul/api" corev1 "k8s.io/api/core/v1" @@ -31,6 +32,7 @@ func init() { // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="ingress-gateway" type IngressGateway struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -41,14 +43,14 @@ type IngressGateway struct { // +kubebuilder:object:root=true -// IngressGatewayList contains a list of IngressGateway +// IngressGatewayList contains a list of IngressGateway. type IngressGatewayList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []IngressGateway `json:"items"` } -// IngressGatewaySpec defines the desired state of IngressGateway +// IngressGatewaySpec defines the desired state of IngressGateway. type IngressGatewaySpec struct { // TLS holds the TLS configuration for this gateway. TLS GatewayTLSConfig `json:"tls,omitempty"` @@ -60,6 +62,19 @@ type IngressGatewaySpec struct { type GatewayTLSConfig struct { // Indicates that TLS should be enabled for this gateway service. Enabled bool `json:"enabled"` + + // SDS allows configuring TLS certificate from an SDS service. + SDS *GatewayTLSSDSConfig `json:"sds,omitempty"` +} + +type GatewayServiceTLSConfig struct { + // SDS allows configuring TLS certificate from an SDS service. + SDS *GatewayTLSSDSConfig `json:"sds,omitempty"` +} + +type GatewayTLSSDSConfig struct { + ClusterName string `json:"clusterName,omitempty"` + CertResource string `json:"certResource,omitempty"` } // IngressListener manages the configuration for a listener on a specific port. @@ -73,6 +88,9 @@ type IngressListener struct { // current supported values are: (tcp | http | http2 | grpc). Protocol string `json:"protocol,omitempty"` + // TLS config for this listener. + TLS *GatewayTLSConfig `json:"tls,omitempty"` + // Services declares the set of services to which the listener forwards // traffic. // @@ -110,6 +128,17 @@ type IngressService struct { // Namespace is the namespace where the service is located. // Namespacing is a Consul Enterprise feature. Namespace string `json:"namespace,omitempty"` + + // Partition is the admin-partition where the service is located. + // Partitioning is a Consul Enterprise feature. + Partition string `json:"partition,omitempty"` + + // TLS allows specifying some TLS configuration per listener. + TLS *GatewayServiceTLSConfig `json:"tls,omitempty"` + + // Allow HTTP header manipulation to be configured. + RequestHeaders *HTTPHeaderModifiers `json:"requestHeaders,omitempty"` + ResponseHeaders *HTTPHeaderModifiers `json:"responseHeaders,omitempty"` } func (in *IngressGateway) GetObjectMeta() metav1.ObjectMeta { @@ -198,7 +227,7 @@ func (in *IngressGateway) ToConsul(datacenter string) capi.ConfigEntry { return &capi.IngressGatewayConfigEntry{ Kind: in.ConsulKind(), Name: in.ConsulName(), - TLS: in.Spec.TLS.toConsul(), + TLS: *in.Spec.TLS.toConsul(), Listeners: listeners, Meta: meta(datacenter), } @@ -210,19 +239,17 @@ func (in *IngressGateway) MatchesConsul(candidate capi.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.IngressGatewayConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.IngressGatewayConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) } -func (in *IngressGateway) Validate(namespacesEnabled bool) error { +func (in *IngressGateway) Validate(consulMeta common.ConsulMeta) error { var errs field.ErrorList path := field.NewPath("spec") for i, v := range in.Spec.Listeners { - errs = append(errs, v.validate(path.Child("listeners").Index(i))...) + errs = append(errs, v.validate(path.Child("listeners").Index(i), consulMeta)...) } - errs = append(errs, in.validateNamespaces(namespacesEnabled)...) - if len(errs) > 0 { return apierrors.NewInvalid( schema.GroupKind{Group: ConsulHashicorpGroup, Kind: ingressGatewayKubeKind}, @@ -232,14 +259,14 @@ func (in *IngressGateway) Validate(namespacesEnabled bool) error { } // DefaultNamespaceFields sets the namespace field on spec.listeners[].services to their default values if namespaces are enabled. -func (in *IngressGateway) DefaultNamespaceFields(consulNamespacesEnabled bool, destinationNamespace string, mirroring bool, prefix string) { +func (in *IngressGateway) DefaultNamespaceFields(consulMeta common.ConsulMeta) { // If namespaces are enabled we want to set the namespace fields to their // defaults. If namespaces are not enabled (i.e. OSS) we don't set the // namespace fields because this would cause errors // making API calls (because namespace fields can't be set in OSS). - if consulNamespacesEnabled { + if consulMeta.NamespacesEnabled { // Default to the current namespace (i.e. the namespace of the config entry). - namespace := namespaces.ConsulNamespace(in.Namespace, consulNamespacesEnabled, destinationNamespace, mirroring, prefix) + namespace := namespaces.ConsulNamespace(in.Namespace, consulMeta.NamespacesEnabled, consulMeta.DestinationNamespace, consulMeta.Mirroring, consulMeta.Prefix) for i, listener := range in.Spec.Listeners { for j, service := range listener.Services { if service.Namespace == "" { @@ -250,9 +277,13 @@ func (in *IngressGateway) DefaultNamespaceFields(consulNamespacesEnabled bool, d } } -func (in GatewayTLSConfig) toConsul() capi.GatewayTLSConfig { - return capi.GatewayTLSConfig{ +func (in *GatewayTLSConfig) toConsul() *capi.GatewayTLSConfig { + if in == nil { + return nil + } + return &capi.GatewayTLSConfig{ Enabled: in.Enabled, + SDS: in.SDS.toConsul(), } } @@ -261,22 +292,47 @@ func (in IngressListener) toConsul() capi.IngressListener { for _, s := range in.Services { services = append(services, s.toConsul()) } + return capi.IngressListener{ Port: in.Port, Protocol: in.Protocol, + TLS: in.TLS.toConsul(), Services: services, } } func (in IngressService) toConsul() capi.IngressService { return capi.IngressService{ - Name: in.Name, - Hosts: in.Hosts, - Namespace: in.Namespace, + Name: in.Name, + Hosts: in.Hosts, + Namespace: in.Namespace, + Partition: in.Partition, + TLS: in.TLS.toConsul(), + RequestHeaders: in.RequestHeaders.toConsul(), + ResponseHeaders: in.ResponseHeaders.toConsul(), } } -func (in IngressListener) validate(path *field.Path) field.ErrorList { +func (in *GatewayTLSSDSConfig) toConsul() *capi.GatewayTLSSDSConfig { + if in == nil { + return nil + } + return &capi.GatewayTLSSDSConfig{ + ClusterName: in.ClusterName, + CertResource: in.CertResource, + } +} + +func (in *GatewayServiceTLSConfig) toConsul() *capi.GatewayServiceTLSConfig { + if in == nil { + return nil + } + return &capi.GatewayServiceTLSConfig{ + SDS: in.SDS.toConsul(), + } +} + +func (in IngressListener) validate(path *field.Path, consulMeta common.ConsulMeta) field.ErrorList { var errs field.ErrorList validProtocols := []string{"tcp", "http", "http2", "grpc"} if !sliceContains(validProtocols, in.Protocol) { @@ -306,6 +362,16 @@ func (in IngressListener) validate(path *field.Path) field.ErrorList { fmt.Sprintf("hosts must be empty if name is %q", wildcardServiceName))) } + if svc.Partition != "" && !consulMeta.PartitionsEnabled { + errs = append(errs, field.Invalid(path.Child("services").Index(i).Child("partition"), + svc.Partition, `Consul Enterprise admin-partitions must be enabled to set service.partition`)) + } + + if svc.Namespace != "" && !consulMeta.NamespacesEnabled { + errs = append(errs, field.Invalid(path.Child("services").Index(i).Child("namespace"), + svc.Namespace, `Consul Enterprise namespaces must be enabled to set service.namespace`)) + } + if len(svc.Hosts) > 0 && in.Protocol == "tcp" { asJSON, _ := json.Marshal(svc.Hosts) errs = append(errs, field.Invalid(path.Child("services").Index(i).Child("hosts"), @@ -315,19 +381,3 @@ func (in IngressListener) validate(path *field.Path) field.ErrorList { } return errs } - -func (in *IngressGateway) validateNamespaces(namespacesEnabled bool) field.ErrorList { - var errs field.ErrorList - path := field.NewPath("spec") - if !namespacesEnabled { - for i, listener := range in.Spec.Listeners { - for j, service := range listener.Services { - if service.Namespace != "" { - errs = append(errs, field.Invalid(path.Child("listeners").Index(i).Child("services").Index(j).Child("namespace"), - service.Namespace, `Consul Enterprise namespaces must be enabled to set service.namespace`)) - } - } - } - } - return errs -} diff --git a/control-plane/api/v1alpha1/ingressgateway_types_test.go b/control-plane/api/v1alpha1/ingressgateway_types_test.go index a7ec548e89..4260b39871 100644 --- a/control-plane/api/v1alpha1/ingressgateway_types_test.go +++ b/control-plane/api/v1alpha1/ingressgateway_types_test.go @@ -46,16 +46,64 @@ func TestIngressGateway_MatchesConsul(t *testing.T) { Spec: IngressGatewaySpec{ TLS: GatewayTLSConfig{ Enabled: true, + SDS: &GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, }, Listeners: []IngressListener{ { Port: 8888, Protocol: "tcp", + TLS: &GatewayTLSConfig{ + Enabled: true, + SDS: &GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, Services: []IngressService{ { Name: "name1", Hosts: []string{"host1_1", "host1_2"}, Namespace: "ns1", + Partition: "default", + TLS: &GatewayServiceTLSConfig{ + SDS: &GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, + RequestHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, { Name: "name2", @@ -82,16 +130,64 @@ func TestIngressGateway_MatchesConsul(t *testing.T) { Namespace: "foobar", TLS: capi.GatewayTLSConfig{ Enabled: true, + SDS: &capi.GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, }, Listeners: []capi.IngressListener{ { Port: 8888, Protocol: "tcp", + TLS: &capi.GatewayTLSConfig{ + Enabled: true, + SDS: &capi.GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, Services: []capi.IngressService{ { Name: "name1", Hosts: []string{"host1_1", "host1_2"}, Namespace: "ns1", + Partition: "default", + TLS: &capi.GatewayServiceTLSConfig{ + SDS: &capi.GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, + RequestHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, { Name: "name2", @@ -173,16 +269,64 @@ func TestIngressGateway_ToConsul(t *testing.T) { Spec: IngressGatewaySpec{ TLS: GatewayTLSConfig{ Enabled: true, + SDS: &GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, }, Listeners: []IngressListener{ { Port: 8888, Protocol: "tcp", + TLS: &GatewayTLSConfig{ + Enabled: true, + SDS: &GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, Services: []IngressService{ { Name: "name1", Hosts: []string{"host1_1", "host1_2"}, Namespace: "ns1", + Partition: "default", + TLS: &GatewayServiceTLSConfig{ + SDS: &GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, + RequestHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, { Name: "name2", @@ -208,16 +352,64 @@ func TestIngressGateway_ToConsul(t *testing.T) { Name: "name", TLS: capi.GatewayTLSConfig{ Enabled: true, + SDS: &capi.GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, }, Listeners: []capi.IngressListener{ { Port: 8888, Protocol: "tcp", + TLS: &capi.GatewayTLSConfig{ + Enabled: true, + SDS: &capi.GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, Services: []capi.IngressService{ { Name: "name1", Hosts: []string{"host1_1", "host1_2"}, Namespace: "ns1", + Partition: "default", + TLS: &capi.GatewayServiceTLSConfig{ + SDS: &capi.GatewayTLSSDSConfig{ + ClusterName: "cluster1", + CertResource: "cert1", + }, + }, + RequestHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, { Name: "name2", @@ -258,6 +450,7 @@ func TestIngressGateway_Validate(t *testing.T) { cases := map[string]struct { input *IngressGateway namespacesEnabled bool + partitionEnabled bool expectedErrMsgs []string }{ "listener.protocol invalid": { @@ -420,6 +613,51 @@ func TestIngressGateway_Validate(t *testing.T) { }, namespacesEnabled: true, }, + "service.partition set when partitions disabled": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Listeners: []IngressListener{ + { + Protocol: "tcp", + Services: []IngressService{ + { + Name: "name", + Partition: "foo", + }, + }, + }, + }, + }, + }, + partitionEnabled: false, + expectedErrMsgs: []string{ + `spec.listeners[0].services[0].partition: Invalid value: "foo": Consul Enterprise admin-partitions must be enabled to set service.partition`, + }, + }, + "service.partition set when partitions enabled": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Listeners: []IngressListener{ + { + Protocol: "tcp", + Services: []IngressService{ + { + Name: "name", + Partition: "foo", + }, + }, + }, + }, + }, + }, + partitionEnabled: true, + }, "multiple errors": { input: &IngressGateway{ ObjectMeta: metav1.ObjectMeta{ @@ -448,7 +686,7 @@ func TestIngressGateway_Validate(t *testing.T) { for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(testCase.namespacesEnabled) + err := testCase.input.Validate(common.ConsulMeta{NamespacesEnabled: testCase.namespacesEnabled, PartitionsEnabled: testCase.partitionEnabled}) if len(testCase.expectedErrMsgs) != 0 { require.Error(t, err) for _, s := range testCase.expectedErrMsgs { @@ -464,39 +702,44 @@ func TestIngressGateway_Validate(t *testing.T) { // Test defaulting behavior when namespaces are enabled as well as disabled. func TestIngressGateway_DefaultNamespaceFields(t *testing.T) { namespaceConfig := map[string]struct { - enabled bool - destinationNamespace string - mirroring bool - prefix string - expectedDestination string + consulMeta common.ConsulMeta + expectedDestination string }{ "disabled": { - enabled: false, - destinationNamespace: "", - mirroring: false, - prefix: "", - expectedDestination: "", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: false, + DestinationNamespace: "", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "", }, "destinationNS": { - enabled: true, - destinationNamespace: "foo", - mirroring: false, - prefix: "", - expectedDestination: "foo", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "foo", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "foo", }, "mirroringEnabledWithoutPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "", - expectedDestination: "bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "", + }, + expectedDestination: "bar", }, "mirroringWithPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "ns-", - expectedDestination: "ns-bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "ns-", + }, + expectedDestination: "ns-bar", }, } @@ -547,7 +790,7 @@ func TestIngressGateway_DefaultNamespaceFields(t *testing.T) { }, }, } - input.DefaultNamespaceFields(s.enabled, s.destinationNamespace, s.mirroring, s.prefix) + input.DefaultNamespaceFields(s.consulMeta) require.True(t, cmp.Equal(input, output)) }) } diff --git a/control-plane/api/v1alpha1/ingressgateway_webhook.go b/control-plane/api/v1alpha1/ingressgateway_webhook.go index 6a3aee2fd0..8dcc2fa9ee 100644 --- a/control-plane/api/v1alpha1/ingressgateway_webhook.go +++ b/control-plane/api/v1alpha1/ingressgateway_webhook.go @@ -17,26 +17,8 @@ type IngressGatewayWebhook struct { ConsulClient *capi.Client Logger logr.Logger - // EnableConsulNamespaces indicates that a user is running Consul Enterprise - // with version 1.7+ which supports namespaces. - EnableConsulNamespaces bool - - // EnableNSMirroring causes Consul namespaces to be created to match the - // k8s namespace of any config entry custom resource. Config entries will - // be created in the matching Consul namespace. - EnableNSMirroring bool - - // ConsulDestinationNamespace is the namespace in Consul that the config entry created - // in k8s will get mapped into. If the Consul namespace does not already exist, it will - // be created. - ConsulDestinationNamespace string - - // NSMirroringPrefix works in conjunction with Namespace Mirroring. - // It is the prefix added to the Consul namespace to map to a specific. - // k8s namespace. For example, if `mirroringK8SPrefix` is set to "k8s-", a - // service in the k8s `staging` namespace will be registered into the - // `k8s-staging` Consul namespace. - NSMirroringPrefix string + // ConsulMeta contains metadata specific to the Consul installation. + ConsulMeta common.ConsulMeta decoder *admission.Decoder client.Client @@ -57,15 +39,7 @@ func (v *IngressGatewayWebhook) Handle(ctx context.Context, req admission.Reques return admission.Errored(http.StatusBadRequest, err) } - return common.ValidateConfigEntry(ctx, - req, - v.Logger, - v, - &resource, - v.EnableConsulNamespaces, - v.EnableNSMirroring, - v.ConsulDestinationNamespace, - v.NSMirroringPrefix) + return common.ValidateConfigEntry(ctx, req, v.Logger, v, &resource, v.ConsulMeta) } func (v *IngressGatewayWebhook) List(ctx context.Context) ([]common.ConfigEntryResource, error) { diff --git a/control-plane/api/v1alpha1/mesh_types.go b/control-plane/api/v1alpha1/mesh_types.go index c637c1a783..057ba9f071 100644 --- a/control-plane/api/v1alpha1/mesh_types.go +++ b/control-plane/api/v1alpha1/mesh_types.go @@ -34,14 +34,14 @@ type Mesh struct { //+kubebuilder:object:root=true -// MeshList contains a list of Mesh +// MeshList contains a list of Mesh. type MeshList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Mesh `json:"items"` } -// MeshSpec defines the desired state of Mesh +// MeshSpec defines the desired state of Mesh. type MeshSpec struct { TransparentProxy TransparentProxyMeshConfig `json:"transparentProxy,omitempty"` } @@ -150,13 +150,13 @@ func (in *Mesh) MatchesConsul(candidate capi.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.MeshConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.MeshConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) } -func (in *Mesh) Validate(_ bool) error { +func (in *Mesh) Validate(_ common.ConsulMeta) error { return nil } // DefaultNamespaceFields has no behaviour here as meshes have no namespace specific fields. -func (in *Mesh) DefaultNamespaceFields(_ bool, _ string, _ bool, _ string) { +func (in *Mesh) DefaultNamespaceFields(_ common.ConsulMeta) { } diff --git a/control-plane/api/v1alpha1/mesh_webhook.go b/control-plane/api/v1alpha1/mesh_webhook.go index 54fbe2ff5d..d28cfc193c 100644 --- a/control-plane/api/v1alpha1/mesh_webhook.go +++ b/control-plane/api/v1alpha1/mesh_webhook.go @@ -17,11 +17,9 @@ import ( type MeshWebhook struct { client.Client - ConsulClient *capi.Client - Logger logr.Logger - decoder *admission.Decoder - EnableConsulNamespaces bool - EnableNSMirroring bool + ConsulClient *capi.Client + Logger logr.Logger + decoder *admission.Decoder } // NOTE: The path value in the below line is the path to the webhook. diff --git a/control-plane/api/v1alpha1/proxydefaults_types.go b/control-plane/api/v1alpha1/proxydefaults_types.go index bf4024eafa..22f498cf8e 100644 --- a/control-plane/api/v1alpha1/proxydefaults_types.go +++ b/control-plane/api/v1alpha1/proxydefaults_types.go @@ -31,6 +31,7 @@ func init() { // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="proxy-defaults" type ProxyDefaults struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -40,7 +41,7 @@ type ProxyDefaults struct { // +kubebuilder:object:root=true -// ProxyDefaultsList contains a list of ProxyDefaults +// ProxyDefaultsList contains a list of ProxyDefaults. type ProxyDefaultsList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` @@ -49,7 +50,7 @@ type ProxyDefaultsList struct { // RawMessage for Config based on recommendation here: https://github.com/kubernetes-sigs/controller-tools/issues/294#issuecomment-518380816 -// ProxyDefaultsSpec defines the desired state of ProxyDefaults +// ProxyDefaultsSpec defines the desired state of ProxyDefaults. type ProxyDefaultsSpec struct { // Config is an arbitrary map of configuration values used by Connect proxies. // Any values that your proxy allows can be configured globally here. @@ -174,11 +175,11 @@ func (in *ProxyDefaults) MatchesConsul(candidate api.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ProxyConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty(), + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ProxyConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty(), cmp.Comparer(transparentProxyConfigComparer)) } -func (in *ProxyDefaults) Validate(namespacesEnabled bool) error { +func (in *ProxyDefaults) Validate(_ common.ConsulMeta) error { var allErrs field.ErrorList path := field.NewPath("spec") @@ -205,7 +206,7 @@ func (in *ProxyDefaults) Validate(namespacesEnabled bool) error { } // DefaultNamespaceFields has no behaviour here as proxy-defaults have no namespace specific fields. -func (in *ProxyDefaults) DefaultNamespaceFields(_ bool, _ string, _ bool, _ string) { +func (in *ProxyDefaults) DefaultNamespaceFields(_ common.ConsulMeta) { } // convertConfig converts the config of type json.RawMessage which is stored diff --git a/control-plane/api/v1alpha1/proxydefaults_types_test.go b/control-plane/api/v1alpha1/proxydefaults_types_test.go index 7b39f4b715..2950a3a36e 100644 --- a/control-plane/api/v1alpha1/proxydefaults_types_test.go +++ b/control-plane/api/v1alpha1/proxydefaults_types_test.go @@ -394,7 +394,7 @@ func TestProxyDefaults_Validate(t *testing.T) { } for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(false) + err := testCase.input.Validate(common.ConsulMeta{}) if testCase.expectedErrMsg != "" { require.EqualError(t, err, testCase.expectedErrMsg) } else { diff --git a/control-plane/api/v1alpha1/proxydefaults_webhook.go b/control-plane/api/v1alpha1/proxydefaults_webhook.go index 85f4a00847..4e221e0130 100644 --- a/control-plane/api/v1alpha1/proxydefaults_webhook.go +++ b/control-plane/api/v1alpha1/proxydefaults_webhook.go @@ -17,11 +17,10 @@ import ( type ProxyDefaultsWebhook struct { client.Client - ConsulClient *capi.Client - Logger logr.Logger - decoder *admission.Decoder - EnableConsulNamespaces bool - EnableNSMirroring bool + ConsulClient *capi.Client + Logger logr.Logger + decoder *admission.Decoder + ConsulMeta common.ConsulMeta } // NOTE: The path value in the below line is the path to the webhook. @@ -61,7 +60,7 @@ func (v *ProxyDefaultsWebhook) Handle(ctx context.Context, req admission.Request } } - if err := proxyDefaults.Validate(v.EnableConsulNamespaces); err != nil { + if err := proxyDefaults.Validate(v.ConsulMeta); err != nil { return admission.Errored(http.StatusBadRequest, err) } return admission.Allowed(fmt.Sprintf("valid %s request", proxyDefaults.KubeKind())) diff --git a/control-plane/api/v1alpha1/servicedefaults_types.go b/control-plane/api/v1alpha1/servicedefaults_types.go index ba3127bd4a..fe4b85c755 100644 --- a/control-plane/api/v1alpha1/servicedefaults_types.go +++ b/control-plane/api/v1alpha1/servicedefaults_types.go @@ -3,6 +3,7 @@ package v1alpha1 import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/control-plane/api/common" capi "github.com/hashicorp/consul/api" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -28,6 +29,7 @@ func init() { // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="service-defaults" type ServiceDefaults struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -37,14 +39,14 @@ type ServiceDefaults struct { // +kubebuilder:object:root=true -// ServiceDefaultsList contains a list of ServiceDefaults +// ServiceDefaultsList contains a list of ServiceDefaults. type ServiceDefaultsList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []ServiceDefaults `json:"items"` } -// ServiceDefaultsSpec defines the desired state of ServiceDefaults +// ServiceDefaultsSpec defines the desired state of ServiceDefaults. type ServiceDefaultsSpec struct { // Protocol sets the protocol of the service. This is used by Connect proxies for // things like observability features and to unlock usage of the @@ -89,6 +91,8 @@ type Upstream struct { Name string `json:"name,omitempty"` // Namespace is only accepted within a service-defaults config entry. Namespace string `json:"namespace,omitempty"` + // Partition is only accepted within a service-defaults config entry. + Partition string `json:"partition,omitempty"` // EnvoyListenerJSON is a complete override ("escape hatch") for the upstream's // listener. // Note: This escape hatch is NOT compatible with the discovery chain and @@ -237,7 +241,7 @@ func (in *ServiceDefaults) ToConsul(datacenter string) capi.ConfigEntry { // Validate validates the fields provided in the spec of the ServiceDefaults and // returns an error which lists all invalid fields in the resource spec. -func (in *ServiceDefaults) Validate(namespacesEnabled bool) error { +func (in *ServiceDefaults) Validate(consulMeta common.ConsulMeta) error { var allErrs field.ErrorList path := field.NewPath("spec") @@ -254,7 +258,7 @@ func (in *ServiceDefaults) Validate(namespacesEnabled bool) error { if err := in.Spec.Mode.validate(path.Child("mode")); err != nil { allErrs = append(allErrs, err) } - allErrs = append(allErrs, in.Spec.UpstreamConfig.validate(path.Child("upstreamConfig"))...) + allErrs = append(allErrs, in.Spec.UpstreamConfig.validate(path.Child("upstreamConfig"), consulMeta.PartitionsEnabled)...) allErrs = append(allErrs, in.Spec.Expose.validate(path.Child("expose"))...) if len(allErrs) > 0 { @@ -266,16 +270,16 @@ func (in *ServiceDefaults) Validate(namespacesEnabled bool) error { return nil } -func (in *Upstreams) validate(path *field.Path) field.ErrorList { +func (in *Upstreams) validate(path *field.Path, partitionsEnabled bool) field.ErrorList { if in == nil { return nil } var errs field.ErrorList - if err := in.Defaults.validate(path.Child("defaults"), defaultUpstream); err != nil { + if err := in.Defaults.validate(path.Child("defaults"), defaultUpstream, partitionsEnabled); err != nil { errs = append(errs, err...) } for i, override := range in.Overrides { - if err := override.validate(path.Child("overrides").Index(i), overrideUpstream); err != nil { + if err := override.validate(path.Child("overrides").Index(i), overrideUpstream, partitionsEnabled); err != nil { errs = append(errs, err...) } } @@ -294,7 +298,7 @@ func (in *Upstreams) toConsul() *capi.UpstreamConfiguration { return upstreams } -func (in *Upstream) validate(path *field.Path, kind string) field.ErrorList { +func (in *Upstream) validate(path *field.Path, kind string, partitionsEnabled bool) field.ErrorList { if in == nil { return nil } @@ -309,6 +313,9 @@ func (in *Upstream) validate(path *field.Path, kind string) field.ErrorList { errs = append(errs, field.Invalid(path.Child("name"), in.Name, "upstream.name for an override upstream cannot be \"\"")) } } + if !partitionsEnabled && in.Partition != "" { + errs = append(errs, field.Invalid(path.Child("partition"), in.Partition, "Consul Enterprise Admin Partitions must be enabled to set upstream.partition")) + } if err := in.MeshGateway.validate(path.Child("meshGateway")); err != nil { errs = append(errs, err) } @@ -322,6 +329,7 @@ func (in *Upstream) toConsul() *capi.UpstreamConfig { return &capi.UpstreamConfig{ Name: in.Name, Namespace: in.Namespace, + Partition: in.Partition, EnvoyListenerJSON: in.EnvoyListenerJSON, EnvoyClusterJSON: in.EnvoyClusterJSON, Protocol: in.Protocol, @@ -354,7 +362,7 @@ func (in *PassiveHealthCheck) toConsul() *capi.PassiveHealthCheck { } // DefaultNamespaceFields has no behaviour here as service-defaults have no namespace specific fields. -func (in *ServiceDefaults) DefaultNamespaceFields(_ bool, _ string, _ bool, _ string) { +func (in *ServiceDefaults) DefaultNamespaceFields(_ common.ConsulMeta) { } // MatchesConsul returns true if entry has the same config as this struct. @@ -364,7 +372,7 @@ func (in *ServiceDefaults) MatchesConsul(candidate capi.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty(), + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty(), cmp.Comparer(transparentProxyConfigComparer)) } diff --git a/control-plane/api/v1alpha1/servicedefaults_types_test.go b/control-plane/api/v1alpha1/servicedefaults_types_test.go index d4d0696ae1..8042a5b2b3 100644 --- a/control-plane/api/v1alpha1/servicedefaults_types_test.go +++ b/control-plane/api/v1alpha1/servicedefaults_types_test.go @@ -68,6 +68,7 @@ func TestServiceDefaults_ToConsul(t *testing.T) { Defaults: &Upstream{ Name: "upstream-default", Namespace: "ns", + Partition: "part", EnvoyListenerJSON: `{"key": "value"}`, EnvoyClusterJSON: `{"key": "value"}`, Protocol: "http2", @@ -91,6 +92,7 @@ func TestServiceDefaults_ToConsul(t *testing.T) { { Name: "upstream-override-1", Namespace: "ns", + Partition: "part", EnvoyListenerJSON: `{"key": "value"}`, EnvoyClusterJSON: `{"key": "value"}`, Protocol: "http2", @@ -113,6 +115,7 @@ func TestServiceDefaults_ToConsul(t *testing.T) { { Name: "upstream-default", Namespace: "ns", + Partition: "part", EnvoyListenerJSON: `{"key": "value"}`, EnvoyClusterJSON: `{"key": "value"}`, Protocol: "http2", @@ -169,6 +172,7 @@ func TestServiceDefaults_ToConsul(t *testing.T) { Defaults: &capi.UpstreamConfig{ Name: "upstream-default", Namespace: "ns", + Partition: "part", EnvoyListenerJSON: `{"key": "value"}`, EnvoyClusterJSON: `{"key": "value"}`, Protocol: "http2", @@ -190,6 +194,7 @@ func TestServiceDefaults_ToConsul(t *testing.T) { { Name: "upstream-override-1", Namespace: "ns", + Partition: "part", EnvoyListenerJSON: `{"key": "value"}`, EnvoyClusterJSON: `{"key": "value"}`, Protocol: "http2", @@ -210,6 +215,7 @@ func TestServiceDefaults_ToConsul(t *testing.T) { { Name: "upstream-default", Namespace: "ns", + Partition: "part", EnvoyListenerJSON: `{"key": "value"}`, EnvoyClusterJSON: `{"key": "value"}`, Protocol: "http2", @@ -555,8 +561,9 @@ func TestServiceDefaults_MatchesConsul(t *testing.T) { func TestServiceDefaults_Validate(t *testing.T) { cases := map[string]struct { - input *ServiceDefaults - expectedErrMsg string + input *ServiceDefaults + partitionsEnabled bool + expectedErrMsg string }{ "valid": { input: &ServiceDefaults{ @@ -583,7 +590,7 @@ func TestServiceDefaults_Validate(t *testing.T) { expectedErrMsg: "", }, "protocol": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -591,10 +598,10 @@ func TestServiceDefaults_Validate(t *testing.T) { Protocol: "foo", }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.protocol: Invalid value: "foo": must be one of "tcp", "http", "http2", "grpc"`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.protocol: Invalid value: "foo": must be one of "tcp", "http", "http2", "grpc"`, }, "meshgateway.mode": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -604,10 +611,10 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.meshGateway.mode: Invalid value: "foobar": must be one of "remote", "local", "none", ""`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.meshGateway.mode: Invalid value: "foobar": must be one of "remote", "local", "none", ""`, }, "expose.paths[].protocol": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -622,10 +629,10 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.expose.paths[0].protocol: Invalid value: "invalid-protocol": must be one of "http", "http2"`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.expose.paths[0].protocol: Invalid value: "invalid-protocol": must be one of "http", "http2"`, }, "expose.paths[].path": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -640,10 +647,10 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.expose.paths[0].path: Invalid value: "invalid-path": must begin with a '/'`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.expose.paths[0].path: Invalid value: "invalid-path": must begin with a '/'`, }, "transparentProxy.outboundListenerPort": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -653,10 +660,10 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - "servicedefaults.consul.hashicorp.com \"my-service\" is invalid: spec.transparentProxy.outboundListenerPort: Invalid value: 1000: use the annotation `consul.hashicorp.com/transparent-proxy-outbound-listener-port` to configure the Outbound Listener Port", + expectedErrMsg: "servicedefaults.consul.hashicorp.com \"my-service\" is invalid: spec.transparentProxy.outboundListenerPort: Invalid value: 1000: use the annotation `consul.hashicorp.com/transparent-proxy-outbound-listener-port` to configure the Outbound Listener Port", }, "mode": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -664,10 +671,10 @@ func TestServiceDefaults_Validate(t *testing.T) { Mode: proxyModeRef("transparent"), }, }, - "servicedefaults.consul.hashicorp.com \"my-service\" is invalid: spec.mode: Invalid value: \"transparent\": use the annotation `consul.hashicorp.com/transparent-proxy` to configure the Transparent Proxy Mode", + expectedErrMsg: "servicedefaults.consul.hashicorp.com \"my-service\" is invalid: spec.mode: Invalid value: \"transparent\": use the annotation `consul.hashicorp.com/transparent-proxy` to configure the Transparent Proxy Mode", }, "upstreamConfig.defaults.meshGateway": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -681,10 +688,10 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.defaults.meshGateway.mode: Invalid value: "foo": must be one of "remote", "local", "none", ""`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.defaults.meshGateway.mode: Invalid value: "foo": must be one of "remote", "local", "none", ""`, }, "upstreamConfig.defaults.name": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -696,10 +703,26 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.defaults.name: Invalid value: "foobar": upstream.name for a default upstream must be ""`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.defaults.name: Invalid value: "foobar": upstream.name for a default upstream must be ""`, + }, + "upstreamConfig.defaults.partition": { + input: &ServiceDefaults{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-service", + }, + Spec: ServiceDefaultsSpec{ + UpstreamConfig: &Upstreams{ + Defaults: &Upstream{ + Partition: "upstream", + }, + }, + }, + }, + partitionsEnabled: false, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.defaults.partition: Invalid value: "upstream": Consul Enterprise Admin Partitions must be enabled to set upstream.partition`, }, "upstreamConfig.overrides.meshGateway": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -716,10 +739,10 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.overrides[0].meshGateway.mode: Invalid value: "foo": must be one of "remote", "local", "none", ""`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.overrides[0].meshGateway.mode: Invalid value: "foo": must be one of "remote", "local", "none", ""`, }, "upstreamConfig.overrides.name": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -733,10 +756,28 @@ func TestServiceDefaults_Validate(t *testing.T) { }, }, }, - `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.overrides[0].name: Invalid value: "": upstream.name for an override upstream cannot be ""`, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.overrides[0].name: Invalid value: "": upstream.name for an override upstream cannot be ""`, + }, + "upstreamConfig.overrides.partition": { + input: &ServiceDefaults{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-service", + }, + Spec: ServiceDefaultsSpec{ + UpstreamConfig: &Upstreams{ + Overrides: []*Upstream{ + { + Name: "service", + Partition: "upstream", + }, + }, + }, + }, + }, + expectedErrMsg: `servicedefaults.consul.hashicorp.com "my-service" is invalid: spec.upstreamConfig.overrides[0].partition: Invalid value: "upstream": Consul Enterprise Admin Partitions must be enabled to set upstream.partition`, }, "multi-error": { - &ServiceDefaults{ + input: &ServiceDefaults{ ObjectMeta: metav1.ObjectMeta{ Name: "my-service", }, @@ -759,13 +800,13 @@ func TestServiceDefaults_Validate(t *testing.T) { Mode: proxyModeRef("transparent"), }, }, - "servicedefaults.consul.hashicorp.com \"my-service\" is invalid: [spec.protocol: Invalid value: \"invalid\": must be one of \"tcp\", \"http\", \"http2\", \"grpc\", spec.meshGateway.mode: Invalid value: \"invalid-mode\": must be one of \"remote\", \"local\", \"none\", \"\", spec.transparentProxy.outboundListenerPort: Invalid value: 1000: use the annotation `consul.hashicorp.com/transparent-proxy-outbound-listener-port` to configure the Outbound Listener Port, spec.mode: Invalid value: \"transparent\": use the annotation `consul.hashicorp.com/transparent-proxy` to configure the Transparent Proxy Mode, spec.expose.paths[0].path: Invalid value: \"invalid-path\": must begin with a '/', spec.expose.paths[0].protocol: Invalid value: \"invalid-protocol\": must be one of \"http\", \"http2\"]", + expectedErrMsg: "servicedefaults.consul.hashicorp.com \"my-service\" is invalid: [spec.protocol: Invalid value: \"invalid\": must be one of \"tcp\", \"http\", \"http2\", \"grpc\", spec.meshGateway.mode: Invalid value: \"invalid-mode\": must be one of \"remote\", \"local\", \"none\", \"\", spec.transparentProxy.outboundListenerPort: Invalid value: 1000: use the annotation `consul.hashicorp.com/transparent-proxy-outbound-listener-port` to configure the Outbound Listener Port, spec.mode: Invalid value: \"transparent\": use the annotation `consul.hashicorp.com/transparent-proxy` to configure the Transparent Proxy Mode, spec.expose.paths[0].path: Invalid value: \"invalid-path\": must begin with a '/', spec.expose.paths[0].protocol: Invalid value: \"invalid-protocol\": must be one of \"http\", \"http2\"]", }, } for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(false) + err := testCase.input.Validate(common.ConsulMeta{}) if testCase.expectedErrMsg != "" { require.EqualError(t, err, testCase.expectedErrMsg) } else { diff --git a/control-plane/api/v1alpha1/servicedefaults_webhook.go b/control-plane/api/v1alpha1/servicedefaults_webhook.go index e218a4f323..a196a6d941 100644 --- a/control-plane/api/v1alpha1/servicedefaults_webhook.go +++ b/control-plane/api/v1alpha1/servicedefaults_webhook.go @@ -17,26 +17,8 @@ type ServiceDefaultsWebhook struct { ConsulClient *capi.Client Logger logr.Logger - // EnableConsulNamespaces indicates that a user is running Consul Enterprise - // with version 1.7+ which supports namespaces. - EnableConsulNamespaces bool - - // EnableNSMirroring causes Consul namespaces to be created to match the - // k8s namespace of any config entry custom resource. Config entries will - // be created in the matching Consul namespace. - EnableNSMirroring bool - - // ConsulDestinationNamespace is the namespace in Consul that the config entry created - // in k8s will get mapped into. If the Consul namespace does not already exist, it will - // be created. - ConsulDestinationNamespace string - - // NSMirroringPrefix works in conjunction with Namespace Mirroring. - // It is the prefix added to the Consul namespace to map to a specific. - // k8s namespace. For example, if `mirroringK8SPrefix` is set to "k8s-", a - // service in the k8s `staging` namespace will be registered into the - // `k8s-staging` Consul namespace. - NSMirroringPrefix string + // ConsulMeta contains metadata specific to the Consul installation. + ConsulMeta common.ConsulMeta decoder *admission.Decoder client.Client @@ -57,15 +39,7 @@ func (v *ServiceDefaultsWebhook) Handle(ctx context.Context, req admission.Reque return admission.Errored(http.StatusBadRequest, err) } - return common.ValidateConfigEntry(ctx, - req, - v.Logger, - v, - &svcDefaults, - v.EnableConsulNamespaces, - v.EnableNSMirroring, - v.ConsulDestinationNamespace, - v.NSMirroringPrefix) + return common.ValidateConfigEntry(ctx, req, v.Logger, v, &svcDefaults, v.ConsulMeta) } func (v *ServiceDefaultsWebhook) List(ctx context.Context) ([]common.ConfigEntryResource, error) { diff --git a/control-plane/api/v1alpha1/serviceintentions_types.go b/control-plane/api/v1alpha1/serviceintentions_types.go index 8abfe41ef9..0bab3adc74 100644 --- a/control-plane/api/v1alpha1/serviceintentions_types.go +++ b/control-plane/api/v1alpha1/serviceintentions_types.go @@ -29,6 +29,7 @@ func init() { // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="service-intentions" type ServiceIntentions struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -39,14 +40,14 @@ type ServiceIntentions struct { // +kubebuilder:object:root=true -// ServiceIntentionsList contains a list of ServiceIntentions +// ServiceIntentionsList contains a list of ServiceIntentions. type ServiceIntentionsList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []ServiceIntentions `json:"items"` } -// ServiceIntentionsSpec defines the desired state of ServiceIntentions +// ServiceIntentionsSpec defines the desired state of ServiceIntentions. type ServiceIntentionsSpec struct { // Destination is the intention destination that will have the authorization granted to. Destination Destination `json:"destination,omitempty"` @@ -77,6 +78,8 @@ type SourceIntention struct { Name string `json:"name,omitempty"` // Namespace is the namespace for the Name parameter. Namespace string `json:"namespace,omitempty"` + // Partition is the Admin Partition for the Name parameter. + Partition string `json:"partition,omitempty"` // Action is required for an L4 intention, and should be set to one of // "allow" or "deny" for the action that should be taken if this intention matches a request. Action IntentionAction `json:"action,omitempty"` @@ -233,7 +236,7 @@ func (in *ServiceIntentions) MatchesConsul(candidate api.ConfigEntry) bool { return cmp.Equal( in.ToConsul(""), configEntry, - cmpopts.IgnoreFields(capi.ServiceIntentionsConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), + cmpopts.IgnoreFields(capi.ServiceIntentionsConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreFields(capi.SourceIntention{}, "LegacyID", "LegacyMeta", "LegacyCreateTime", "LegacyUpdateTime", "Precedence", "Type"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty(), @@ -247,7 +250,7 @@ func (in *ServiceIntentions) MatchesConsul(candidate api.ConfigEntry) bool { ) } -func (in *ServiceIntentions) Validate(namespacesEnabled bool) error { +func (in *ServiceIntentions) Validate(consulMeta common.ConsulMeta) error { var errs field.ErrorList path := field.NewPath("spec") if len(in.Spec.Sources) == 0 { @@ -266,7 +269,8 @@ func (in *ServiceIntentions) Validate(namespacesEnabled bool) error { } } - errs = append(errs, in.validateNamespaces(namespacesEnabled)...) + errs = append(errs, in.validateNamespaces(consulMeta.NamespacesEnabled)...) + errs = append(errs, in.validatePartitions(consulMeta.PartitionsEnabled)...) if len(errs) > 0 { return apierrors.NewInvalid( @@ -277,14 +281,14 @@ func (in *ServiceIntentions) Validate(namespacesEnabled bool) error { } // DefaultNamespaceFields sets the namespace field on spec.destination to their default values if namespaces are enabled. -func (in *ServiceIntentions) DefaultNamespaceFields(consulNamespacesEnabled bool, destinationNamespace string, mirroring bool, prefix string) { +func (in *ServiceIntentions) DefaultNamespaceFields(consulMeta common.ConsulMeta) { // If namespaces are enabled we want to set the destination namespace field to it's // default. If namespaces are not enabled (i.e. OSS) we don't set the // namespace fields because this would cause errors // making API calls (because namespace fields can't be set in OSS). - if consulNamespacesEnabled { + if consulMeta.NamespacesEnabled { // Default to the current namespace (i.e. the namespace of the config entry). - namespace := namespaces.ConsulNamespace(in.Namespace, consulNamespacesEnabled, destinationNamespace, mirroring, prefix) + namespace := namespaces.ConsulNamespace(in.Namespace, consulMeta.NamespacesEnabled, consulMeta.DestinationNamespace, consulMeta.Mirroring, consulMeta.Prefix) if in.Spec.Destination.Namespace == "" { in.Spec.Destination.Namespace = namespace } @@ -306,6 +310,7 @@ func (in *SourceIntention) toConsul() *capi.SourceIntention { return &capi.SourceIntention{ Name: in.Name, Namespace: in.Namespace, + Partition: in.Partition, Action: in.Action.toConsul(), Permissions: in.Permissions.toConsul(), Description: in.Description, @@ -450,6 +455,19 @@ func (in *ServiceIntentions) validateNamespaces(namespacesEnabled bool) field.Er return errs } +func (in *ServiceIntentions) validatePartitions(partitionsEnabled bool) field.ErrorList { + var errs field.ErrorList + path := field.NewPath("spec") + if !partitionsEnabled { + for i, source := range in.Spec.Sources { + if source.Partition != "" { + errs = append(errs, field.Invalid(path.Child("sources").Index(i).Child("partition"), source.Partition, `Consul Enterprise Admin Partitions must be enabled to set source.partition`)) + } + } + } + return errs +} + func (in IntentionAction) validate(path *field.Path) *field.Error { actions := []string{"allow", "deny"} if !sliceContains(actions, string(in)) { diff --git a/control-plane/api/v1alpha1/serviceintentions_types_test.go b/control-plane/api/v1alpha1/serviceintentions_types_test.go index 27dfdb41f4..e6fbb4109c 100644 --- a/control-plane/api/v1alpha1/serviceintentions_types_test.go +++ b/control-plane/api/v1alpha1/serviceintentions_types_test.go @@ -51,18 +51,21 @@ func TestServiceIntentions_MatchesConsul(t *testing.T) { { Name: "svc1", Namespace: "test", + Partition: "test", Action: "allow", Description: "allow access from svc1", }, { Name: "*", Namespace: "not-test", + Partition: "not-test", Action: "deny", Description: "disallow access from namespace not-test", }, { Name: "svc-2", Namespace: "bar", + Partition: "bar", Permissions: IntentionPermissions{ { Action: "allow", @@ -101,6 +104,7 @@ func TestServiceIntentions_MatchesConsul(t *testing.T) { { Name: "svc1", Namespace: "test", + Partition: "test", Action: "allow", Precedence: 0, Description: "allow access from svc1", @@ -108,6 +112,7 @@ func TestServiceIntentions_MatchesConsul(t *testing.T) { { Name: "*", Namespace: "not-test", + Partition: "not-test", Action: "deny", Precedence: 1, Description: "disallow access from namespace not-test", @@ -115,6 +120,7 @@ func TestServiceIntentions_MatchesConsul(t *testing.T) { { Name: "svc-2", Namespace: "bar", + Partition: "bar", Permissions: []*capi.IntentionPermission{ { Action: "allow", @@ -249,18 +255,21 @@ func TestServiceIntentions_ToConsul(t *testing.T) { { Name: "svc1", Namespace: "test", + Partition: "test", Action: "allow", Description: "allow access from svc1", }, { Name: "*", Namespace: "not-test", + Partition: "not-test", Action: "deny", Description: "disallow access from namespace not-test", }, { Name: "svc-2", Namespace: "bar", + Partition: "bar", Permissions: IntentionPermissions{ { Action: "allow", @@ -299,18 +308,21 @@ func TestServiceIntentions_ToConsul(t *testing.T) { { Name: "svc1", Namespace: "test", + Partition: "test", Action: "allow", Description: "allow access from svc1", }, { Name: "*", Namespace: "not-test", + Partition: "not-test", Action: "deny", Description: "disallow access from namespace not-test", }, { Name: "svc-2", Namespace: "bar", + Partition: "bar", Permissions: []*capi.IntentionPermission{ { Action: "allow", @@ -514,39 +526,44 @@ func TestServiceIntentions_ObjectMeta(t *testing.T) { // Test defaulting behavior when namespaces are enabled as well as disabled. func TestServiceIntentions_DefaultNamespaceFields(t *testing.T) { namespaceConfig := map[string]struct { - enabled bool - destinationNamespace string - mirroring bool - prefix string - expectedDestination string + consulMeta common.ConsulMeta + expectedDestination string }{ "disabled": { - enabled: false, - destinationNamespace: "", - mirroring: false, - prefix: "", - expectedDestination: "", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: false, + DestinationNamespace: "", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "", }, "destinationNS": { - enabled: true, - destinationNamespace: "foo", - mirroring: false, - prefix: "", - expectedDestination: "foo", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "foo", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "foo", }, "mirroringEnabledWithoutPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "", - expectedDestination: "bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "", + }, + expectedDestination: "bar", }, "mirroringWithPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "ns-", - expectedDestination: "ns-bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "ns-", + }, + expectedDestination: "ns-bar", }, } @@ -575,7 +592,7 @@ func TestServiceIntentions_DefaultNamespaceFields(t *testing.T) { }, }, } - input.DefaultNamespaceFields(s.enabled, s.destinationNamespace, s.mirroring, s.prefix) + input.DefaultNamespaceFields(s.consulMeta) require.True(t, cmp.Equal(input, output)) }) } @@ -585,8 +602,64 @@ func TestServiceIntentions_Validate(t *testing.T) { cases := map[string]struct { input *ServiceIntentions namespacesEnabled bool + partitionsEnabled bool expectedErrMsgs []string }{ + "partitions enabled: valid": { + input: &ServiceIntentions{ + ObjectMeta: metav1.ObjectMeta{ + Name: "does-not-matter", + }, + Spec: ServiceIntentionsSpec{ + Destination: Destination{ + Name: "dest-service", + Namespace: "namespace", + }, + Sources: SourceIntentions{ + { + Name: "web", + Namespace: "web", + Partition: "web", + Action: "allow", + }, + { + Name: "db", + Namespace: "db", + Partition: "db", + Action: "deny", + }, + { + Name: "bar", + Namespace: "bar", + Partition: "bar", + Permissions: IntentionPermissions{ + { + Action: "allow", + HTTP: &IntentionHTTPPermission{ + PathExact: "/foo", + Header: IntentionHTTPHeaderPermissions{ + { + Name: "header", + Present: true, + Invert: true, + }, + }, + Methods: []string{ + "GET", + "PUT", + }, + }, + }, + }, + Description: "an L7 config", + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: true, + expectedErrMsgs: nil, + }, "namespaces enabled: valid": { input: &ServiceIntentions{ ObjectMeta: metav1.ObjectMeta{ @@ -636,6 +709,7 @@ func TestServiceIntentions_Validate(t *testing.T) { }, }, namespacesEnabled: true, + partitionsEnabled: false, expectedErrMsgs: nil, }, "namespaces disabled: valid": { @@ -683,6 +757,7 @@ func TestServiceIntentions_Validate(t *testing.T) { }, }, namespacesEnabled: false, + partitionsEnabled: false, expectedErrMsgs: nil, }, "no sources": { @@ -1162,10 +1237,84 @@ func TestServiceIntentions_Validate(t *testing.T) { `spec.sources[2].namespace: Invalid value: "namespace-d": Consul Enterprise namespaces must be enabled to set source.namespace`, }, }, + "partitions disabled: single source partition specified": { + input: &ServiceIntentions{ + ObjectMeta: metav1.ObjectMeta{ + Name: "does-not-matter", + }, + Spec: ServiceIntentionsSpec{ + Destination: Destination{ + Name: "dest-service", + Namespace: "namespace-a", + }, + Sources: SourceIntentions{ + { + Name: "web", + Action: "allow", + Namespace: "namespace-b", + Partition: "partition-other", + }, + { + Name: "db", + Action: "deny", + Namespace: "namespace-c", + }, + { + Name: "bar", + Namespace: "namespace-d", + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{ + `spec.sources[0].partition: Invalid value: "partition-other": Consul Enterprise Admin Partitions must be enabled to set source.partition`, + }, + }, + "partitions disabled: multiple source partition specified": { + input: &ServiceIntentions{ + ObjectMeta: metav1.ObjectMeta{ + Name: "does-not-matter", + }, + Spec: ServiceIntentionsSpec{ + Destination: Destination{ + Name: "dest-service", + Namespace: "namespace-a", + }, + Sources: SourceIntentions{ + { + Name: "web", + Action: "allow", + Namespace: "namespace-b", + Partition: "partition-other", + }, + { + Name: "db", + Action: "deny", + Namespace: "namespace-c", + Partition: "partition-first", + }, + { + Name: "bar", + Namespace: "namespace-d", + Partition: "partition-foo", + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{ + `spec.sources[0].partition: Invalid value: "partition-other": Consul Enterprise Admin Partitions must be enabled to set source.partition`, + `spec.sources[1].partition: Invalid value: "partition-first": Consul Enterprise Admin Partitions must be enabled to set source.partition`, + `spec.sources[2].partition: Invalid value: "partition-foo": Consul Enterprise Admin Partitions must be enabled to set source.partition`, + }, + }, } for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(testCase.namespacesEnabled) + err := testCase.input.Validate(common.ConsulMeta{NamespacesEnabled: testCase.namespacesEnabled, PartitionsEnabled: testCase.partitionsEnabled}) if len(testCase.expectedErrMsgs) != 0 { require.Error(t, err) for _, s := range testCase.expectedErrMsgs { diff --git a/control-plane/api/v1alpha1/serviceintentions_webhook.go b/control-plane/api/v1alpha1/serviceintentions_webhook.go index 8e5287a457..0287ddfeb8 100644 --- a/control-plane/api/v1alpha1/serviceintentions_webhook.go +++ b/control-plane/api/v1alpha1/serviceintentions_webhook.go @@ -18,13 +18,10 @@ import ( type ServiceIntentionsWebhook struct { client.Client - ConsulClient *capi.Client - Logger logr.Logger - decoder *admission.Decoder - EnableConsulNamespaces bool - EnableNSMirroring bool - ConsulDestinationNamespace string - NSMirroringPrefix string + ConsulClient *capi.Client + Logger logr.Logger + decoder *admission.Decoder + ConsulMeta common.ConsulMeta } // NOTE: The path value in the below line is the path to the webhook. @@ -44,12 +41,12 @@ func (v *ServiceIntentionsWebhook) Handle(ctx context.Context, req admission.Req return admission.Errored(http.StatusBadRequest, err) } - defaultingPatches, err := common.DefaultingPatches(&svcIntentions, v.EnableConsulNamespaces, v.EnableNSMirroring, v.ConsulDestinationNamespace, v.NSMirroringPrefix) + defaultingPatches, err := common.DefaultingPatches(&svcIntentions, v.ConsulMeta) if err != nil { return admission.Errored(http.StatusInternalServerError, err) } - singleConsulDestNS := !(v.EnableConsulNamespaces && v.EnableNSMirroring) + singleConsulDestNS := !(v.ConsulMeta.NamespacesEnabled && v.ConsulMeta.Mirroring) if req.Operation == admissionv1.Create { v.Logger.Info("validate create", "name", svcIntentions.KubernetesName()) @@ -89,7 +86,7 @@ func (v *ServiceIntentionsWebhook) Handle(ctx context.Context, req admission.Req } // ServiceIntentions are invalid if destination namespaces or source namespaces are set when Consul Namespaces are not enabled. - if err := svcIntentions.Validate(v.EnableConsulNamespaces); err != nil { + if err := svcIntentions.Validate(v.ConsulMeta); err != nil { return admission.Errored(http.StatusBadRequest, err) } diff --git a/control-plane/api/v1alpha1/serviceintentions_webhook_test.go b/control-plane/api/v1alpha1/serviceintentions_webhook_test.go index aed9d0b6e2..8c1ba98fd7 100644 --- a/control-plane/api/v1alpha1/serviceintentions_webhook_test.go +++ b/control-plane/api/v1alpha1/serviceintentions_webhook_test.go @@ -7,6 +7,7 @@ import ( "testing" logrtest "github.com/go-logr/logr/testing" + "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/stretchr/testify/require" "gomodules.xyz/jsonpatch/v2" admissionv1 "k8s.io/api/admission/v1" @@ -248,12 +249,14 @@ func TestHandle_ServiceIntentions_Create(t *testing.T) { require.NoError(t, err) validator := &ServiceIntentionsWebhook{ - Client: client, - ConsulClient: nil, - Logger: logrtest.TestLogger{T: t}, - decoder: decoder, - EnableConsulNamespaces: true, - EnableNSMirroring: c.mirror, + Client: client, + ConsulClient: nil, + Logger: logrtest.TestLogger{T: t}, + decoder: decoder, + ConsulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + Mirroring: c.mirror, + }, } response := validator.Handle(ctx, admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ @@ -436,12 +439,14 @@ func TestHandle_ServiceIntentions_Update(t *testing.T) { require.NoError(t, err) validator := &ServiceIntentionsWebhook{ - Client: client, - ConsulClient: nil, - Logger: logrtest.TestLogger{T: t}, - decoder: decoder, - EnableConsulNamespaces: true, - EnableNSMirroring: c.mirror, + Client: client, + ConsulClient: nil, + Logger: logrtest.TestLogger{T: t}, + decoder: decoder, + ConsulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + Mirroring: c.mirror, + }, } response := validator.Handle(ctx, admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ @@ -595,12 +600,14 @@ func TestHandle_ServiceIntentions_Patches(t *testing.T) { require.NoError(t, err) validator := &ServiceIntentionsWebhook{ - Client: client, - ConsulClient: nil, - Logger: logrtest.TestLogger{T: t}, - decoder: decoder, - EnableConsulNamespaces: namespacesEnabled, - EnableNSMirroring: true, + Client: client, + ConsulClient: nil, + Logger: logrtest.TestLogger{T: t}, + decoder: decoder, + ConsulMeta: common.ConsulMeta{ + NamespacesEnabled: namespacesEnabled, + Mirroring: true, + }, } response := validator.Handle(ctx, admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ diff --git a/control-plane/api/v1alpha1/serviceresolver_types.go b/control-plane/api/v1alpha1/serviceresolver_types.go index d606cfdf30..2d5fc1e8c8 100644 --- a/control-plane/api/v1alpha1/serviceresolver_types.go +++ b/control-plane/api/v1alpha1/serviceresolver_types.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/control-plane/api/common" capi "github.com/hashicorp/consul/api" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -26,6 +27,7 @@ func init() { // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="service-resolver" type ServiceResolver struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -35,14 +37,14 @@ type ServiceResolver struct { // +kubebuilder:object:root=true -// ServiceResolverList contains a list of ServiceResolver +// ServiceResolverList contains a list of ServiceResolver. type ServiceResolverList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []ServiceResolver `json:"items"` } -// ServiceResolverSpec defines the desired state of ServiceResolver +// ServiceResolverSpec defines the desired state of ServiceResolver. type ServiceResolverSpec struct { // DefaultSubset is the subset to use when no explicit subset is requested. // If empty the unnamed subset is used. @@ -80,9 +82,12 @@ type ServiceResolverRedirect struct { // of one defined as that service's DefaultSubset If empty the default // subset is used. ServiceSubset string `json:"serviceSubset,omitempty"` - // Namespace is the namespace to resolve the service from instead of the - // current one. + // Namespace is the Consul namespace to resolve the service from instead of + // the current namespace. If empty the current namespace is assumed. Namespace string `json:"namespace,omitempty"` + // Partition is the Consul partition to resolve the service from instead of + // the current partition. If empty the current partition is assumed. + Partition string `json:"partition,omitempty"` // Datacenter is the datacenter to resolve the service from instead of the // current one. Datacenter string `json:"datacenter,omitempty"` @@ -281,14 +286,14 @@ func (in *ServiceResolver) MatchesConsul(candidate capi.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceResolverConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceResolverConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) } func (in *ServiceResolver) ConsulGlobalResource() bool { return false } -func (in *ServiceResolver) Validate(namespacesEnabled bool) error { +func (in *ServiceResolver) Validate(consulMeta common.ConsulMeta) error { var errs field.ErrorList path := field.NewPath("spec") @@ -300,7 +305,7 @@ func (in *ServiceResolver) Validate(namespacesEnabled bool) error { errs = append(errs, in.Spec.LoadBalancer.validate(path.Child("loadBalancer"))...) - errs = append(errs, in.validateNamespaces(namespacesEnabled)...) + errs = append(errs, in.validateEnterprise(consulMeta)...) if len(errs) > 0 { return apierrors.NewInvalid( @@ -312,7 +317,7 @@ func (in *ServiceResolver) Validate(namespacesEnabled bool) error { // DefaultNamespaceFields has no behaviour here as service-resolver have namespace fields // that do not default. -func (in *ServiceResolver) DefaultNamespaceFields(_ bool, _ string, _ bool, _ string) { +func (in *ServiceResolver) DefaultNamespaceFields(_ common.ConsulMeta) { } func (in ServiceResolverSubsetMap) toConsul() map[string]capi.ServiceResolverSubset { @@ -434,10 +439,10 @@ func (in *CookieConfig) validate(path *field.Path) *field.Error { return nil } -func (in *ServiceResolver) validateNamespaces(namespacesEnabled bool) field.ErrorList { +func (in *ServiceResolver) validateEnterprise(consulMeta common.ConsulMeta) field.ErrorList { var errs field.ErrorList path := field.NewPath("spec") - if !namespacesEnabled { + if !consulMeta.NamespacesEnabled { if in.Spec.Redirect != nil { if in.Spec.Redirect.Namespace != "" { errs = append(errs, field.Invalid(path.Child("redirect").Child("namespace"), in.Spec.Redirect.Namespace, `Consul Enterprise namespaces must be enabled to set redirect.namespace`)) @@ -448,7 +453,13 @@ func (in *ServiceResolver) validateNamespaces(namespacesEnabled bool) field.Erro errs = append(errs, field.Invalid(path.Child("failover").Key(k).Child("namespace"), v.Namespace, `Consul Enterprise namespaces must be enabled to set failover.namespace`)) } } - + } + if !consulMeta.PartitionsEnabled { + if in.Spec.Redirect != nil { + if in.Spec.Redirect.Partition != "" { + errs = append(errs, field.Invalid(path.Child("redirect").Child("partition"), in.Spec.Redirect.Partition, `Consul Enterprise partitions must be enabled to set redirect.partition`)) + } + } } return errs } diff --git a/control-plane/api/v1alpha1/serviceresolver_types_test.go b/control-plane/api/v1alpha1/serviceresolver_types_test.go index fed0e89d1e..44b838cc50 100644 --- a/control-plane/api/v1alpha1/serviceresolver_types_test.go +++ b/control-plane/api/v1alpha1/serviceresolver_types_test.go @@ -455,6 +455,7 @@ func TestServiceResolver_Validate(t *testing.T) { cases := map[string]struct { input *ServiceResolver namespacesEnabled bool + partitionsEnabled bool expectedErrMsgs []string }{ "namespaces enabled: valid": { @@ -476,6 +477,7 @@ func TestServiceResolver_Validate(t *testing.T) { }, }, namespacesEnabled: true, + partitionsEnabled: false, expectedErrMsgs: nil, }, "namespaces disabled: valid": { @@ -495,6 +497,50 @@ func TestServiceResolver_Validate(t *testing.T) { }, }, namespacesEnabled: false, + partitionsEnabled: false, + expectedErrMsgs: nil, + }, + "partitions enabled: valid": { + input: &ServiceResolver{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceResolverSpec{ + Redirect: &ServiceResolverRedirect{ + Service: "bar", + Namespace: "namespace-a", + Partition: "other", + }, + Failover: map[string]ServiceResolverFailover{ + "failA": { + Service: "baz", + Namespace: "namespace-b", + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: true, + expectedErrMsgs: nil, + }, + "partitions disabled: valid": { + input: &ServiceResolver{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceResolverSpec{ + Redirect: &ServiceResolverRedirect{ + Service: "bar", + }, + Failover: map[string]ServiceResolverFailover{ + "failA": { + Service: "baz", + }, + }, + }, + }, + namespacesEnabled: false, + partitionsEnabled: false, expectedErrMsgs: nil, }, "failover service, servicesubset, namespace, datacenters empty": { @@ -662,6 +708,24 @@ func TestServiceResolver_Validate(t *testing.T) { "serviceresolver.consul.hashicorp.com \"foo\" is invalid: spec.redirect.namespace: Invalid value: \"namespace-a\": Consul Enterprise namespaces must be enabled to set redirect.namespace", }, }, + "partitions disabled: redirect partition specified": { + input: &ServiceResolver{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceResolverSpec{ + Redirect: &ServiceResolverRedirect{ + Namespace: "namespace-a", + Partition: "other", + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{ + "serviceresolver.consul.hashicorp.com \"foo\" is invalid: spec.redirect.partition: Invalid value: \"other\": Consul Enterprise partitions must be enabled to set redirect.partition", + }, + }, "namespaces disabled: single failover namespace specified": { input: &ServiceResolver{ ObjectMeta: metav1.ObjectMeta{ @@ -705,7 +769,7 @@ func TestServiceResolver_Validate(t *testing.T) { } for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(testCase.namespacesEnabled) + err := testCase.input.Validate(common.ConsulMeta{NamespacesEnabled: testCase.namespacesEnabled, PartitionsEnabled: testCase.partitionsEnabled}) if len(testCase.expectedErrMsgs) != 0 { require.Error(t, err) for _, s := range testCase.expectedErrMsgs { diff --git a/control-plane/api/v1alpha1/serviceresolver_webhook.go b/control-plane/api/v1alpha1/serviceresolver_webhook.go index 602d0a5b6e..1af2fa0383 100644 --- a/control-plane/api/v1alpha1/serviceresolver_webhook.go +++ b/control-plane/api/v1alpha1/serviceresolver_webhook.go @@ -17,26 +17,8 @@ type ServiceResolverWebhook struct { ConsulClient *capi.Client Logger logr.Logger - // EnableConsulNamespaces indicates that a user is running Consul Enterprise - // with version 1.7+ which supports namespaces. - EnableConsulNamespaces bool - - // EnableNSMirroring causes Consul namespaces to be created to match the - // k8s namespace of any config entry custom resource. Config entries will - // be created in the matching Consul namespace. - EnableNSMirroring bool - - // ConsulDestinationNamespace is the namespace in Consul that the config entry created - // in k8s will get mapped into. If the Consul namespace does not already exist, it will - // be created. - ConsulDestinationNamespace string - - // NSMirroringPrefix works in conjunction with Namespace Mirroring. - // It is the prefix added to the Consul namespace to map to a specific. - // k8s namespace. For example, if `mirroringK8SPrefix` is set to "k8s-", a - // service in the k8s `staging` namespace will be registered into the - // `k8s-staging` Consul namespace. - NSMirroringPrefix string + // ConsulMeta contains metadata specific to the Consul installation. + ConsulMeta common.ConsulMeta decoder *admission.Decoder client.Client @@ -57,15 +39,7 @@ func (v *ServiceResolverWebhook) Handle(ctx context.Context, req admission.Reque return admission.Errored(http.StatusBadRequest, err) } - return common.ValidateConfigEntry(ctx, - req, - v.Logger, - v, - &svcResolver, - v.EnableConsulNamespaces, - v.EnableNSMirroring, - v.ConsulDestinationNamespace, - v.NSMirroringPrefix) + return common.ValidateConfigEntry(ctx, req, v.Logger, v, &svcResolver, v.ConsulMeta) } func (v *ServiceResolverWebhook) List(ctx context.Context) ([]common.ConfigEntryResource, error) { diff --git a/control-plane/api/v1alpha1/servicerouter_types.go b/control-plane/api/v1alpha1/servicerouter_types.go index a1a86d7b80..9f8f7fc3fd 100644 --- a/control-plane/api/v1alpha1/servicerouter_types.go +++ b/control-plane/api/v1alpha1/servicerouter_types.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/hashicorp/consul-k8s/control-plane/namespaces" capi "github.com/hashicorp/consul/api" corev1 "k8s.io/api/core/v1" @@ -29,6 +30,7 @@ const ( // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="service-router" type ServiceRouter struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -39,14 +41,14 @@ type ServiceRouter struct { // +kubebuilder:object:root=true -// ServiceRouterList contains a list of ServiceRouter +// ServiceRouterList contains a list of ServiceRouter. type ServiceRouterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []ServiceRouter `json:"items"` } -// ServiceRouterSpec defines the desired state of ServiceRouter +// ServiceRouterSpec defines the desired state of ServiceRouter. type ServiceRouterSpec struct { // Routes are the list of routes to consider when processing L7 requests. // The first route to match in the list is terminal and stops further @@ -127,6 +129,9 @@ type ServiceRouteDestination struct { // Namespace is the Consul namespace to resolve the service from instead of // the current namespace. If empty the current namespace is assumed. Namespace string `json:"namespace,omitempty"` + // Partition is the Consul partition to resolve the service from instead of + // the current partition. If empty the current partition is assumed. + Partition string `json:"partition,omitempty"` // PrefixRewrite defines how to rewrite the HTTP request path before proxying // it to its final destination. // This requires that either match.http.pathPrefix or match.http.pathExact @@ -141,6 +146,9 @@ type ServiceRouteDestination struct { RetryOnConnectFailure bool `json:"retryOnConnectFailure,omitempty"` // RetryOnStatusCodes is a flat list of http response status codes that are eligible for retry. RetryOnStatusCodes []uint32 `json:"retryOnStatusCodes,omitempty"` + // Allow HTTP header manipulation to be configured. + RequestHeaders *HTTPHeaderModifiers `json:"requestHeaders,omitempty"` + ResponseHeaders *HTTPHeaderModifiers `json:"responseHeaders,omitempty"` } func (in *ServiceRouter) ConsulMirroringNS() string { @@ -240,17 +248,17 @@ func (in *ServiceRouter) MatchesConsul(candidate capi.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceRouterConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceRouterConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) } -func (in *ServiceRouter) Validate(namespacesEnabled bool) error { +func (in *ServiceRouter) Validate(consulMeta common.ConsulMeta) error { var errs field.ErrorList path := field.NewPath("spec") for i, r := range in.Spec.Routes { errs = append(errs, r.validate(path.Child("routes").Index(i))...) } - errs = append(errs, in.validateNamespaces(namespacesEnabled)...) + errs = append(errs, in.validateEnterprise(consulMeta)...) if len(errs) > 0 { return apierrors.NewInvalid( @@ -261,14 +269,14 @@ func (in *ServiceRouter) Validate(namespacesEnabled bool) error { } // DefaultNamespaceFields sets the namespace field on spec.routes[].destination to their default values if namespaces are enabled. -func (in *ServiceRouter) DefaultNamespaceFields(consulNamespacesEnabled bool, destinationNamespace string, mirroring bool, prefix string) { +func (in *ServiceRouter) DefaultNamespaceFields(consulMeta common.ConsulMeta) { // If namespaces are enabled we want to set the namespace fields to their // defaults. If namespaces are not enabled (i.e. OSS) we don't set the // namespace fields because this would cause errors // making API calls (because namespace fields can't be set in OSS). - if consulNamespacesEnabled { + if consulMeta.NamespacesEnabled { // Default to the current namespace (i.e. the namespace of the config entry). - namespace := namespaces.ConsulNamespace(in.Namespace, consulNamespacesEnabled, destinationNamespace, mirroring, prefix) + namespace := namespaces.ConsulNamespace(in.Namespace, consulMeta.NamespacesEnabled, consulMeta.DestinationNamespace, consulMeta.Mirroring, consulMeta.Prefix) for i, r := range in.Spec.Routes { if r.Destination != nil { if r.Destination.Namespace == "" { @@ -324,18 +332,21 @@ func (in *ServiceRouteDestination) toConsul() *capi.ServiceRouteDestination { Service: in.Service, ServiceSubset: in.ServiceSubset, Namespace: in.Namespace, + Partition: in.Partition, PrefixRewrite: in.PrefixRewrite, RequestTimeout: in.RequestTimeout.Duration, NumRetries: in.NumRetries, RetryOnConnectFailure: in.RetryOnConnectFailure, RetryOnStatusCodes: in.RetryOnStatusCodes, + RequestHeaders: in.RequestHeaders.toConsul(), + ResponseHeaders: in.ResponseHeaders.toConsul(), } } -func (in *ServiceRouter) validateNamespaces(namespacesEnabled bool) field.ErrorList { +func (in *ServiceRouter) validateEnterprise(consulMeta common.ConsulMeta) field.ErrorList { var errs field.ErrorList path := field.NewPath("spec") - if !namespacesEnabled { + if !consulMeta.NamespacesEnabled { for i, r := range in.Spec.Routes { if r.Destination != nil { if r.Destination.Namespace != "" { @@ -344,6 +355,15 @@ func (in *ServiceRouter) validateNamespaces(namespacesEnabled bool) field.ErrorL } } } + if !consulMeta.PartitionsEnabled { + for i, r := range in.Spec.Routes { + if r.Destination != nil { + if r.Destination.Partition != "" { + errs = append(errs, field.Invalid(path.Child("routes").Index(i).Child("destination").Child("partition"), r.Destination.Partition, `Consul Enterprise partitions must be enabled to set destination.partition`)) + } + } + } + } return errs } diff --git a/control-plane/api/v1alpha1/servicerouter_types_test.go b/control-plane/api/v1alpha1/servicerouter_types_test.go index 2a7ef7c50c..eb0568db81 100644 --- a/control-plane/api/v1alpha1/servicerouter_types_test.go +++ b/control-plane/api/v1alpha1/servicerouter_types_test.go @@ -83,6 +83,36 @@ func TestServiceRouter_MatchesConsul(t *testing.T) { NumRetries: 1, RetryOnConnectFailure: true, RetryOnStatusCodes: []uint32{500, 400}, + RequestHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, }, @@ -129,6 +159,36 @@ func TestServiceRouter_MatchesConsul(t *testing.T) { NumRetries: 1, RetryOnConnectFailure: true, RetryOnStatusCodes: []uint32{500, 400}, + RequestHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, }, @@ -224,6 +284,36 @@ func TestServiceRouter_ToConsul(t *testing.T) { NumRetries: 1, RetryOnConnectFailure: true, RetryOnStatusCodes: []uint32{500, 400}, + RequestHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, }, @@ -270,6 +360,36 @@ func TestServiceRouter_ToConsul(t *testing.T) { NumRetries: 1, RetryOnConnectFailure: true, RetryOnStatusCodes: []uint32{500, 400}, + RequestHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, }, @@ -401,6 +521,7 @@ func TestServiceRouter_Validate(t *testing.T) { cases := map[string]struct { input *ServiceRouter namespacesEnabled bool + partitionsEnabled bool expectedErrMsgs []string }{ "namespaces enabled: valid": { @@ -450,6 +571,56 @@ func TestServiceRouter_Validate(t *testing.T) { namespacesEnabled: false, expectedErrMsgs: nil, }, + "partitions enabled: valid": { + input: &ServiceRouter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceRouterSpec{ + Routes: []ServiceRoute{ + { + Match: &ServiceRouteMatch{ + HTTP: &ServiceRouteHTTPMatch{ + PathPrefix: "/admin", + }, + }, + Destination: &ServiceRouteDestination{ + Service: "destA", + Namespace: "namespace-a", + Partition: "other", + }, + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: true, + expectedErrMsgs: nil, + }, + "partitions disabled: valid": { + input: &ServiceRouter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceRouterSpec{ + Routes: []ServiceRoute{ + { + Match: &ServiceRouteMatch{ + HTTP: &ServiceRouteHTTPMatch{ + PathPrefix: "/admin", + }, + }, + Destination: &ServiceRouteDestination{ + Service: "destA", + }, + }, + }, + }, + }, + namespacesEnabled: false, + partitionsEnabled: false, + expectedErrMsgs: nil, + }, "http match queryParam": { input: &ServiceRouter{ ObjectMeta: metav1.ObjectMeta{ @@ -592,10 +763,61 @@ func TestServiceRouter_Validate(t *testing.T) { "spec.routes[1].destination.namespace: Invalid value: \"namespace-b\": Consul Enterprise namespaces must be enabled to set destination.namespace", }, }, + "partitions disabled: single destination partition specified": { + input: &ServiceRouter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceRouterSpec{ + Routes: []ServiceRoute{ + { + Destination: &ServiceRouteDestination{ + Namespace: "namespace-a", + Partition: "partition-a", + }, + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{ + "servicerouter.consul.hashicorp.com \"foo\" is invalid: spec.routes[0].destination.partition: Invalid value: \"partition-a\": Consul Enterprise partitions must be enabled to set destination.partition", + }, + }, + "partitions disabled: multiple destination partitions specified": { + input: &ServiceRouter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceRouterSpec{ + Routes: []ServiceRoute{ + { + Destination: &ServiceRouteDestination{ + Namespace: "namespace-a", + Partition: "partition-a", + }, + }, + { + Destination: &ServiceRouteDestination{ + Namespace: "namespace-b", + Partition: "partition-b", + }, + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{ + "spec.routes[0].destination.partition: Invalid value: \"partition-a\": Consul Enterprise partitions must be enabled to set destination.partition", + "spec.routes[1].destination.partition: Invalid value: \"partition-b\": Consul Enterprise partitions must be enabled to set destination.partition", + }, + }, } for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(testCase.namespacesEnabled) + err := testCase.input.Validate(common.ConsulMeta{NamespacesEnabled: testCase.namespacesEnabled, PartitionsEnabled: testCase.partitionsEnabled}) if len(testCase.expectedErrMsgs) != 0 { require.Error(t, err) for _, s := range testCase.expectedErrMsgs { @@ -611,39 +833,44 @@ func TestServiceRouter_Validate(t *testing.T) { // Test defaulting behavior when namespaces are enabled as well as disabled. func TestServiceRouter_DefaultNamespaceFields(t *testing.T) { namespaceConfig := map[string]struct { - enabled bool - destinationNamespace string - mirroring bool - prefix string - expectedDestination string + consulMeta common.ConsulMeta + expectedDestination string }{ "disabled": { - enabled: false, - destinationNamespace: "", - mirroring: false, - prefix: "", - expectedDestination: "", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: false, + DestinationNamespace: "", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "", }, "destinationNS": { - enabled: true, - destinationNamespace: "foo", - mirroring: false, - prefix: "", - expectedDestination: "foo", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "foo", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "foo", }, "mirroringEnabledWithoutPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "", - expectedDestination: "bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "", + }, + expectedDestination: "bar", }, "mirroringWithPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "ns-", - expectedDestination: "ns-bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "ns-", + }, + expectedDestination: "ns-bar", }, } @@ -712,7 +939,7 @@ func TestServiceRouter_DefaultNamespaceFields(t *testing.T) { }, }, } - input.DefaultNamespaceFields(s.enabled, s.destinationNamespace, s.mirroring, s.prefix) + input.DefaultNamespaceFields(s.consulMeta) require.True(t, cmp.Equal(input, output)) }) } diff --git a/control-plane/api/v1alpha1/servicerouter_webhook.go b/control-plane/api/v1alpha1/servicerouter_webhook.go index 48af57d7bb..03644432e6 100644 --- a/control-plane/api/v1alpha1/servicerouter_webhook.go +++ b/control-plane/api/v1alpha1/servicerouter_webhook.go @@ -17,26 +17,8 @@ type ServiceRouterWebhook struct { ConsulClient *capi.Client Logger logr.Logger - // EnableConsulNamespaces indicates that a user is running Consul Enterprise - // with version 1.7+ which supports namespaces. - EnableConsulNamespaces bool - - // EnableNSMirroring causes Consul namespaces to be created to match the - // k8s namespace of any config entry custom resource. Config entries will - // be created in the matching Consul namespace. - EnableNSMirroring bool - - // ConsulDestinationNamespace is the namespace in Consul that the config entry created - // in k8s will get mapped into. If the Consul namespace does not already exist, it will - // be created. - ConsulDestinationNamespace string - - // NSMirroringPrefix works in conjunction with Namespace Mirroring. - // It is the prefix added to the Consul namespace to map to a specific. - // k8s namespace. For example, if `mirroringK8SPrefix` is set to "k8s-", a - // service in the k8s `staging` namespace will be registered into the - // `k8s-staging` Consul namespace. - NSMirroringPrefix string + // ConsulMeta contains metadata specific to the Consul installation. + ConsulMeta common.ConsulMeta decoder *admission.Decoder client.Client @@ -57,15 +39,7 @@ func (v *ServiceRouterWebhook) Handle(ctx context.Context, req admission.Request return admission.Errored(http.StatusBadRequest, err) } - return common.ValidateConfigEntry(ctx, - req, - v.Logger, - v, - &svcRouter, - v.EnableConsulNamespaces, - v.EnableNSMirroring, - v.ConsulDestinationNamespace, - v.NSMirroringPrefix) + return common.ValidateConfigEntry(ctx, req, v.Logger, v, &svcRouter, v.ConsulMeta) } func (v *ServiceRouterWebhook) List(ctx context.Context) ([]common.ConfigEntryResource, error) { diff --git a/control-plane/api/v1alpha1/servicesplitter_types.go b/control-plane/api/v1alpha1/servicesplitter_types.go index 45e0e7cb66..b61b1a320b 100644 --- a/control-plane/api/v1alpha1/servicesplitter_types.go +++ b/control-plane/api/v1alpha1/servicesplitter_types.go @@ -26,6 +26,7 @@ func init() { // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="service-splitter" type ServiceSplitter struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -36,7 +37,7 @@ type ServiceSplitter struct { // +kubebuilder:object:root=true -// ServiceSplitterList contains a list of ServiceSplitter +// ServiceSplitterList contains a list of ServiceSplitter. type ServiceSplitterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` @@ -45,7 +46,7 @@ type ServiceSplitterList struct { type ServiceSplits []ServiceSplit -// ServiceSplitterSpec defines the desired state of ServiceSplitter +// ServiceSplitterSpec defines the desired state of ServiceSplitter. type ServiceSplitterSpec struct { // Splits defines how much traffic to send to which set of service instances during a traffic split. // The sum of weights across all splits must add up to 100. @@ -61,9 +62,15 @@ type ServiceSplit struct { // ServiceSubset is a named subset of the given service to resolve instead of one defined // as that service's DefaultSubset. If empty the default subset is used. ServiceSubset string `json:"serviceSubset,omitempty"` - // The namespace to resolve the service from instead of the current namespace. - // If empty the current namespace is assumed. + // Namespace is the Consul namespace to resolve the service from instead of + // the current namespace. If empty the current namespace is assumed. Namespace string `json:"namespace,omitempty"` + // Partition is the Consul partition to resolve the service from instead of + // the current partition. If empty the current partition is assumed. + Partition string `json:"partition,omitempty"` + // Allow HTTP header manipulation to be configured. + RequestHeaders *HTTPHeaderModifiers `json:"requestHeaders,omitempty"` + ResponseHeaders *HTTPHeaderModifiers `json:"responseHeaders,omitempty"` } func (in *ServiceSplitter) GetObjectMeta() metav1.ObjectMeta { @@ -159,13 +166,13 @@ func (in *ServiceSplitter) MatchesConsul(candidate capi.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceSplitterConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.ServiceSplitterConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) } -func (in *ServiceSplitter) Validate(namespacesEnabled bool) error { +func (in *ServiceSplitter) Validate(consulMeta common.ConsulMeta) error { errs := in.Spec.Splits.validate(field.NewPath("spec").Child("splits")) - errs = append(errs, in.validateNamespaces(namespacesEnabled)...) + errs = append(errs, in.validateEnterprise(consulMeta)...) if len(errs) > 0 { return apierrors.NewInvalid( @@ -177,7 +184,7 @@ func (in *ServiceSplitter) Validate(namespacesEnabled bool) error { // DefaultNamespaceFields has no behaviour here as service-splitter have namespace fields // that do not default. -func (in *ServiceSplitter) DefaultNamespaceFields(_ bool, _ string, _ bool, _ string) { +func (in *ServiceSplitter) DefaultNamespaceFields(_ common.ConsulMeta) { } func (in ServiceSplits) toConsul() []capi.ServiceSplit { @@ -191,23 +198,33 @@ func (in ServiceSplits) toConsul() []capi.ServiceSplit { func (in ServiceSplit) toConsul() capi.ServiceSplit { return capi.ServiceSplit{ - Weight: in.Weight, - Service: in.Service, - ServiceSubset: in.ServiceSubset, - Namespace: in.Namespace, + Weight: in.Weight, + Service: in.Service, + ServiceSubset: in.ServiceSubset, + Namespace: in.Namespace, + Partition: in.Partition, + RequestHeaders: in.RequestHeaders.toConsul(), + ResponseHeaders: in.ResponseHeaders.toConsul(), } } -func (in *ServiceSplitter) validateNamespaces(namespacesEnabled bool) field.ErrorList { +func (in *ServiceSplitter) validateEnterprise(consulMeta common.ConsulMeta) field.ErrorList { var errs field.ErrorList path := field.NewPath("spec") - if !namespacesEnabled { + if !consulMeta.NamespacesEnabled { for i, s := range in.Spec.Splits { if s.Namespace != "" { errs = append(errs, field.Invalid(path.Child("splits").Index(i).Child("namespace"), s.Namespace, `Consul Enterprise namespaces must be enabled to set split.namespace`)) } } } + if !consulMeta.PartitionsEnabled { + for i, s := range in.Spec.Splits { + if s.Partition != "" { + errs = append(errs, field.Invalid(path.Child("splits").Index(i).Child("partition"), s.Partition, `Consul Enterprise partitions must be enabled to set split.partition`)) + } + } + } return errs } diff --git a/control-plane/api/v1alpha1/servicesplitter_types_test.go b/control-plane/api/v1alpha1/servicesplitter_types_test.go index 2455e6349b..48e9eeac54 100644 --- a/control-plane/api/v1alpha1/servicesplitter_types_test.go +++ b/control-plane/api/v1alpha1/servicesplitter_types_test.go @@ -50,6 +50,36 @@ func TestServiceSplitter_MatchesConsul(t *testing.T) { Service: "foo", ServiceSubset: "bar", Namespace: "baz", + RequestHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, }, @@ -63,6 +93,36 @@ func TestServiceSplitter_MatchesConsul(t *testing.T) { Service: "foo", ServiceSubset: "bar", Namespace: "baz", + RequestHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, }, @@ -125,6 +185,36 @@ func TestServiceSplitter_ToConsul(t *testing.T) { Service: "foo", ServiceSubset: "bar", Namespace: "baz", + RequestHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, }, @@ -138,6 +228,36 @@ func TestServiceSplitter_ToConsul(t *testing.T) { Service: "foo", ServiceSubset: "bar", Namespace: "baz", + RequestHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "foo": "bar", + "source": "dest", + }, + Set: map[string]string{ + "bar": "baz", + "key": "car", + }, + Remove: []string{ + "foo", + "bar", + "baz", + }, + }, + ResponseHeaders: &capi.HTTPHeaderModifiers{ + Add: map[string]string{ + "doo": "var", + "aource": "sest", + }, + Set: map[string]string{ + "var": "vaz", + "jey": "xar", + }, + Remove: []string{ + "doo", + "var", + "vaz", + }, + }, }, }, Meta: map[string]string{ @@ -267,6 +387,7 @@ func TestServiceSplitter_Validate(t *testing.T) { cases := map[string]struct { input *ServiceSplitter namespacesEnabled bool + partitionsEnabled bool expectedErrMsgs []string }{ "namespaces enabled: valid": { @@ -309,6 +430,50 @@ func TestServiceSplitter_Validate(t *testing.T) { namespacesEnabled: false, expectedErrMsgs: nil, }, + "partitions enabled: valid": { + input: &ServiceSplitter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceSplitterSpec{ + Splits: []ServiceSplit{ + { + Weight: 99.99, + Namespace: "namespace-a", + Partition: "partition-a", + }, + { + Weight: 0.01, + Namespace: "namespace-b", + Partition: "partition-b", + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: true, + expectedErrMsgs: nil, + }, + "partitions disabled: valid": { + input: &ServiceSplitter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceSplitterSpec{ + Splits: []ServiceSplit{ + { + Weight: 99.99, + }, + { + Weight: 0.01, + }, + }, + }, + }, + namespacesEnabled: false, + partitionsEnabled: false, + expectedErrMsgs: nil, + }, "splits with 0 weight: valid": { input: &ServiceSplitter{ ObjectMeta: metav1.ObjectMeta{ @@ -421,10 +586,58 @@ func TestServiceSplitter_Validate(t *testing.T) { "spec.splits[1].namespace: Invalid value: \"namespace-b\": Consul Enterprise namespaces must be enabled to set split.namespace", }, }, + "partitions disabled: single split partition specified": { + input: &ServiceSplitter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceSplitterSpec{ + Splits: []ServiceSplit{ + { + Namespace: "namespace-a", + Partition: "partition-a", + Weight: 100, + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{ + "servicesplitter.consul.hashicorp.com \"foo\" is invalid: spec.splits[0].partition: Invalid value: \"partition-a\": Consul Enterprise partitions must be enabled to set split.partition", + }, + }, + "partitions disabled: multiple split partitions specified": { + input: &ServiceSplitter{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceSplitterSpec{ + Splits: []ServiceSplit{ + { + Namespace: "namespace-a", + Partition: "partition-a", + Weight: 50, + }, + { + Namespace: "namespace-b", + Partition: "partition-b", + Weight: 50, + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{ + "spec.splits[0].partition: Invalid value: \"partition-a\": Consul Enterprise partitions must be enabled to set split.partition", + "spec.splits[1].partition: Invalid value: \"partition-b\": Consul Enterprise partitions must be enabled to set split.partition", + }, + }, } for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(testCase.namespacesEnabled) + err := testCase.input.Validate(common.ConsulMeta{NamespacesEnabled: testCase.namespacesEnabled, PartitionsEnabled: testCase.partitionsEnabled}) if len(testCase.expectedErrMsgs) != 0 { require.Error(t, err) for _, s := range testCase.expectedErrMsgs { diff --git a/control-plane/api/v1alpha1/servicesplitter_webhook.go b/control-plane/api/v1alpha1/servicesplitter_webhook.go index 18d2074bdd..f90c49f45a 100644 --- a/control-plane/api/v1alpha1/servicesplitter_webhook.go +++ b/control-plane/api/v1alpha1/servicesplitter_webhook.go @@ -17,26 +17,8 @@ type ServiceSplitterWebhook struct { ConsulClient *capi.Client Logger logr.Logger - // EnableConsulNamespaces indicates that a user is running Consul Enterprise - // with version 1.7+ which supports namespaces. - EnableConsulNamespaces bool - - // EnableNSMirroring causes Consul namespaces to be created to match the - // k8s namespace of any config entry custom resource. Config entries will - // be created in the matching Consul namespace. - EnableNSMirroring bool - - // ConsulDestinationNamespace is the namespace in Consul that the config entry created - // in k8s will get mapped into. If the Consul namespace does not already exist, it will - // be created. - ConsulDestinationNamespace string - - // NSMirroringPrefix works in conjunction with Namespace Mirroring. - // It is the prefix added to the Consul namespace to map to a specific. - // k8s namespace. For example, if `mirroringK8SPrefix` is set to "k8s-", a - // service in the k8s `staging` namespace will be registered into the - // `k8s-staging` Consul namespace. - NSMirroringPrefix string + // ConsulMeta contains metadata specific to the Consul installation. + ConsulMeta common.ConsulMeta decoder *admission.Decoder client.Client @@ -58,15 +40,7 @@ func (v *ServiceSplitterWebhook) Handle(ctx context.Context, req admission.Reque return admission.Errored(http.StatusBadRequest, err) } - return common.ValidateConfigEntry(ctx, - req, - v.Logger, - v, - &serviceSplitter, - v.EnableConsulNamespaces, - v.EnableNSMirroring, - v.ConsulDestinationNamespace, - v.NSMirroringPrefix) + return common.ValidateConfigEntry(ctx, req, v.Logger, v, &serviceSplitter, v.ConsulMeta) } func (v *ServiceSplitterWebhook) List(ctx context.Context) ([]common.ConfigEntryResource, error) { diff --git a/control-plane/api/v1alpha1/shared_types.go b/control-plane/api/v1alpha1/shared_types.go index c26ca5d4f0..9b884cf476 100644 --- a/control-plane/api/v1alpha1/shared_types.go +++ b/control-plane/api/v1alpha1/shared_types.go @@ -52,7 +52,7 @@ type TransparentProxy struct { } // MeshGateway controls how Mesh Gateways are used for upstream Connect -// services +// services. type MeshGateway struct { // Mode is the mode that should be used for the upstream connection. // One of none, local, or remote. @@ -61,6 +61,24 @@ type MeshGateway struct { type ProxyMode string +// HTTPHeaderModifiers is a set of rules for HTTP header modification that +// should be performed by proxies as the request passes through them. It can +// operate on either request or response headers depending on the context in +// which it is used. +type HTTPHeaderModifiers struct { + // Add is a set of name -> value pairs that should be appended to the request + // or response (i.e. allowing duplicates if the same header already exists). + Add map[string]string `json:"add,omitempty"` + + // Set is a set of name -> value pairs that should be added to the request or + // response, overwriting any existing header values of the same name. + Set map[string]string `json:"set,omitempty"` + + // Remove is the set of header names that should be stripped from the request + // or response. + Remove []string `json:"remove,omitempty"` +} + func (in MeshGateway) toConsul() capi.MeshGatewayConfig { mode := capi.MeshGatewayMode(in.Mode) switch mode { @@ -147,6 +165,17 @@ func (in *ProxyMode) validate(path *field.Path) *field.Error { return nil } +func (in *HTTPHeaderModifiers) toConsul() *capi.HTTPHeaderModifiers { + if in == nil { + return nil + } + return &capi.HTTPHeaderModifiers{ + Add: in.Add, + Set: in.Set, + Remove: in.Remove, + } +} + func notInSliceMessage(slice []string) string { return fmt.Sprintf(`must be one of "%s"`, strings.Join(slice, `", "`)) } diff --git a/control-plane/api/v1alpha1/status.go b/control-plane/api/v1alpha1/status.go index 1dd19241f2..d7cd0e0b78 100644 --- a/control-plane/api/v1alpha1/status.go +++ b/control-plane/api/v1alpha1/status.go @@ -5,7 +5,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// Conditions is the schema for the conditions portion of the payload +// Conditions is the schema for the conditions portion of the payload. type Conditions []Condition // ConditionType is a camel-cased condition type. @@ -42,7 +42,7 @@ type Condition struct { Message string `json:"message,omitempty" description:"human-readable message indicating details about last transition"` } -// IsTrue is true if the condition is True +// IsTrue is true if the condition is True. func (c *Condition) IsTrue() bool { if c == nil { return false @@ -50,7 +50,7 @@ func (c *Condition) IsTrue() bool { return c.Status == corev1.ConditionTrue } -// IsFalse is true if the condition is False +// IsFalse is true if the condition is False. func (c *Condition) IsFalse() bool { if c == nil { return false @@ -58,7 +58,7 @@ func (c *Condition) IsFalse() bool { return c.Status == corev1.ConditionFalse } -// IsUnknown is true if the condition is Unknown +// IsUnknown is true if the condition is Unknown. func (c *Condition) IsUnknown() bool { if c == nil { return true diff --git a/control-plane/api/v1alpha1/terminatinggateway_types.go b/control-plane/api/v1alpha1/terminatinggateway_types.go index d8138b1a42..6e708b5d44 100644 --- a/control-plane/api/v1alpha1/terminatinggateway_types.go +++ b/control-plane/api/v1alpha1/terminatinggateway_types.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/hashicorp/consul-k8s/control-plane/namespaces" capi "github.com/hashicorp/consul/api" corev1 "k8s.io/api/core/v1" @@ -29,6 +30,7 @@ func init() { // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" // +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="terminating-gateway" type TerminatingGateway struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -39,20 +41,20 @@ type TerminatingGateway struct { // +kubebuilder:object:root=true -// TerminatingGatewayList contains a list of TerminatingGateway +// TerminatingGatewayList contains a list of TerminatingGateway. type TerminatingGatewayList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []TerminatingGateway `json:"items"` } -// TerminatingGatewaySpec defines the desired state of TerminatingGateway +// TerminatingGatewaySpec defines the desired state of TerminatingGateway. type TerminatingGatewaySpec struct { // Services is a list of service names represented by the terminating gateway. Services []LinkedService `json:"services,omitempty"` } -// A LinkedService is a service represented by a terminating gateway +// A LinkedService is a service represented by a terminating gateway. type LinkedService struct { // The namespace the service is registered in. Namespace string `json:"namespace,omitempty"` @@ -173,10 +175,10 @@ func (in *TerminatingGateway) MatchesConsul(candidate capi.ConfigEntry) bool { return false } // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. - return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.TerminatingGatewayConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.TerminatingGatewayConfigEntry{}, "Partition", "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) } -func (in *TerminatingGateway) Validate(namespacesEnabled bool) error { +func (in *TerminatingGateway) Validate(consulMeta common.ConsulMeta) error { var errs field.ErrorList path := field.NewPath("spec") @@ -184,7 +186,7 @@ func (in *TerminatingGateway) Validate(namespacesEnabled bool) error { errs = append(errs, v.validate(path.Child("services").Index(i))...) } - errs = append(errs, in.validateNamespaces(namespacesEnabled)...) + errs = append(errs, in.validateNamespaces(consulMeta.NamespacesEnabled)...) if len(errs) > 0 { return apierrors.NewInvalid( @@ -195,14 +197,14 @@ func (in *TerminatingGateway) Validate(namespacesEnabled bool) error { } // DefaultNamespaceFields sets the namespace field on spec.services to their default values if namespaces are enabled. -func (in *TerminatingGateway) DefaultNamespaceFields(consulNamespacesEnabled bool, destinationNamespace string, mirroring bool, prefix string) { +func (in *TerminatingGateway) DefaultNamespaceFields(consulMeta common.ConsulMeta) { // If namespaces are enabled we want to set the namespace fields to their // defaults. If namespaces are not enabled (i.e. OSS) we don't set the // namespace fields because this would cause errors // making API calls (because namespace fields can't be set in OSS). - if consulNamespacesEnabled { + if consulMeta.NamespacesEnabled { // Default to the current namespace (i.e. the namespace of the config entry). - namespace := namespaces.ConsulNamespace(in.Namespace, consulNamespacesEnabled, destinationNamespace, mirroring, prefix) + namespace := namespaces.ConsulNamespace(in.Namespace, consulMeta.NamespacesEnabled, consulMeta.DestinationNamespace, consulMeta.Mirroring, consulMeta.Prefix) for i, service := range in.Spec.Services { if service.Namespace == "" { in.Spec.Services[i].Namespace = namespace diff --git a/control-plane/api/v1alpha1/terminatinggateway_types_test.go b/control-plane/api/v1alpha1/terminatinggateway_types_test.go index f9da98729c..9d8ba9948d 100644 --- a/control-plane/api/v1alpha1/terminatinggateway_types_test.go +++ b/control-plane/api/v1alpha1/terminatinggateway_types_test.go @@ -268,7 +268,7 @@ func TestTerminatingGateway_Validate(t *testing.T) { for name, testCase := range cases { t.Run(name, func(t *testing.T) { - err := testCase.input.Validate(testCase.namespacesEnabled) + err := testCase.input.Validate(common.ConsulMeta{NamespacesEnabled: testCase.namespacesEnabled}) if len(testCase.expectedErrMsgs) != 0 { require.Error(t, err) for _, s := range testCase.expectedErrMsgs { @@ -284,39 +284,44 @@ func TestTerminatingGateway_Validate(t *testing.T) { // Test defaulting behavior when namespaces are enabled as well as disabled. func TestTerminatingGateway_DefaultNamespaceFields(t *testing.T) { namespaceConfig := map[string]struct { - enabled bool - destinationNamespace string - mirroring bool - prefix string - expectedDestination string + consulMeta common.ConsulMeta + expectedDestination string }{ "disabled": { - enabled: false, - destinationNamespace: "", - mirroring: false, - prefix: "", - expectedDestination: "", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: false, + DestinationNamespace: "", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "", }, "destinationNS": { - enabled: true, - destinationNamespace: "foo", - mirroring: false, - prefix: "", - expectedDestination: "foo", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "foo", + Mirroring: false, + Prefix: "", + }, + expectedDestination: "foo", }, "mirroringEnabledWithoutPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "", - expectedDestination: "bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "", + }, + expectedDestination: "bar", }, "mirroringWithPrefix": { - enabled: true, - destinationNamespace: "", - mirroring: true, - prefix: "ns-", - expectedDestination: "ns-bar", + consulMeta: common.ConsulMeta{ + NamespacesEnabled: true, + DestinationNamespace: "", + Mirroring: true, + Prefix: "ns-", + }, + expectedDestination: "ns-bar", }, } @@ -357,7 +362,7 @@ func TestTerminatingGateway_DefaultNamespaceFields(t *testing.T) { }, }, } - input.DefaultNamespaceFields(s.enabled, s.destinationNamespace, s.mirroring, s.prefix) + input.DefaultNamespaceFields(s.consulMeta) require.True(t, cmp.Equal(input, output)) }) } diff --git a/control-plane/api/v1alpha1/terminatinggateway_webhook.go b/control-plane/api/v1alpha1/terminatinggateway_webhook.go index 44d79ecc21..2d3367fcaa 100644 --- a/control-plane/api/v1alpha1/terminatinggateway_webhook.go +++ b/control-plane/api/v1alpha1/terminatinggateway_webhook.go @@ -17,26 +17,8 @@ type TerminatingGatewayWebhook struct { ConsulClient *capi.Client Logger logr.Logger - // EnableConsulNamespaces indicates that a user is running Consul Enterprise - // with version 1.7+ which supports namespaces. - EnableConsulNamespaces bool - - // EnableNSMirroring causes Consul namespaces to be created to match the - // k8s namespace of any config entry custom resource. Config entries will - // be created in the matching Consul namespace. - EnableNSMirroring bool - - // ConsulDestinationNamespace is the namespace in Consul that the config entry created - // in k8s will get mapped into. If the Consul namespace does not already exist, it will - // be created. - ConsulDestinationNamespace string - - // NSMirroringPrefix works in conjunction with Namespace Mirroring. - // It is the prefix added to the Consul namespace to map to a specific. - // k8s namespace. For example, if `mirroringK8SPrefix` is set to "k8s-", a - // service in the k8s `staging` namespace will be registered into the - // `k8s-staging` Consul namespace. - NSMirroringPrefix string + // ConsulMeta contains metadata specific to the Consul installation. + ConsulMeta common.ConsulMeta decoder *admission.Decoder client.Client @@ -57,15 +39,7 @@ func (v *TerminatingGatewayWebhook) Handle(ctx context.Context, req admission.Re return admission.Errored(http.StatusBadRequest, err) } - return common.ValidateConfigEntry(ctx, - req, - v.Logger, - v, - &resource, - v.EnableConsulNamespaces, - v.EnableNSMirroring, - v.ConsulDestinationNamespace, - v.NSMirroringPrefix) + return common.ValidateConfigEntry(ctx, req, v.Logger, v, &resource, v.ConsulMeta) } func (v *TerminatingGatewayWebhook) List(ctx context.Context) ([]common.ConfigEntryResource, error) { diff --git a/control-plane/api/v1alpha1/zz_generated.deepcopy.go b/control-plane/api/v1alpha1/zz_generated.deepcopy.go index 77b6b2d4e2..2fc836ebe8 100644 --- a/control-plane/api/v1alpha1/zz_generated.deepcopy.go +++ b/control-plane/api/v1alpha1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated // Code generated by controller-gen. DO NOT EDIT. @@ -77,6 +78,107 @@ func (in *Destination) DeepCopy() *Destination { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExportedService) DeepCopyInto(out *ExportedService) { + *out = *in + if in.Consumers != nil { + in, out := &in.Consumers, &out.Consumers + *out = make([]ServiceConsumer, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExportedService. +func (in *ExportedService) DeepCopy() *ExportedService { + if in == nil { + return nil + } + out := new(ExportedService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExportedServices) DeepCopyInto(out *ExportedServices) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExportedServices. +func (in *ExportedServices) DeepCopy() *ExportedServices { + if in == nil { + return nil + } + out := new(ExportedServices) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExportedServices) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExportedServicesList) DeepCopyInto(out *ExportedServicesList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ExportedServices, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExportedServicesList. +func (in *ExportedServicesList) DeepCopy() *ExportedServicesList { + if in == nil { + return nil + } + out := new(ExportedServicesList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExportedServicesList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExportedServicesSpec) DeepCopyInto(out *ExportedServicesSpec) { + *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]ExportedService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExportedServicesSpec. +func (in *ExportedServicesSpec) DeepCopy() *ExportedServicesSpec { + if in == nil { + return nil + } + out := new(ExportedServicesSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Expose) DeepCopyInto(out *Expose) { *out = *in @@ -112,9 +214,34 @@ func (in *ExposePath) DeepCopy() *ExposePath { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayServiceTLSConfig) DeepCopyInto(out *GatewayServiceTLSConfig) { + *out = *in + if in.SDS != nil { + in, out := &in.SDS, &out.SDS + *out = new(GatewayTLSSDSConfig) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayServiceTLSConfig. +func (in *GatewayServiceTLSConfig) DeepCopy() *GatewayServiceTLSConfig { + if in == nil { + return nil + } + out := new(GatewayServiceTLSConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewayTLSConfig) DeepCopyInto(out *GatewayTLSConfig) { *out = *in + if in.SDS != nil { + in, out := &in.SDS, &out.SDS + *out = new(GatewayTLSSDSConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayTLSConfig. @@ -127,6 +254,55 @@ func (in *GatewayTLSConfig) DeepCopy() *GatewayTLSConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayTLSSDSConfig) DeepCopyInto(out *GatewayTLSSDSConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayTLSSDSConfig. +func (in *GatewayTLSSDSConfig) DeepCopy() *GatewayTLSSDSConfig { + if in == nil { + return nil + } + out := new(GatewayTLSSDSConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPHeaderModifiers) DeepCopyInto(out *HTTPHeaderModifiers) { + *out = *in + if in.Add != nil { + in, out := &in.Add, &out.Add + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Set != nil { + in, out := &in.Set, &out.Set + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Remove != nil { + in, out := &in.Remove, &out.Remove + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHeaderModifiers. +func (in *HTTPHeaderModifiers) DeepCopy() *HTTPHeaderModifiers { + if in == nil { + return nil + } + out := new(HTTPHeaderModifiers) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HashPolicy) DeepCopyInto(out *HashPolicy) { *out = *in @@ -209,7 +385,7 @@ func (in *IngressGatewayList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IngressGatewaySpec) DeepCopyInto(out *IngressGatewaySpec) { *out = *in - out.TLS = in.TLS + in.TLS.DeepCopyInto(&out.TLS) if in.Listeners != nil { in, out := &in.Listeners, &out.Listeners *out = make([]IngressListener, len(*in)) @@ -232,6 +408,11 @@ func (in *IngressGatewaySpec) DeepCopy() *IngressGatewaySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IngressListener) DeepCopyInto(out *IngressListener) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(GatewayTLSConfig) + (*in).DeepCopyInto(*out) + } if in.Services != nil { in, out := &in.Services, &out.Services *out = make([]IngressService, len(*in)) @@ -259,6 +440,21 @@ func (in *IngressService) DeepCopyInto(out *IngressService) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(GatewayServiceTLSConfig) + (*in).DeepCopyInto(*out) + } + if in.RequestHeaders != nil { + in, out := &in.RequestHeaders, &out.RequestHeaders + *out = new(HTTPHeaderModifiers) + (*in).DeepCopyInto(*out) + } + if in.ResponseHeaders != nil { + in, out := &in.ResponseHeaders, &out.ResponseHeaders + *out = new(HTTPHeaderModifiers) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressService. @@ -649,6 +845,21 @@ func (in *RingHashConfig) DeepCopy() *RingHashConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceConsumer) DeepCopyInto(out *ServiceConsumer) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceConsumer. +func (in *ServiceConsumer) DeepCopy() *ServiceConsumer { + if in == nil { + return nil + } + out := new(ServiceConsumer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceDefaults) DeepCopyInto(out *ServiceDefaults) { *out = *in @@ -1051,6 +1262,16 @@ func (in *ServiceRouteDestination) DeepCopyInto(out *ServiceRouteDestination) { *out = make([]uint32, len(*in)) copy(*out, *in) } + if in.RequestHeaders != nil { + in, out := &in.RequestHeaders, &out.RequestHeaders + *out = new(HTTPHeaderModifiers) + (*in).DeepCopyInto(*out) + } + if in.ResponseHeaders != nil { + in, out := &in.ResponseHeaders, &out.ResponseHeaders + *out = new(HTTPHeaderModifiers) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRouteDestination. @@ -1227,6 +1448,16 @@ func (in *ServiceRouterSpec) DeepCopy() *ServiceRouterSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceSplit) DeepCopyInto(out *ServiceSplit) { *out = *in + if in.RequestHeaders != nil { + in, out := &in.RequestHeaders, &out.RequestHeaders + *out = new(HTTPHeaderModifiers) + (*in).DeepCopyInto(*out) + } + if in.ResponseHeaders != nil { + in, out := &in.ResponseHeaders, &out.ResponseHeaders + *out = new(HTTPHeaderModifiers) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSplit. @@ -1244,7 +1475,9 @@ func (in ServiceSplits) DeepCopyInto(out *ServiceSplits) { { in := &in *out = make(ServiceSplits, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } @@ -1323,7 +1556,9 @@ func (in *ServiceSplitterSpec) DeepCopyInto(out *ServiceSplitterSpec) { if in.Splits != nil { in, out := &in.Splits, &out.Splits *out = make(ServiceSplits, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } diff --git a/control-plane/build-support/docker/Release.dockerfile b/control-plane/build-support/docker/Release.dockerfile index 33b21e155f..b782bd579b 100644 --- a/control-plane/build-support/docker/Release.dockerfile +++ b/control-plane/build-support/docker/Release.dockerfile @@ -5,7 +5,7 @@ # We don't rebuild the software because we want the exact checksums and # binary signatures to match the software and our builds aren't fully # reproducible currently. -FROM alpine:3.13 +FROM alpine:3.15 # NAME and VERSION are the name of the software in releases.hashicorp.com # and the version to download. Example: NAME=consul VERSION=1.2.3. @@ -50,7 +50,7 @@ RUN set -eux && \ apkArch="$(apk --print-arch)" && \ case "${apkArch}" in \ aarch64) ARCH='arm64' ;; \ - armhf) ARCH='armhfv6' ;; \ + armhf) ARCH='arm' ;; \ x86) ARCH='386' ;; \ x86_64) ARCH='amd64' ;; \ *) echo >&2 "error: unsupported architecture: ${apkArch} (see ${HASHICORP_RELEASES}/${NAME}/${VERSION}/)" && exit 1 ;; \ diff --git a/control-plane/build-support/docker/Release.ubi.dockerfile b/control-plane/build-support/docker/Release.ubi.dockerfile index b57832398b..f5db390532 100644 --- a/control-plane/build-support/docker/Release.ubi.dockerfile +++ b/control-plane/build-support/docker/Release.ubi.dockerfile @@ -8,7 +8,7 @@ # We don't rebuild the software because we want the exact checksums and # binary signatures to match the software and our builds aren't fully # reproducible currently. -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5 # NAME and VERSION are the name of the software in releases.hashicorp.com # and the version to download. Example: NAME=consul VERSION=1.2.3. diff --git a/control-plane/build-support/functions/10-util.sh b/control-plane/build-support/functions/10-util.sh index 1fb2f45b3a..a39b2307e7 100644 --- a/control-plane/build-support/functions/10-util.sh +++ b/control-plane/build-support/functions/10-util.sh @@ -795,9 +795,14 @@ function set_dev_mode { local sdir="$1" local vers="$(parse_version "${sdir}" false false)" - status_stage "==> Setting VersionPreRelease back to 'dev'" + status_stage "==> Setting VersionPreRelease back to 'dev' for Control Plane" update_version "${sdir}/version/version.go" "${vers}" dev || return 1 + # This function has been modified for Consul-K8s monorepo. It now sets dev mode + # for the CLI module as well. + status_stage "==> Setting VersionPreRelease back to 'dev' for CLI" + update_version "${sdir}/../cli/version/version.go" "${vers}" dev || return 1 + status_stage "==> Adding new UNRELEASED label in CHANGELOG.md" add_unreleased_to_changelog "${sdir}/.." || return 1 @@ -857,7 +862,7 @@ function commit_dev_mode { pushd "$1" > /dev/null status "Staging CHANGELOG.md and version_*.go files" - git add CHANGELOG.md && git add control-plane/version/version*.go + git add CHANGELOG.md && git add */version/version*.go ret=$? if test ${ret} -eq 0 diff --git a/control-plane/build-support/functions/20-build.sh b/control-plane/build-support/functions/20-build.sh index 539647aa26..dd1551589f 100644 --- a/control-plane/build-support/functions/20-build.sh +++ b/control-plane/build-support/functions/20-build.sh @@ -242,7 +242,6 @@ function build_consul_local { CGO_ENABLED=0 gox \ -os="${build_os}" \ -arch="${build_arch}" \ - -osarch="!darwin/arm !darwin/arm64" \ -ldflags="${GOLDFLAGS}" \ -parallel="${GOXPARALLEL:-"-1"}" \ -output "pkg.bin.new/${extra_dir}{{.OS}}_{{.Arch}}/${bin_name}" \ diff --git a/control-plane/build-support/scripts/dev.sh b/control-plane/build-support/scripts/dev.sh index a128698193..7cb79d01e1 100755 --- a/control-plane/build-support/scripts/dev.sh +++ b/control-plane/build-support/scripts/dev.sh @@ -84,6 +84,7 @@ function main { esac done + # Set dev mode for both CLI and Control Plane modules set_dev_mode "${sdir}" || return 1 @@ -91,6 +92,7 @@ function main { then status_stage "==> Commiting Dev Mode Changes" # Currently ${sdir} is consul-k8s/control-plane, but for git functions we should be in top-level, so we pass in "${sdir}/..". + # This will commit `version.go` for both CLI and Control Plane modules as well as the CHANGELOG.md. commit_dev_mode "${sdir}/.." || return 1 if is_set "${do_push}" diff --git a/control-plane/catalog/to-consul/consul_node_services_client_ent_test.go b/control-plane/catalog/to-consul/consul_node_services_client_ent_test.go index 42c68efdfe..ac570948f5 100644 --- a/control-plane/catalog/to-consul/consul_node_services_client_ent_test.go +++ b/control-plane/catalog/to-consul/consul_node_services_client_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package catalog diff --git a/control-plane/catalog/to-consul/resource.go b/control-plane/catalog/to-consul/resource.go index 9fd0b5cfc0..4444f14b18 100644 --- a/control-plane/catalog/to-consul/resource.go +++ b/control-plane/catalog/to-consul/resource.go @@ -35,14 +35,14 @@ type NodePortSyncType string const ( // Only sync NodePort services with a node's ExternalIP address. - // Doesn't sync if an ExternalIP doesn't exist + // Doesn't sync if an ExternalIP doesn't exist. ExternalOnly NodePortSyncType = "ExternalOnly" // Sync with an ExternalIP first, if it doesn't exist, use the - // node's InternalIP address instead + // node's InternalIP address instead. ExternalFirst NodePortSyncType = "ExternalFirst" - // Sync NodePort services using + // Sync NodePort services using. InternalOnly NodePortSyncType = "InternalOnly" ) @@ -53,6 +53,9 @@ type ServiceResource struct { Client kubernetes.Interface Syncer Syncer + // Ctx is used to cancel processes kicked off by ServiceResource. + Ctx context.Context + // AllowK8sNamespacesSet is a set of k8s namespaces to explicitly allow for // syncing. It supports the special character `*` which indicates that // all k8s namespaces are eligible unless explicitly denied. This filter @@ -143,11 +146,11 @@ func (t *ServiceResource) Informer() cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { - return t.Client.CoreV1().Services(metav1.NamespaceAll).List(context.TODO(), options) + return t.Client.CoreV1().Services(metav1.NamespaceAll).List(t.Ctx, options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - return t.Client.CoreV1().Services(metav1.NamespaceAll).Watch(context.TODO(), options) + return t.Client.CoreV1().Services(metav1.NamespaceAll).Watch(t.Ctx, options) }, }, &apiv1.Service{}, @@ -191,7 +194,7 @@ func (t *ServiceResource) Upsert(key string, raw interface{}) error { if t.shouldTrackEndpoints(key) { endpoints, err := t.Client.CoreV1(). Endpoints(service.Namespace). - Get(context.TODO(), service.Name, metav1.GetOptions{}) + Get(t.Ctx, service.Name, metav1.GetOptions{}) if err != nil { t.Log.Warn("error loading initial endpoints", "key", key, @@ -223,7 +226,7 @@ func (t *ServiceResource) Delete(key string, _ interface{}) error { // doDelete is a helper function for deletion. // -// Precondition: assumes t.serviceLock is held +// Precondition: assumes t.serviceLock is held. func (t *ServiceResource) doDelete(key string) { delete(t.serviceMap, key) t.Log.Debug("[doDelete] deleting service from serviceMap", "key", key) @@ -242,7 +245,7 @@ func (t *ServiceResource) Run(ch <-chan struct{}) { t.Log.Info("starting runner for endpoints") (&controller.Controller{ Log: t.Log.Named("controller/endpoints"), - Resource: &serviceEndpointsResource{Service: t}, + Resource: &serviceEndpointsResource{Service: t, Ctx: t.Ctx}, }).Run(ch) } @@ -289,7 +292,7 @@ func (t *ServiceResource) shouldSync(svc *apiv1.Service) bool { // shouldTrackEndpoints returns true if the endpoints for the given key // should be tracked. // -// Precondition: this requires the lock to be held +// Precondition: this requires the lock to be held. func (t *ServiceResource) shouldTrackEndpoints(key string) bool { // The service must be one we care about for us to watch the endpoints. // We care about a service that exists in our service map (is enabled @@ -431,10 +434,8 @@ func (t *ServiceResource) generateRegistrations(key string) { } // Parse any additional tags - if tags, ok := svc.Annotations[annotationServiceTags]; ok { - for _, t := range strings.Split(tags, ",") { - baseService.Tags = append(baseService.Tags, strings.TrimSpace(t)) - } + if rawTags, ok := svc.Annotations[annotationServiceTags]; ok { + baseService.Tags = append(baseService.Tags, parseTags(rawTags)...) } // Parse any additional meta @@ -526,7 +527,7 @@ func (t *ServiceResource) generateRegistrations(key string) { } // Look up the node's ip address by getting node info - node, err := t.Client.CoreV1().Nodes().Get(context.TODO(), *subsetAddr.NodeName, metav1.GetOptions{}) + node, err := t.Client.CoreV1().Nodes().Get(t.Ctx, *subsetAddr.NodeName, metav1.GetOptions{}) if err != nil { t.Log.Warn("error getting node info", "error", err) continue @@ -663,7 +664,7 @@ func (t *ServiceResource) registerServiceInstance( // sync calls the Syncer.Sync function from the generated registrations. // -// Precondition: lock must be held +// Precondition: lock must be held. func (t *ServiceResource) sync() { // NOTE(mitchellh): This isn't the most efficient way to do this and // the times that sync are called are also not the most efficient. All @@ -683,6 +684,7 @@ func (t *ServiceResource) sync() { // to keep track of changing endpoints for registered services. type serviceEndpointsResource struct { Service *ServiceResource + Ctx context.Context } func (t *serviceEndpointsResource) Informer() cache.SharedIndexInformer { @@ -695,13 +697,13 @@ func (t *serviceEndpointsResource) Informer() cache.SharedIndexInformer { ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return t.Service.Client.CoreV1(). Endpoints(metav1.NamespaceAll). - List(context.TODO(), options) + List(t.Ctx, options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { return t.Service.Client.CoreV1(). Endpoints(metav1.NamespaceAll). - Watch(context.TODO(), options) + Watch(t.Ctx, options) }, }, &apiv1.Endpoints{}, @@ -769,3 +771,53 @@ func (t *ServiceResource) addPrefixAndK8SNamespace(name, namespace string) strin return name } + +// parseTags parses the tags annotation into a slice of tags. +// Tags are split on commas (except for escaped commas "\,"). +func parseTags(tagsAnno string) []string { + + // This algorithm parses the tagsAnno string into a slice of strings. + // Ideally we'd just split on commas but since Consul tags support commas, + // we allow users to escape commas so they're included in the tag, e.g. + // the annotation "tag\,with\,commas,tag2" will become the tags: + // ["tag,with,commas", "tag2"]. + + var tags []string + // nextTag is built up char by char until we see a comma. Then we + // append it to tags. + var nextTag string + + for _, runeChar := range tagsAnno { + runeStr := fmt.Sprintf("%c", runeChar) + + // Not a comma, just append to nextTag. + if runeStr != "," { + nextTag += runeStr + continue + } + + // Reached a comma but there's nothing in nextTag, + // skip. (e.g. "a,,b" => ["a", "b"]) + if len(nextTag) == 0 { + continue + } + + // Check if the comma was escaped comma, e.g. "a\,b". + if string(nextTag[len(nextTag)-1]) == `\` { + // Replace the backslash with a comma. + nextTag = nextTag[0:len(nextTag)-1] + "," + continue + } + + // Non-escaped comma. We're ready to push nextTag onto tags and reset nextTag. + tags = append(tags, strings.TrimSpace(nextTag)) + nextTag = "" + } + + // We're done the loop but nextTag still contains the last tag. + if len(nextTag) > 0 { + tags = append(tags, strings.TrimSpace(nextTag)) + } + + return tags +} diff --git a/control-plane/catalog/to-consul/resource_test.go b/control-plane/catalog/to-consul/resource_test.go index 5dc0f5e04a..9fc5ced094 100644 --- a/control-plane/catalog/to-consul/resource_test.go +++ b/control-plane/catalog/to-consul/resource_test.go @@ -102,7 +102,7 @@ func TestServiceResource_defaultEnableDisable(t *testing.T) { }) } -// Test that we can default disable +// Test that we can default disable. func TestServiceResource_defaultDisable(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -128,7 +128,7 @@ func TestServiceResource_defaultDisable(t *testing.T) { }) } -// Test that we can default disable but override +// Test that we can default disable but override. func TestServiceResource_defaultDisableEnable(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -196,7 +196,7 @@ func TestServiceResource_changeSyncToFalse(t *testing.T) { } // Test that the k8s namespace is appended with a '-' -// when AddK8SNamespaceSuffix is true +// when AddK8SNamespaceSuffix is true. func TestServiceResource_addK8SNamespace(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -224,7 +224,7 @@ func TestServiceResource_addK8SNamespace(t *testing.T) { } // Test k8s namespace suffix is appended -// when the consul prefix is provided +// when the consul prefix is provided. func TestServiceResource_addK8SNamespaceWithPrefix(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -281,7 +281,7 @@ func TestServiceResource_ConsulNodeName(t *testing.T) { } // Test k8s namespace suffix is not appended -// when the service name annotation is provided +// when the service name annotation is provided. func TestServiceResource_addK8SNamespaceWithNameAnnotation(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -339,7 +339,7 @@ func TestServiceResource_externalIP(t *testing.T) { }) } -// Test externalIP with Prefix +// Test externalIP with Prefix. func TestServiceResource_externalIPPrefix(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -397,7 +397,7 @@ func TestServiceResource_lb(t *testing.T) { }) } -// Test that the proper registrations are generated for a LoadBalancer with a prefix +// Test that the proper registrations are generated for a LoadBalancer with a prefix. func TestServiceResource_lbPrefix(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -460,7 +460,7 @@ func TestServiceResource_lbMultiEndpoint(t *testing.T) { }) } -// Test explicit name annotation +// Test explicit name annotation. func TestServiceResource_lbAnnotatedName(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -487,7 +487,7 @@ func TestServiceResource_lbAnnotatedName(t *testing.T) { }) } -// Test default port and additional ports in the meta +// Test default port and additional ports in the meta. func TestServiceResource_lbPort(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -519,7 +519,7 @@ func TestServiceResource_lbPort(t *testing.T) { }) } -// Test default port works with override annotation +// Test default port works with override annotation. func TestServiceResource_lbAnnotatedPort(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -552,7 +552,7 @@ func TestServiceResource_lbAnnotatedPort(t *testing.T) { }) } -// Test annotated tags +// Test annotated tags. func TestServiceResource_lbAnnotatedTags(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -566,7 +566,7 @@ func TestServiceResource_lbAnnotatedTags(t *testing.T) { // Insert an LB service svc := lbService("foo", metav1.NamespaceDefault, "1.2.3.4") - svc.Annotations[annotationServiceTags] = "one, two,three" + svc.Annotations[annotationServiceTags] = `one, leadingwhitespace,trailingwhitespace ,\,leadingcomma,trailingcomma\,,middle\,comma,,` _, err := client.CoreV1().Services(metav1.NamespaceDefault).Create(context.Background(), svc, metav1.CreateOptions{}) require.NoError(t, err) @@ -576,11 +576,11 @@ func TestServiceResource_lbAnnotatedTags(t *testing.T) { defer syncer.Unlock() actual := syncer.Registrations require.Len(r, actual, 1) - require.Equal(r, []string{"k8s", "one", "two", "three"}, actual[0].Service.Tags) + require.Equal(r, []string{"k8s", "one", "leadingwhitespace", "trailingwhitespace", ",leadingcomma", "trailingcomma,", "middle,comma"}, actual[0].Service.Tags) }) } -// Test annotated service meta +// Test annotated service meta. func TestServiceResource_lbAnnotatedMeta(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -607,7 +607,7 @@ func TestServiceResource_lbAnnotatedMeta(t *testing.T) { }) } -// Test that with LoadBalancerEndpointsSync set to true we track the IP of the endpoints not the LB IP/name +// Test that with LoadBalancerEndpointsSync set to true we track the IP of the endpoints not the LB IP/name. func TestServiceResource_lbRegisterEndpoints(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -701,7 +701,7 @@ func TestServiceResource_nodePort(t *testing.T) { }) } -// Test node port works with prefix +// Test node port works with prefix. func TestServiceResource_nodePortPrefix(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -922,7 +922,7 @@ func TestServiceResource_nodePort_internalOnlySync(t *testing.T) { } // Test that the proper registrations are generated for a NodePort type -// when preferring to sync external Node IPs over internal IPs +// when preferring to sync external Node IPs over internal IPs. func TestServiceResource_nodePort_externalFirstSync(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -1005,7 +1005,7 @@ func TestServiceResource_clusterIP(t *testing.T) { }) } -// Test clusterIP with prefix +// Test clusterIP with prefix. func TestServiceResource_clusterIPPrefix(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -1188,7 +1188,7 @@ func TestServiceResource_clusterIPSyncDisabled(t *testing.T) { }) } -// Test that the ClusterIP services are synced when watching all namespaces +// Test that the ClusterIP services are synced when watching all namespaces. func TestServiceResource_clusterIPAllNamespaces(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -1453,6 +1453,50 @@ func TestServiceResource_MirroredPrefixNamespace(t *testing.T) { }) } +func TestParseTags(t *testing.T) { + cases := []struct { + tagsAnno string + exp []string + }{ + { + "tag", + []string{"tag"}, + }, + { + ",,removes,,empty,elems,,", + []string{"removes", "empty", "elems"}, + }, + { + "removes , white ,space ", + []string{"removes", "white", "space"}, + }, + { + `\,leading,comma`, + []string{",leading", "comma"}, + }, + { + `trailing,comma\,`, + []string{"trailing", "comma,"}, + }, + { + `mid\,dle,com\,ma`, + []string{"mid,dle", "com,ma"}, + }, + { + `\,\,multi\,\,,\,com\,\,ma`, + []string{",,multi,,", ",com,,ma"}, + }, + { + ` every\,\, , thing `, + []string{"every,,", "thing"}, + }, + } + + for _, c := range cases { + require.Equal(t, c.exp, parseTags(c.tagsAnno)) + } +} + // lbService returns a Kubernetes service of type LoadBalancer. func lbService(name, namespace, lbIP string) *apiv1.Service { return &apiv1.Service{ @@ -1597,6 +1641,7 @@ func defaultServiceResource(client kubernetes.Interface, syncer Syncer) ServiceR Log: hclog.Default(), Client: client, Syncer: syncer, + Ctx: context.Background(), AllowK8sNamespacesSet: mapset.NewSet("*"), DenyK8sNamespacesSet: mapset.NewSet(), ConsulNodeName: ConsulSyncNodeName, diff --git a/control-plane/catalog/to-consul/syncer.go b/control-plane/catalog/to-consul/syncer.go index 557e51bf0d..2e2edad61a 100644 --- a/control-plane/catalog/to-consul/syncer.go +++ b/control-plane/catalog/to-consul/syncer.go @@ -98,7 +98,7 @@ type ConsulSyncer struct { watchers map[string]map[string]context.CancelFunc } -// Sync implements Syncer +// Sync implements Syncer. func (s *ConsulSyncer) Sync(rs []*api.CatalogRegistration) { // Grab the lock so we can replace the sync state s.lock.Lock() @@ -317,7 +317,7 @@ func (s *ConsulSyncer) watchService(ctx context.Context, name, namespace string) // scheduleReapService finds all the instances of the service with the given // name that have the k8s tag and schedules them for removal. // -// Precondition: lock must be held +// Precondition: lock must be held. func (s *ConsulSyncer) scheduleReapServiceLocked(name, namespace string) error { // Set up query options opts := api.QueryOptions{AllowStale: true} diff --git a/control-plane/catalog/to-consul/syncer_ent_test.go b/control-plane/catalog/to-consul/syncer_ent_test.go index c6b9941f23..2cc206f908 100644 --- a/control-plane/catalog/to-consul/syncer_ent_test.go +++ b/control-plane/catalog/to-consul/syncer_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package catalog diff --git a/control-plane/catalog/to-consul/testing.go b/control-plane/catalog/to-consul/testing.go index 89813ac5d6..e6541c6ba1 100644 --- a/control-plane/catalog/to-consul/testing.go +++ b/control-plane/catalog/to-consul/testing.go @@ -17,7 +17,7 @@ type testSyncer struct { Registrations []*api.CatalogRegistration } -// Sync implements Syncer +// Sync implements Syncer. func (s *testSyncer) Sync(rs []*api.CatalogRegistration) { s.Lock() defer s.Unlock() diff --git a/control-plane/catalog/to-k8s/sink.go b/control-plane/catalog/to-k8s/sink.go index a823842fb3..fa8821989e 100644 --- a/control-plane/catalog/to-k8s/sink.go +++ b/control-plane/catalog/to-k8s/sink.go @@ -51,6 +51,9 @@ type K8SSink struct { // done if there are no changes. SyncPeriod time.Duration + // Ctx is used to cancel the Sink. + Ctx context.Context + // lock gates concurrent access to all the maps. lock sync.Mutex @@ -79,7 +82,7 @@ type K8SSink struct { triggerCh chan struct{} } -// SetServices implements Sink +// SetServices implements Sink. func (s *K8SSink) SetServices(svcs map[string]string) { s.lock.Lock() defer s.lock.Unlock() @@ -105,11 +108,11 @@ func (s *K8SSink) Informer() cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { - return s.Client.CoreV1().Services(s.namespace()).List(context.TODO(), options) + return s.Client.CoreV1().Services(s.namespace()).List(s.Ctx, options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - return s.Client.CoreV1().Services(s.namespace()).Watch(context.TODO(), options) + return s.Client.CoreV1().Services(s.namespace()).Watch(s.Ctx, options) }, }, &apiv1.Service{}, @@ -207,7 +210,7 @@ func (s *K8SSink) Run(ch <-chan struct{}) { return case <-triggerCh: // Coalesce to prevent lots of API calls during churn periods. - coalesce.Coalesce(context.TODO(), + coalesce.Coalesce(s.Ctx, K8SQuietPeriod, K8SMaxPeriod, func(ctx context.Context) { select { @@ -224,20 +227,20 @@ func (s *K8SSink) Run(ch <-chan struct{}) { svcClient := s.Client.CoreV1().Services(s.namespace()) for _, name := range delete { - if err := svcClient.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil { + if err := svcClient.Delete(s.Ctx, name, metav1.DeleteOptions{}); err != nil { s.Log.Warn("error deleting service", "name", name, "error", err) } } for _, svc := range update { - _, err := svcClient.Update(context.TODO(), svc, metav1.UpdateOptions{}) + _, err := svcClient.Update(s.Ctx, svc, metav1.UpdateOptions{}) if err != nil { s.Log.Warn("error updating service", "name", svc.Name, "error", err) } } for _, svc := range create { - _, err := svcClient.Create(context.TODO(), svc, metav1.CreateOptions{}) + _, err := svcClient.Create(s.Ctx, svc, metav1.CreateOptions{}) if err != nil { s.Log.Warn("error creating service", "name", svc.Name, "error", err) } diff --git a/control-plane/catalog/to-k8s/sink_test.go b/control-plane/catalog/to-k8s/sink_test.go index d66102cde2..fbce7bbaaf 100644 --- a/control-plane/catalog/to-k8s/sink_test.go +++ b/control-plane/catalog/to-k8s/sink_test.go @@ -150,8 +150,7 @@ func TestK8SSink_createExists(t *testing.T) { if actual == nil { r.Fatal("web not found") - } - if actual.Spec.ExternalName != "example.com." { + } else if actual.Spec.ExternalName != "example.com." { r.Fatal("modified") } }) @@ -222,7 +221,7 @@ func TestK8SSink_updateReconcile(t *testing.T) { }) } -// Test that if the service is updated locally, it is reconciled +// Test that if the service is updated locally, it is reconciled. func TestK8SSink_updateService(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -285,7 +284,7 @@ func TestK8SSink_updateService(t *testing.T) { }) } -// Test that if the service is deleted remotely, it is recreated +// Test that if the service is deleted remotely, it is recreated. func TestK8SSink_deleteReconcileRemote(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -348,7 +347,7 @@ func TestK8SSink_deleteReconcileRemote(t *testing.T) { }) } -// Test that if the service is deleted locally, it is recreated +// Test that if the service is deleted locally, it is recreated. func TestK8SSink_deleteReconcileLocal(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() @@ -398,6 +397,7 @@ func testSink(t *testing.T, client kubernetes.Interface) (*K8SSink, func()) { sink := &K8SSink{ Client: client, Log: hclog.Default(), + Ctx: context.Background(), } closer := controller.TestControllerRun(sink) diff --git a/control-plane/catalog/to-k8s/source_test.go b/control-plane/catalog/to-k8s/source_test.go index f7e97fe19f..d3ed4a8a26 100644 --- a/control-plane/catalog/to-k8s/source_test.go +++ b/control-plane/catalog/to-k8s/source_test.go @@ -259,7 +259,7 @@ func TestSource_deleteServiceInstance(t *testing.T) { }) } -// testRegistration creates a Consul test registration +// testRegistration creates a Consul test registration. func testRegistration(node, service string, tags []string) *api.CatalogRegistration { return &api.CatalogRegistration{ Node: node, @@ -271,13 +271,13 @@ func testRegistration(node, service string, tags []string) *api.CatalogRegistrat } } -// testSource creates a Source and Sink for testing +// testSource creates a Source and Sink for testing. func testSource(client *api.Client) (*Source, *TestSink, func()) { return testSourceWithConfig(client, func(source *Source) {}) } // testSourceWithConfig starts a Source that can be configured -// prior to starting via the configurator method +// prior to starting via the configurator method. func testSourceWithConfig(client *api.Client, configurator func(*Source)) (*Source, *TestSink, func()) { sink := &TestSink{} s := &Source{ diff --git a/control-plane/commands.go b/control-plane/commands.go index a08c15b611..db43863642 100644 --- a/control-plane/commands.go +++ b/control-plane/commands.go @@ -10,7 +10,9 @@ import ( cmdCreateFederationSecret "github.com/hashicorp/consul-k8s/control-plane/subcommand/create-federation-secret" cmdDeleteCompletedJob "github.com/hashicorp/consul-k8s/control-plane/subcommand/delete-completed-job" cmdGetConsulClientCA "github.com/hashicorp/consul-k8s/control-plane/subcommand/get-consul-client-ca" + cmdGossipEncryptionAutogenerate "github.com/hashicorp/consul-k8s/control-plane/subcommand/gossip-encryption-autogenerate" cmdInjectConnect "github.com/hashicorp/consul-k8s/control-plane/subcommand/inject-connect" + cmdPartitionInit "github.com/hashicorp/consul-k8s/control-plane/subcommand/partition-init" cmdServerACLInit "github.com/hashicorp/consul-k8s/control-plane/subcommand/server-acl-init" cmdServiceAddress "github.com/hashicorp/consul-k8s/control-plane/subcommand/service-address" cmdSyncCatalog "github.com/hashicorp/consul-k8s/control-plane/subcommand/sync-catalog" @@ -48,6 +50,10 @@ func init() { return &cmdServerACLInit.Command{UI: ui}, nil }, + "partition-init": func() (cli.Command, error) { + return &cmdPartitionInit.Command{UI: ui}, nil + }, + "sync-catalog": func() (cli.Command, error) { return &cmdSyncCatalog.Command{UI: ui}, nil }, @@ -83,6 +89,10 @@ func init() { "tls-init": func() (cli.Command, error) { return &cmdTLSInit.Command{UI: ui}, nil }, + + "gossip-encryption-autogenerate": func() (cli.Command, error) { + return &cmdGossipEncryptionAutogenerate.Command{UI: ui}, nil + }, } } diff --git a/control-plane/config/certmanager/certificate.yaml b/control-plane/config/certmanager/certificate.yaml deleted file mode 100644 index 58db114fa0..0000000000 --- a/control-plane/config/certmanager/certificate.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# The following manifests contain a self-signed issuer CR and a certificate CR. -# More document can be found at https://docs.cert-manager.io -# WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for -# breaking changes -apiVersion: cert-manager.io/v1alpha2 -kind: Issuer -metadata: - name: selfsigned-issuer - namespace: system -spec: - selfSigned: {} ---- -apiVersion: cert-manager.io/v1alpha2 -kind: Certificate -metadata: - name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml - namespace: system -spec: - # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize - dnsNames: - - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc - - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local - issuerRef: - kind: Issuer - name: selfsigned-issuer - secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/control-plane/config/certmanager/kustomization.yaml b/control-plane/config/certmanager/kustomization.yaml deleted file mode 100644 index bebea5a595..0000000000 --- a/control-plane/config/certmanager/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -resources: -- certificate.yaml - -configurations: -- kustomizeconfig.yaml diff --git a/control-plane/config/certmanager/kustomizeconfig.yaml b/control-plane/config/certmanager/kustomizeconfig.yaml deleted file mode 100644 index 90d7c313ca..0000000000 --- a/control-plane/config/certmanager/kustomizeconfig.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# This configuration is for teaching kustomize how to update name ref and var substitution -nameReference: -- kind: Issuer - group: cert-manager.io - fieldSpecs: - - kind: Certificate - group: cert-manager.io - path: spec/issuerRef/name - -varReference: -- kind: Certificate - group: cert-manager.io - path: spec/commonName -- kind: Certificate - group: cert-manager.io - path: spec/dnsNames diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_exportedservices.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_exportedservices.yaml new file mode 100644 index 0000000000..c8e10c6f09 --- /dev/null +++ b/control-plane/config/crd/bases/consul.hashicorp.com_exportedservices.yaml @@ -0,0 +1,132 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.0 + creationTimestamp: null + name: exportedservices.consul.hashicorp.com +spec: + group: consul.hashicorp.com + names: + kind: ExportedServices + listKind: ExportedServicesList + plural: exportedservices + shortNames: + - exported-services + singular: exportedservices + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The sync status of the resource with Consul + jsonPath: .status.conditions[?(@.type=="Synced")].status + name: Synced + type: string + - description: The last successful synced time of the resource with Consul + jsonPath: .status.lastSyncedTime + name: Last Synced + type: date + - description: The age of the resource + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExportedServices is the Schema for the exportedservices API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ExportedServicesSpec defines the desired state of ExportedServices + properties: + services: + description: Services is a list of services to be exported and the + list of partitions to expose them to. + items: + description: ExportedService manages the exporting of a service + in the local partition to other partitions. + properties: + consumers: + description: Consumers is a list of downstream consumers of + the service to be exported. + items: + description: ServiceConsumer represents a downstream consumer + of the service to be exported. + properties: + partition: + description: Partition is the admin partition to export + the service to. + type: string + type: object + type: array + name: + description: Name is the name of the service to be exported. + type: string + namespace: + description: Namespace is the namespace to export the service + from. + type: string + type: object + type: array + type: object + status: + properties: + conditions: + description: Conditions indicate the latest available observations + of a resource's current state. + items: + description: 'Conditions define a readiness condition for a Consul + resource. See: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties' + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition. + type: string + required: + - status + - type + type: object + type: array + lastSyncedTime: + description: LastSyncedTime is the last time the resource successfully + synced with Consul. + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml index 63baf9787b..7ea78b875b 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml @@ -13,6 +13,8 @@ spec: kind: IngressGateway listKind: IngressGatewayList plural: ingressgateways + shortNames: + - ingress-gateway singular: ingressgateway scope: Namespaced versions: @@ -56,6 +58,25 @@ spec: description: IngressListener manages the configuration for a listener on a specific port. properties: + tls: + description: TLS config for this listener. + properties: + enabled: + description: Indicates that TLS should be enabled for this + gateway service. + type: boolean + sds: + description: SDS allows configuring TLS certificate from + an SDS service. + properties: + certResource: + type: string + clusterName: + type: string + type: object + required: + - enabled + type: object port: description: Port declares the port on which the ingress gateway should listen for traffic. @@ -76,6 +97,20 @@ spec: description: IngressService manages configuration for services that are exposed to ingress traffic. properties: + tls: + description: TLS allows specifying some TLS configuration + per listener. + properties: + sds: + description: SDS allows configuring TLS certificate + from an SDS service. + properties: + certResource: + type: string + clusterName: + type: string + type: object + type: object hosts: description: "Hosts is a list of hostnames which should be associated to this service on the defined listener. @@ -103,6 +138,63 @@ spec: description: Namespace is the namespace where the service is located. Namespacing is a Consul Enterprise feature. type: string + partition: + description: Partition is the admin-partition where the + service is located. Partitioning is a Consul Enterprise + feature. + type: string + requestHeaders: + description: Allow HTTP header manipulation to be configured. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object + responseHeaders: + description: HTTPHeaderModifiers is a set of rules for + HTTP header modification that should be performed by + proxies as the request passes through them. It can operate + on either request or response headers depending on the + context in which it is used. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object type: object type: array type: object @@ -114,6 +206,15 @@ spec: description: Indicates that TLS should be enabled for this gateway service. type: boolean + sds: + description: SDS allows configuring TLS certificate from an SDS + service. + properties: + certResource: + type: string + clusterName: + type: string + type: object required: - enabled type: object diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml index f440d012c8..520fe95443 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml @@ -13,6 +13,8 @@ spec: kind: ProxyDefaults listKind: ProxyDefaultsList plural: proxydefaults + shortNames: + - proxy-defaults singular: proxydefaults scope: Namespaced versions: diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_servicedefaults.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_servicedefaults.yaml index bed1204c43..b29905b01a 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_servicedefaults.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_servicedefaults.yaml @@ -13,6 +13,8 @@ spec: kind: ServiceDefaults listKind: ServiceDefaultsList plural: servicedefaults + shortNames: + - service-defaults singular: servicedefaults scope: Namespaced versions: @@ -200,6 +202,10 @@ spec: description: Namespace is only accepted within a service-defaults config entry. type: string + partition: + description: Partition is only accepted within a service-defaults + config entry. + type: string passiveHealthCheck: description: PassiveHealthCheck configuration determines how upstream proxy instances will be monitored for removal from @@ -288,6 +294,10 @@ spec: description: Namespace is only accepted within a service-defaults config entry. type: string + partition: + description: Partition is only accepted within a service-defaults + config entry. + type: string passiveHealthCheck: description: PassiveHealthCheck configuration determines how upstream proxy instances will be monitored for removal diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml index b2af5c1ea2..1593fc86fd 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_serviceintentions.yaml @@ -13,6 +13,8 @@ spec: kind: ServiceIntentions listKind: ServiceIntentionsList plural: serviceintentions + shortNames: + - service-intentions singular: serviceintentions scope: Namespaced versions: @@ -90,6 +92,9 @@ spec: namespace: description: Namespace is the namespace for the Name parameter. type: string + partition: + description: Partition is the Admin Partition for the Name parameter. + type: string permissions: description: Permissions is the list of all additional L7 attributes that extend the intention match criteria. Permission precedence diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml index 0be581e8b1..c170465322 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml @@ -13,6 +13,8 @@ spec: kind: ServiceResolver listKind: ServiceResolverList plural: serviceresolvers + shortNames: + - service-resolver singular: serviceresolver scope: Namespaced versions: @@ -180,8 +182,14 @@ spec: from instead of the current one. type: string namespace: - description: Namespace is the namespace to resolve the service - from instead of the current one. + description: Namespace is the Consul namespace to resolve the + service from instead of the current namespace. If empty the + current namespace is assumed. + type: string + partition: + description: Partition is the Consul partition to resolve the + service from instead of the current partition. If empty the + current partition is assumed. type: string service: description: Service is a service to resolve instead of the current diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_servicerouters.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_servicerouters.yaml index 0919eafb75..77707c0770 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_servicerouters.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_servicerouters.yaml @@ -13,6 +13,8 @@ spec: kind: ServiceRouter listKind: ServiceRouterList plural: servicerouters + shortNames: + - service-router singular: servicerouter scope: Namespaced versions: @@ -70,17 +72,74 @@ spec: the request when a retryable result occurs format: int32 type: integer + partition: + description: Partition is the Consul partition to resolve + the service from instead of the current partition. If + empty the current partition is assumed. + type: string prefixRewrite: description: PrefixRewrite defines how to rewrite the HTTP request path before proxying it to its final destination. This requires that either match.http.pathPrefix or match.http.pathExact be configured on this route. type: string + requestHeaders: + description: Allow HTTP header manipulation to be configured. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object requestTimeout: description: RequestTimeout is the total amount of time permitted for the entire downstream request (and retries) to be processed. type: string + responseHeaders: + description: HTTPHeaderModifiers is a set of rules for HTTP + header modification that should be performed by proxies + as the request passes through them. It can operate on + either request or response headers depending on the context + in which it is used. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that + should be appended to the request or response (i.e. + allowing duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that + should be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that + should be added to the request or response, overwriting + any existing header values of the same name. + type: object + type: object retryOnConnectFailure: description: RetryOnConnectFailure allows for connection failure errors to trigger a retry. diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_servicesplitters.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_servicesplitters.yaml index 95ab93b617..204179c000 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_servicesplitters.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_servicesplitters.yaml @@ -13,6 +13,8 @@ spec: kind: ServiceSplitter listKind: ServiceSplitterList plural: servicesplitters + shortNames: + - service-splitter singular: servicesplitter scope: Namespaced versions: @@ -56,10 +58,67 @@ spec: items: properties: namespace: - description: The namespace to resolve the service from instead - of the current namespace. If empty the current namespace is - assumed. + description: Namespace is the Consul namespace to resolve the + service from instead of the current namespace. If empty the + current namespace is assumed. type: string + partition: + description: Partition is the Consul partition to resolve the + service from instead of the current partition. If empty the + current partition is assumed. + type: string + requestHeaders: + description: Allow HTTP header manipulation to be configured. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that should + be appended to the request or response (i.e. allowing + duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that should + be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that should + be added to the request or response, overwriting any existing + header values of the same name. + type: object + type: object + responseHeaders: + description: HTTPHeaderModifiers is a set of rules for HTTP + header modification that should be performed by proxies as + the request passes through them. It can operate on either + request or response headers depending on the context in which + it is used. + properties: + add: + additionalProperties: + type: string + description: Add is a set of name -> value pairs that should + be appended to the request or response (i.e. allowing + duplicates if the same header already exists). + type: object + remove: + description: Remove is the set of header names that should + be stripped from the request or response. + items: + type: string + type: array + set: + additionalProperties: + type: string + description: Set is a set of name -> value pairs that should + be added to the request or response, overwriting any existing + header values of the same name. + type: object + type: object service: description: Service is the service to resolve instead of the default. diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_terminatinggateways.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_terminatinggateways.yaml index ce87093245..716f22e4ef 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_terminatinggateways.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_terminatinggateways.yaml @@ -13,6 +13,8 @@ spec: kind: TerminatingGateway listKind: TerminatingGatewayList plural: terminatinggateways + shortNames: + - terminating-gateway singular: terminatinggateway scope: Namespaced versions: diff --git a/control-plane/config/crd/kustomization.yaml b/control-plane/config/crd/kustomization.yaml deleted file mode 100644 index ca5496003f..0000000000 --- a/control-plane/config/crd/kustomization.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# This kustomization.yaml is not intended to be run by itself, -# since it depends on service name and namespace that are out of this kustomize package. -# It should be run by config/default -resources: -- bases/consul.hashicorp.com_servicedefaults.yaml -- bases/consul.hashicorp.com_serviceresolvers.yaml -- bases/consul.hashicorp.com_proxydefaults.yaml -- bases/consul.hashicorp.com_servicerouters.yaml -- bases/consul.hashicorp.com_serviceintentions.yaml -- bases/consul.hashicorp.com_ingressgateways.yaml -- bases/consul.hashicorp.com_terminatinggateways.yaml -- bases/consul.hashicorp.com_meshes.yaml -# +kubebuilder:scaffold:crdkustomizeresource - -patchesStrategicMerge: -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. -# patches here are for enabling the conversion webhook for each CRD -- patches/webhook_in_servicedefaults.yaml -- patches/webhook_in_serviceresolvers.yaml -- patches/webhook_in_proxydefaults.yaml -- patches/webhook_in_servicerouters.yaml -- patches/webhook_in_serviceintentions.yaml -- patches/webhook_in_ingressgateways.yaml -- patches/webhook_in_terminatinggateways.yaml -#- patches/webhook_in_meshes.yaml -# +kubebuilder:scaffold:crdkustomizewebhookpatch - -# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. -# patches here are for enabling the CA injection for each CRD -#- patches/cainjection_in_servicedefaults.yaml -#- patches/cainjection_in_serviceresolvers.yaml -#- patches/cainjection_in_proxydefaults.yaml -#- patches/cainjection_in_servicerouters.yaml -#- patches/cainjection_in_serviceintentions.yaml -#- patches/cainjection_in_ingressgateways.yaml -#- patches/cainjection_in_terminatinggateways.yaml -#- patches/cainjection_in_meshes.yaml -# +kubebuilder:scaffold:crdkustomizecainjectionpatch - -# the following config is for teaching kustomize how to do kustomization for CRDs. -configurations: -- kustomizeconfig.yaml diff --git a/control-plane/config/crd/kustomizeconfig.yaml b/control-plane/config/crd/kustomizeconfig.yaml deleted file mode 100644 index 6f83d9a94b..0000000000 --- a/control-plane/config/crd/kustomizeconfig.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# This file is for teaching kustomize how to substitute name and namespace reference in CRD -nameReference: -- kind: Service - version: v1 - fieldSpecs: - - kind: CustomResourceDefinition - group: apiextensions.k8s.io - path: spec/conversion/webhookClientConfig/service/name - -namespace: -- kind: CustomResourceDefinition - group: apiextensions.k8s.io - path: spec/conversion/webhookClientConfig/service/namespace - create: false - -varReference: -- path: metadata/annotations diff --git a/control-plane/config/crd/patches/cainjection_in_ingressgateways.yaml b/control-plane/config/crd/patches/cainjection_in_ingressgateways.yaml deleted file mode 100644 index aa1074fa2b..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_ingressgateways.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: ingressgateways.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/cainjection_in_proxydefaults.yaml b/control-plane/config/crd/patches/cainjection_in_proxydefaults.yaml deleted file mode 100644 index df0460c54c..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_proxydefaults.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: proxydefaults.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/cainjection_in_servicedefaults.yaml b/control-plane/config/crd/patches/cainjection_in_servicedefaults.yaml deleted file mode 100644 index 3ae80de8ce..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_servicedefaults.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: servicedefaults.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/cainjection_in_serviceintentions.yaml b/control-plane/config/crd/patches/cainjection_in_serviceintentions.yaml deleted file mode 100644 index e53864ab64..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_serviceintentions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: serviceintentions.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/cainjection_in_serviceresolvers.yaml b/control-plane/config/crd/patches/cainjection_in_serviceresolvers.yaml deleted file mode 100644 index 39a09eb8e1..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_serviceresolvers.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: serviceresolvers.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/cainjection_in_servicerouters.yaml b/control-plane/config/crd/patches/cainjection_in_servicerouters.yaml deleted file mode 100644 index 798e76fd2a..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_servicerouters.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: servicerouters.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/cainjection_in_servicesplitters.yaml b/control-plane/config/crd/patches/cainjection_in_servicesplitters.yaml deleted file mode 100644 index 288ca2cf23..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_servicesplitters.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: servicesplitters.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/cainjection_in_terminatinggateways.yaml b/control-plane/config/crd/patches/cainjection_in_terminatinggateways.yaml deleted file mode 100644 index 9423b79fbf..0000000000 --- a/control-plane/config/crd/patches/cainjection_in_terminatinggateways.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: terminatinggateways.consul.hashicorp.com diff --git a/control-plane/config/crd/patches/webhook_in_ingressgateways.yaml b/control-plane/config/crd/patches/webhook_in_ingressgateways.yaml deleted file mode 100644 index fcc6f69fa5..0000000000 --- a/control-plane/config/crd/patches/webhook_in_ingressgateways.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: ingressgateways.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/crd/patches/webhook_in_proxydefaults.yaml b/control-plane/config/crd/patches/webhook_in_proxydefaults.yaml deleted file mode 100644 index 4d2d4bfff1..0000000000 --- a/control-plane/config/crd/patches/webhook_in_proxydefaults.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: proxydefaults.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/crd/patches/webhook_in_servicedefaults.yaml b/control-plane/config/crd/patches/webhook_in_servicedefaults.yaml deleted file mode 100644 index 774953f50f..0000000000 --- a/control-plane/config/crd/patches/webhook_in_servicedefaults.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: servicedefaults.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/crd/patches/webhook_in_serviceintentions.yaml b/control-plane/config/crd/patches/webhook_in_serviceintentions.yaml deleted file mode 100644 index 77fcdb48ff..0000000000 --- a/control-plane/config/crd/patches/webhook_in_serviceintentions.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: serviceintentions.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/crd/patches/webhook_in_serviceresolvers.yaml b/control-plane/config/crd/patches/webhook_in_serviceresolvers.yaml deleted file mode 100644 index b9f9b42a21..0000000000 --- a/control-plane/config/crd/patches/webhook_in_serviceresolvers.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: serviceresolvers.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/crd/patches/webhook_in_servicerouters.yaml b/control-plane/config/crd/patches/webhook_in_servicerouters.yaml deleted file mode 100644 index e4856434c8..0000000000 --- a/control-plane/config/crd/patches/webhook_in_servicerouters.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: servicerouters.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/crd/patches/webhook_in_servicesplitters.yaml b/control-plane/config/crd/patches/webhook_in_servicesplitters.yaml deleted file mode 100644 index e8f7c1371f..0000000000 --- a/control-plane/config/crd/patches/webhook_in_servicesplitters.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: servicesplitters.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/crd/patches/webhook_in_terminatinggateways.yaml b/control-plane/config/crd/patches/webhook_in_terminatinggateways.yaml deleted file mode 100644 index 05063cc954..0000000000 --- a/control-plane/config/crd/patches/webhook_in_terminatinggateways.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# The following patch enables conversion webhook for CRD -# CRD conversion requires k8s 1.13 or later. -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: terminatinggateways.consul.hashicorp.com -spec: - conversion: - strategy: Webhook - webhookClientConfig: - # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, - # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) - caBundle: Cg== - service: - namespace: system - name: webhook-service - path: /convert diff --git a/control-plane/config/default/kustomization.yaml b/control-plane/config/default/kustomization.yaml deleted file mode 100644 index df978482e5..0000000000 --- a/control-plane/config/default/kustomization.yaml +++ /dev/null @@ -1,74 +0,0 @@ -# Adds namespace to all resources. -namespace: default - -# Value of this field is prepended to the -# names of all resources, e.g. a deployment named -# "wordpress" becomes "alices-wordpress". -# Note that it should also match with the prefix (text before '-') of the namespace -# field above. -namePrefix: consul-controller- - -# Labels to add to all resources and selectors. -#commonLabels: -# someName: someValue - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -#- ../prometheus - - # Protect the /metrics endpoint by putting it behind auth. - # If you want your controller-manager to expose the /metrics - # endpoint w/o any authn/z, please comment the following line. - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. -# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. -# 'CERTMANAGER' needs to be enabled to use ca injection -patchesStrategicMerge: -- manager_auth_proxy_patch.yaml -- manager_webhook_patch.yaml -- webhookcainjection_patch.yaml - -# the following config is for teaching kustomize how to do var substitution -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -vars: -- fieldref: - fieldPath: metadata.namespace - name: CERTIFICATE_NAMESPACE - objref: - group: cert-manager.io - kind: Certificate - name: serving-cert - version: v1alpha2 -- fieldref: {} - name: CERTIFICATE_NAME - objref: - group: cert-manager.io - kind: Certificate - name: serving-cert - version: v1alpha2 -- fieldref: - fieldPath: metadata.namespace - name: SERVICE_NAMESPACE - objref: - kind: Service - name: webhook-service - version: v1 -- fieldref: {} - name: SERVICE_NAME - objref: - kind: Service - name: webhook-service - version: v1 -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- ../crd -- ../rbac -- ../manager -- ../webhook -- ../certmanager diff --git a/control-plane/config/default/manager_auth_proxy_patch.yaml b/control-plane/config/default/manager_auth_proxy_patch.yaml deleted file mode 100644 index 77e743d1c1..0000000000 --- a/control-plane/config/default/manager_auth_proxy_patch.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# This patch inject a sidecar container which is a HTTP proxy for the -# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: kube-rbac-proxy - image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 - args: - - "--secure-listen-address=0.0.0.0:8443" - - "--upstream=http://127.0.0.1:8080/" - - "--logtostderr=true" - - "--v=10" - ports: - - containerPort: 8443 - name: https - - name: manager - args: - - "--metrics-addr=127.0.0.1:8080" - - "--enable-leader-election" diff --git a/control-plane/config/default/manager_webhook_patch.yaml b/control-plane/config/default/manager_webhook_patch.yaml deleted file mode 100644 index 738de350b7..0000000000 --- a/control-plane/config/default/manager_webhook_patch.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system -spec: - template: - spec: - containers: - - name: manager - ports: - - containerPort: 9443 - name: webhook-server - protocol: TCP - volumeMounts: - - mountPath: /tmp/k8s-webhook-server/serving-certs - name: cert - readOnly: true - volumes: - - name: cert - secret: - defaultMode: 420 - secretName: webhook-server-cert diff --git a/control-plane/config/default/webhookcainjection_patch.yaml b/control-plane/config/default/webhookcainjection_patch.yaml deleted file mode 100644 index 79c5b20a63..0000000000 --- a/control-plane/config/default/webhookcainjection_patch.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# This patch add annotation to admission webhook config and -# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. -apiVersion: admissionregistration.k8s.io/v1beta1 -kind: MutatingWebhookConfiguration -metadata: - name: mutating-webhook-configuration - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/control-plane/config/manager/kustomization.yaml b/control-plane/config/manager/kustomization.yaml deleted file mode 100644 index 957b423a0a..0000000000 --- a/control-plane/config/manager/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -resources: -- manager.yaml -# todo: this was auto-generated -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -images: -- name: controller - newName: ashwinvenkatesh/consul-k8s - newTag: latest diff --git a/control-plane/config/manager/manager.yaml b/control-plane/config/manager/manager.yaml deleted file mode 100644 index b086533218..0000000000 --- a/control-plane/config/manager/manager.yaml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - labels: - control-plane: controller-manager - name: system ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system - labels: - control-plane: controller-manager -spec: - selector: - matchLabels: - control-plane: controller-manager - replicas: 1 - template: - metadata: - labels: - control-plane: controller-manager - spec: - containers: - - name: manager - env: - - name: HOST_IP - valueFrom: - fieldRef: - fieldPath: status.hostIP - - name: CONSUL_HTTP_ADDR - value: http://$(HOST_IP):8500 - command: - - consul-k8s - - controller - args: - - --enable-leader-election - image: controller:latest - resources: - limits: - cpu: 100m - memory: 30Mi - requests: - cpu: 100m - memory: 20Mi - - terminationGracePeriodSeconds: 10 diff --git a/control-plane/config/rbac/auth_proxy_client_clusterrole.yaml b/control-plane/config/rbac/auth_proxy_client_clusterrole.yaml deleted file mode 100644 index 7d62534c5f..0000000000 --- a/control-plane/config/rbac/auth_proxy_client_clusterrole.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - name: metrics-reader -rules: -- nonResourceURLs: ["/metrics"] - verbs: ["get"] diff --git a/control-plane/config/rbac/auth_proxy_role.yaml b/control-plane/config/rbac/auth_proxy_role.yaml deleted file mode 100644 index 618f5e4177..0000000000 --- a/control-plane/config/rbac/auth_proxy_role.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: proxy-role -rules: -- apiGroups: ["authentication.k8s.io"] - resources: - - tokenreviews - verbs: ["create"] -- apiGroups: ["authorization.k8s.io"] - resources: - - subjectaccessreviews - verbs: ["create"] diff --git a/control-plane/config/rbac/auth_proxy_role_binding.yaml b/control-plane/config/rbac/auth_proxy_role_binding.yaml deleted file mode 100644 index 48ed1e4b85..0000000000 --- a/control-plane/config/rbac/auth_proxy_role_binding.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: proxy-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: proxy-role -subjects: -- kind: ServiceAccount - name: default - namespace: system diff --git a/control-plane/config/rbac/auth_proxy_service.yaml b/control-plane/config/rbac/auth_proxy_service.yaml deleted file mode 100644 index 6cf656be14..0000000000 --- a/control-plane/config/rbac/auth_proxy_service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - control-plane: controller-manager - name: controller-manager-metrics-service - namespace: system -spec: - ports: - - name: https - port: 8443 - targetPort: https - selector: - control-plane: controller-manager diff --git a/control-plane/config/rbac/ingressgateway_editor_role.yaml b/control-plane/config/rbac/ingressgateway_editor_role.yaml deleted file mode 100644 index 424e12e33c..0000000000 --- a/control-plane/config/rbac/ingressgateway_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit ingressgateways. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: ingressgateway-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - ingressgateways - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - ingressgateways/status - verbs: - - get diff --git a/control-plane/config/rbac/ingressgateway_viewer_role.yaml b/control-plane/config/rbac/ingressgateway_viewer_role.yaml deleted file mode 100644 index 82ca5e79db..0000000000 --- a/control-plane/config/rbac/ingressgateway_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view ingressgateways. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: ingressgateway-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - ingressgateways - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - ingressgateways/status - verbs: - - get diff --git a/control-plane/config/rbac/kustomization.yaml b/control-plane/config/rbac/kustomization.yaml deleted file mode 100644 index 66c28338fe..0000000000 --- a/control-plane/config/rbac/kustomization.yaml +++ /dev/null @@ -1,12 +0,0 @@ -resources: -- role.yaml -- role_binding.yaml -- leader_election_role.yaml -- leader_election_role_binding.yaml -# Comment the following 4 lines if you want to disable -# the auth proxy (https://github.com/brancz/kube-rbac-proxy) -# which protects your /metrics endpoint. -- auth_proxy_service.yaml -- auth_proxy_role.yaml -- auth_proxy_role_binding.yaml -- auth_proxy_client_clusterrole.yaml diff --git a/control-plane/config/rbac/leader_election_role.yaml b/control-plane/config/rbac/leader_election_role.yaml deleted file mode 100644 index 7dc16c420e..0000000000 --- a/control-plane/config/rbac/leader_election_role.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# permissions to do leader election. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: leader-election-role -rules: -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - "" - resources: - - configmaps/status - verbs: - - get - - update - - patch -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch diff --git a/control-plane/config/rbac/leader_election_role_binding.yaml b/control-plane/config/rbac/leader_election_role_binding.yaml deleted file mode 100644 index eed16906f4..0000000000 --- a/control-plane/config/rbac/leader_election_role_binding.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: leader-election-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: leader-election-role -subjects: -- kind: ServiceAccount - name: default - namespace: system diff --git a/control-plane/config/rbac/proxydefaults_editor_role.yaml b/control-plane/config/rbac/proxydefaults_editor_role.yaml deleted file mode 100644 index 42afc1a916..0000000000 --- a/control-plane/config/rbac/proxydefaults_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit proxydefaults. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: proxydefaults-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - proxydefaults - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - proxydefaults/status - verbs: - - get diff --git a/control-plane/config/rbac/proxydefaults_viewer_role.yaml b/control-plane/config/rbac/proxydefaults_viewer_role.yaml deleted file mode 100644 index b16fda3894..0000000000 --- a/control-plane/config/rbac/proxydefaults_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view proxydefaults. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: proxydefaults-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - proxydefaults - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - proxydefaults/status - verbs: - - get diff --git a/control-plane/config/rbac/role.yaml b/control-plane/config/rbac/role.yaml index 734e80da0f..6009210f4b 100644 --- a/control-plane/config/rbac/role.yaml +++ b/control-plane/config/rbac/role.yaml @@ -6,6 +6,26 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - consul.hashicorp.com + resources: + - exportedservices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - consul.hashicorp.com + resources: + - exportedservices/status + verbs: + - get + - patch + - update - apiGroups: - consul.hashicorp.com resources: diff --git a/control-plane/config/rbac/role_binding.yaml b/control-plane/config/rbac/role_binding.yaml deleted file mode 100644 index 8f2658702c..0000000000 --- a/control-plane/config/rbac/role_binding.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: manager-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: manager-role -subjects: -- kind: ServiceAccount - name: default - namespace: system diff --git a/control-plane/config/rbac/servicedefaults_editor_role.yaml b/control-plane/config/rbac/servicedefaults_editor_role.yaml deleted file mode 100644 index f5a7d27a26..0000000000 --- a/control-plane/config/rbac/servicedefaults_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit servicedefaults. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: servicedefaults-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - servicedefaults - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - servicedefaults/status - verbs: - - get diff --git a/control-plane/config/rbac/servicedefaults_viewer_role.yaml b/control-plane/config/rbac/servicedefaults_viewer_role.yaml deleted file mode 100644 index ac9ccc9b3b..0000000000 --- a/control-plane/config/rbac/servicedefaults_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view servicedefaults. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: servicedefaults-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - servicedefaults - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - servicedefaults/status - verbs: - - get diff --git a/control-plane/config/rbac/serviceintentions_editor_role.yaml b/control-plane/config/rbac/serviceintentions_editor_role.yaml deleted file mode 100644 index 83a0437d7d..0000000000 --- a/control-plane/config/rbac/serviceintentions_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit serviceintentions. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: serviceintentions-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - serviceintentions - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - serviceintentions/status - verbs: - - get diff --git a/control-plane/config/rbac/serviceintentions_viewer_role.yaml b/control-plane/config/rbac/serviceintentions_viewer_role.yaml deleted file mode 100644 index 6a5f41c960..0000000000 --- a/control-plane/config/rbac/serviceintentions_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view serviceintentions. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: serviceintentions-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - serviceintentions - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - serviceintentions/status - verbs: - - get diff --git a/control-plane/config/rbac/serviceresolver_editor_role.yaml b/control-plane/config/rbac/serviceresolver_editor_role.yaml deleted file mode 100644 index 5baff84934..0000000000 --- a/control-plane/config/rbac/serviceresolver_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit serviceresolvers. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: serviceresolver-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - serviceresolvers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - serviceresolvers/status - verbs: - - get diff --git a/control-plane/config/rbac/serviceresolver_viewer_role.yaml b/control-plane/config/rbac/serviceresolver_viewer_role.yaml deleted file mode 100644 index ca990258fb..0000000000 --- a/control-plane/config/rbac/serviceresolver_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view serviceresolvers. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: serviceresolver-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - serviceresolvers - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - serviceresolvers/status - verbs: - - get diff --git a/control-plane/config/rbac/servicerouter_editor_role.yaml b/control-plane/config/rbac/servicerouter_editor_role.yaml deleted file mode 100644 index c66e6c1ddd..0000000000 --- a/control-plane/config/rbac/servicerouter_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit servicerouters. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: servicerouter-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - servicerouters - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - servicerouters/status - verbs: - - get diff --git a/control-plane/config/rbac/servicerouter_viewer_role.yaml b/control-plane/config/rbac/servicerouter_viewer_role.yaml deleted file mode 100644 index c2cb68dfe8..0000000000 --- a/control-plane/config/rbac/servicerouter_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view servicerouters. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: servicerouter-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - servicerouters - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - servicerouters/status - verbs: - - get diff --git a/control-plane/config/rbac/servicesplitter_editor_role.yaml b/control-plane/config/rbac/servicesplitter_editor_role.yaml deleted file mode 100644 index ec08f8a114..0000000000 --- a/control-plane/config/rbac/servicesplitter_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit servicesplitters. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: servicesplitter-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - servicesplitters - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - servicesplitters/status - verbs: - - get diff --git a/control-plane/config/rbac/servicesplitter_viewer_role.yaml b/control-plane/config/rbac/servicesplitter_viewer_role.yaml deleted file mode 100644 index 6e9458243f..0000000000 --- a/control-plane/config/rbac/servicesplitter_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view servicesplitters. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: servicesplitter-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - servicesplitters - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - servicesplitters/status - verbs: - - get diff --git a/control-plane/config/rbac/terminatinggateway_editor_role.yaml b/control-plane/config/rbac/terminatinggateway_editor_role.yaml deleted file mode 100644 index 6d5da0b881..0000000000 --- a/control-plane/config/rbac/terminatinggateway_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit terminatinggateways. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: terminatinggateway-editor-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - terminatinggateways - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - terminatinggateways/status - verbs: - - get diff --git a/control-plane/config/rbac/terminatinggateway_viewer_role.yaml b/control-plane/config/rbac/terminatinggateway_viewer_role.yaml deleted file mode 100644 index 6f6c220d59..0000000000 --- a/control-plane/config/rbac/terminatinggateway_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view terminatinggateways. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: terminatinggateway-viewer-role -rules: -- apiGroups: - - consul.hashicorp.com - resources: - - terminatinggateways - verbs: - - get - - list - - watch -- apiGroups: - - consul.hashicorp.com - resources: - - terminatinggateways/status - verbs: - - get diff --git a/control-plane/config/samples/consul_v1alpha1_ingressgateway.yaml b/control-plane/config/samples/consul_v1alpha1_ingressgateway.yaml deleted file mode 100644 index 9a87883ed0..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_ingressgateway.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: IngressGateway -metadata: - name: ingressgateway-sample -spec: - tls: - enabled: false - listeners: - - port: 8080 - protocol: "tcp" - services: - - name: "foo" diff --git a/control-plane/config/samples/consul_v1alpha1_proxydefaults.yaml b/control-plane/config/samples/consul_v1alpha1_proxydefaults.yaml deleted file mode 100644 index 2a87f89ed7..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_proxydefaults.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ProxyDefaults -metadata: - name: proxydefaults-sample -spec: - # Add fields here - foo: bar diff --git a/control-plane/config/samples/consul_v1alpha1_servicedefaults.yaml b/control-plane/config/samples/consul_v1alpha1_servicedefaults.yaml deleted file mode 100644 index f395256b81..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_servicedefaults.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceDefaults -metadata: - name: servicedefaults-sample -spec: - protocol: "http" diff --git a/control-plane/config/samples/consul_v1alpha1_serviceintentions.yaml b/control-plane/config/samples/consul_v1alpha1_serviceintentions.yaml deleted file mode 100644 index 1c6dc83597..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_serviceintentions.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceIntentions -metadata: - name: serviceintentions-sample -spec: - # Add fields here - foo: bar diff --git a/control-plane/config/samples/consul_v1alpha1_serviceresolver.yaml b/control-plane/config/samples/consul_v1alpha1_serviceresolver.yaml deleted file mode 100644 index f1a2f29c8b..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_serviceresolver.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceResolver -metadata: - name: serviceresolver-sample -spec: - # Add fields here - foo: bar diff --git a/control-plane/config/samples/consul_v1alpha1_servicerouter.yaml b/control-plane/config/samples/consul_v1alpha1_servicerouter.yaml deleted file mode 100644 index df597031a9..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_servicerouter.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceRouter -metadata: - name: servicerouter-sample -spec: - routes: - - match: - http: - pathPrefix: "/admin" - destination: - service: admin diff --git a/control-plane/config/samples/consul_v1alpha1_servicesplitter.yaml b/control-plane/config/samples/consul_v1alpha1_servicesplitter.yaml deleted file mode 100644 index a432bd8117..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_servicesplitter.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: ServiceSplitter -metadata: - name: servicesplitter-sample -spec: - # Add fields here - foo: bar diff --git a/control-plane/config/samples/consul_v1alpha1_terminatinggateway.yaml b/control-plane/config/samples/consul_v1alpha1_terminatinggateway.yaml deleted file mode 100644 index 4708f6cb8a..0000000000 --- a/control-plane/config/samples/consul_v1alpha1_terminatinggateway.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: consul.hashicorp.com/v1alpha1 -kind: TerminatingGateway -metadata: - name: terminatinggateway-sample -spec: - services: - - name: name - caFile: "caFile" - certFile: "certFile" - keyFile: "keyFile" - sni: "sni" diff --git a/control-plane/config/samples/kustomization.yaml b/control-plane/config/samples/kustomization.yaml deleted file mode 100644 index c6cf924560..0000000000 --- a/control-plane/config/samples/kustomization.yaml +++ /dev/null @@ -1,3 +0,0 @@ -## This file is auto-generated, do not modify ## -resources: -- consul_v1alpha1_servicedefaults.yaml diff --git a/control-plane/config/webhook/kustomization.yaml b/control-plane/config/webhook/kustomization.yaml deleted file mode 100644 index 9cf26134e4..0000000000 --- a/control-plane/config/webhook/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -resources: -- manifests.yaml -- service.yaml - -configurations: -- kustomizeconfig.yaml diff --git a/control-plane/config/webhook/kustomizeconfig.yaml b/control-plane/config/webhook/kustomizeconfig.yaml deleted file mode 100644 index 25e21e3c96..0000000000 --- a/control-plane/config/webhook/kustomizeconfig.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# the following config is for teaching kustomize where to look at when substituting vars. -# It requires kustomize v2.1.0 or newer to work properly. -nameReference: -- kind: Service - version: v1 - fieldSpecs: - - kind: MutatingWebhookConfiguration - group: admissionregistration.k8s.io - path: webhooks/clientConfig/service/name - - kind: ValidatingWebhookConfiguration - group: admissionregistration.k8s.io - path: webhooks/clientConfig/service/name - -namespace: -- kind: MutatingWebhookConfiguration - group: admissionregistration.k8s.io - path: webhooks/clientConfig/service/namespace - create: true -- kind: ValidatingWebhookConfiguration - group: admissionregistration.k8s.io - path: webhooks/clientConfig/service/namespace - create: true - -varReference: -- path: metadata/annotations diff --git a/control-plane/config/webhook/manifests.yaml b/control-plane/config/webhook/manifests.yaml index 21d6cf6dec..ae0eb15fc5 100644 --- a/control-plane/config/webhook/manifests.yaml +++ b/control-plane/config/webhook/manifests.yaml @@ -6,6 +6,27 @@ metadata: creationTimestamp: null name: mutating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1beta1 + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-v1alpha1-exportedservices + failurePolicy: Fail + name: mutate-exportedservices.consul.hashicorp.com + rules: + - apiGroups: + - consul.hashicorp.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - exportedservices + sideEffects: None - admissionReviewVersions: - v1beta1 - v1 diff --git a/control-plane/config/webhook/service.yaml b/control-plane/config/webhook/service.yaml deleted file mode 100644 index 31e0f82959..0000000000 --- a/control-plane/config/webhook/service.yaml +++ /dev/null @@ -1,12 +0,0 @@ - -apiVersion: v1 -kind: Service -metadata: - name: webhook-service - namespace: system -spec: - ports: - - port: 443 - targetPort: 9443 - selector: - control-plane: controller-manager diff --git a/control-plane/connect-inject/annotations.go b/control-plane/connect-inject/annotations.go index e1dd440903..870a44ff50 100644 --- a/control-plane/connect-inject/annotations.go +++ b/control-plane/connect-inject/annotations.go @@ -13,7 +13,7 @@ const ( // annotationInject is the key of the annotation that controls whether // injection is explicitly enabled or disabled for a pod. This should - // be set to a truthy or falsy value, as parseable by strconv.ParseBool + // be set to a truthy or falsy value, as parseable by strconv.ParseBool. annotationInject = "consul.hashicorp.com/connect-inject" // annotationInjectMountVolumes is the key of the annotation that controls whether @@ -22,8 +22,8 @@ const ( // to mount the volume on. It will be mounted at the path `/consul/connect-inject` annotationInjectMountVolumes = "consul.hashicorp.com/connect-inject-mount-volume" - // annotationService is the name of the service to proxy. This defaults - // to the name of the first container. + // annotationService is the name of the service to proxy. + // This defaults to the name of the Kubernetes service associated with the pod. annotationService = "consul.hashicorp.com/connect-service" // annotationPort is the name or value of the port to proxy incoming @@ -45,7 +45,7 @@ const ( annotationUpstreams = "consul.hashicorp.com/connect-service-upstreams" // annotationTags is a list of tags to register with the service - // this is specified as a comma separated list e.g. abc,123 + // this is specified as a comma separated list e.g. abc,123. annotationTags = "consul.hashicorp.com/service-tags" // annotationConnectTags is a list of tags to register with the service @@ -59,7 +59,7 @@ const ( // annotationMeta is a list of metadata key/value pairs to add to the service // registration. This is specified in the format `:` - // e.g. consul.hashicorp.com/service-meta-foo:bar + // e.g. consul.hashicorp.com/service-meta-foo:bar. annotationMeta = "consul.hashicorp.com/service-meta-" // annotationSyncPeriod controls the -sync-period flag passed to the @@ -75,6 +75,12 @@ const ( annotationSidecarProxyMemoryLimit = "consul.hashicorp.com/sidecar-proxy-memory-limit" annotationSidecarProxyMemoryRequest = "consul.hashicorp.com/sidecar-proxy-memory-request" + // annotations for consul sidecar resource limits. + annotationConsulSidecarCPULimit = "consul.hashicorp.com/consul-sidecar-cpu-limit" + annotationConsulSidecarCPURequest = "consul.hashicorp.com/consul-sidecar-cpu-request" + annotationConsulSidecarMemoryLimit = "consul.hashicorp.com/consul-sidecar-memory-limit" + annotationConsulSidecarMemoryRequest = "consul.hashicorp.com/consul-sidecar-memory-request" + // annotations for metrics to configure where Prometheus scrapes // metrics from, whether to run a merged metrics endpoint on the consul // sidecar, and configure the connect service metrics. @@ -96,6 +102,12 @@ const ( // annotationConsulNamespace is the Consul namespace the service is registered into. annotationConsulNamespace = "consul.hashicorp.com/consul-namespace" + // keyConsulDNS enables or disables Consul DNS for a given pod. It can also be set as a label + // on a namespace to define the default behaviour for connect-injected pods which do not otherwise override this setting + // with their own annotation. + // This annotation/label takes a boolean value (true/false). + keyConsulDNS = "consul.hashicorp.com/consul-dns" + // keyTransparentProxy enables or disables transparent proxy for a given pod. It can also be set as a label // on a namespace to define the default behaviour for connect-injected pods which do not otherwise override this setting // with their own annotation. @@ -122,6 +134,10 @@ const ( // webhook/handler. annotationOriginalPod = "consul.hashicorp.com/original-pod" + // labelServiceIgnore is a label that can be added to a service to prevent it from being + // registered with Consul. + labelServiceIgnore = "consul.hashicorp.com/service-ignore" + // injected is used as the annotation value for annotationInjected. injected = "injected" diff --git a/control-plane/connect-inject/consul_sidecar.go b/control-plane/connect-inject/consul_sidecar.go index bb88224ec6..d296980988 100644 --- a/control-plane/connect-inject/consul_sidecar.go +++ b/control-plane/connect-inject/consul_sidecar.go @@ -4,6 +4,7 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" ) // consulSidecar starts the consul-sidecar command to only run @@ -16,6 +17,11 @@ func (h *Handler) consulSidecar(pod corev1.Pod) (corev1.Container, error) { return corev1.Container{}, err } + resources, err := h.consulSidecarResources(pod) + if err != nil { + return corev1.Container{}, err + } + command := []string{ "consul-k8s-control-plane", "consul-sidecar", @@ -38,6 +44,72 @@ func (h *Handler) consulSidecar(pod corev1.Pod) (corev1.Container, error) { }, }, Command: command, - Resources: h.ConsulSidecarResources, + Resources: resources, }, nil } + +func (h *Handler) consulSidecarResources(pod corev1.Pod) (corev1.ResourceRequirements, error) { + resources := corev1.ResourceRequirements{ + Limits: corev1.ResourceList{}, + Requests: corev1.ResourceList{}, + } + // zeroQuantity is used for comparison to see if a quantity was explicitly + // set. + var zeroQuantity resource.Quantity + + // NOTE: We only want to set the limit/request if the default or annotation + // was explicitly set. If it's not explicitly set, it will be the zero value + // which would show up in the pod spec as being explicitly set to zero if we + // set that key, e.g. "cpu" to zero. + // We want it to not show up in the pod spec at all if if it's not explicitly + // set so that users aren't wondering why it's set to 0 when they didn't specify + // a request/limit. If they have explicitly set it to 0 then it will be set + // to 0 in the pod spec because we're doing a comparison to the zero-valued + // struct. + + // CPU Limit. + if anno, ok := pod.Annotations[annotationConsulSidecarCPULimit]; ok { + cpuLimit, err := resource.ParseQuantity(anno) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing annotation %s:%q: %s", annotationConsulSidecarCPULimit, anno, err) + } + resources.Limits[corev1.ResourceCPU] = cpuLimit + } else if h.DefaultConsulSidecarResources.Limits[corev1.ResourceCPU] != zeroQuantity { + resources.Limits[corev1.ResourceCPU] = h.DefaultConsulSidecarResources.Limits[corev1.ResourceCPU] + } + + // CPU Request. + if anno, ok := pod.Annotations[annotationConsulSidecarCPURequest]; ok { + cpuRequest, err := resource.ParseQuantity(anno) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing annotation %s:%q: %s", annotationConsulSidecarCPURequest, anno, err) + } + resources.Requests[corev1.ResourceCPU] = cpuRequest + } else if h.DefaultConsulSidecarResources.Requests[corev1.ResourceCPU] != zeroQuantity { + resources.Requests[corev1.ResourceCPU] = h.DefaultConsulSidecarResources.Requests[corev1.ResourceCPU] + } + + // Memory Limit. + if anno, ok := pod.Annotations[annotationConsulSidecarMemoryLimit]; ok { + memoryLimit, err := resource.ParseQuantity(anno) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing annotation %s:%q: %s", annotationConsulSidecarMemoryLimit, anno, err) + } + resources.Limits[corev1.ResourceMemory] = memoryLimit + } else if h.DefaultConsulSidecarResources.Limits[corev1.ResourceMemory] != zeroQuantity { + resources.Limits[corev1.ResourceMemory] = h.DefaultConsulSidecarResources.Limits[corev1.ResourceMemory] + } + + // Memory Request. + if anno, ok := pod.Annotations[annotationConsulSidecarMemoryRequest]; ok { + memoryRequest, err := resource.ParseQuantity(anno) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing annotation %s:%q: %s", annotationConsulSidecarMemoryRequest, anno, err) + } + resources.Requests[corev1.ResourceMemory] = memoryRequest + } else if h.DefaultConsulSidecarResources.Requests[corev1.ResourceMemory] != zeroQuantity { + resources.Requests[corev1.ResourceMemory] = h.DefaultConsulSidecarResources.Requests[corev1.ResourceMemory] + } + + return resources, nil +} diff --git a/control-plane/connect-inject/consul_sidecar_test.go b/control-plane/connect-inject/consul_sidecar_test.go index b5f7c80c3b..da3cd5c7e1 100644 --- a/control-plane/connect-inject/consul_sidecar_test.go +++ b/control-plane/connect-inject/consul_sidecar_test.go @@ -6,6 +6,7 @@ import ( logrtest "github.com/go-logr/logr/testing" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,3 +44,300 @@ func TestConsulSidecar_MetricsFlags(t *testing.T) { require.Contains(t, container.Command, "-service-metrics-port=8080") require.Contains(t, container.Command, "-service-metrics-path=/metrics") } + +func TestHandlerConsulSidecar_Resources(t *testing.T) { + mem1 := resource.MustParse("100Mi") + mem2 := resource.MustParse("200Mi") + cpu1 := resource.MustParse("100m") + cpu2 := resource.MustParse("200m") + zero := resource.MustParse("0") + + cases := map[string]struct { + handler Handler + annotations map[string]string + expResources corev1.ResourceRequirements + expErr string + }{ + "no defaults, no annotations": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + }, + expResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{}, + Requests: corev1.ResourceList{}, + }, + }, + "all defaults, no annotations": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + DefaultConsulSidecarResources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: cpu1, + corev1.ResourceMemory: mem1, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: cpu2, + corev1.ResourceMemory: mem2, + }, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + }, + expResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: cpu2, + corev1.ResourceMemory: mem2, + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: cpu1, + corev1.ResourceMemory: mem1, + }, + }, + }, + "no defaults, all annotations": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + annotationConsulSidecarCPURequest: "100m", + annotationConsulSidecarMemoryRequest: "100Mi", + annotationConsulSidecarCPULimit: "200m", + annotationConsulSidecarMemoryLimit: "200Mi", + }, + expResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: cpu2, + corev1.ResourceMemory: mem2, + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: cpu1, + corev1.ResourceMemory: mem1, + }, + }, + }, + "annotations override defaults": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + DefaultConsulSidecarResources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + annotationConsulSidecarCPURequest: "100m", + annotationConsulSidecarMemoryRequest: "100Mi", + annotationConsulSidecarCPULimit: "200m", + annotationConsulSidecarMemoryLimit: "200Mi", + }, + expResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: cpu2, + corev1.ResourceMemory: mem2, + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: cpu1, + corev1.ResourceMemory: mem1, + }, + }, + }, + "defaults set to zero, no annotations": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + DefaultConsulSidecarResources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + }, + expResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + }, + }, + "annotations set to 0": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + annotationConsulSidecarCPURequest: "0", + annotationConsulSidecarMemoryRequest: "0", + annotationConsulSidecarCPULimit: "0", + annotationConsulSidecarMemoryLimit: "0", + }, + expResources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: zero, + corev1.ResourceMemory: zero, + }, + }, + }, + "invalid cpu request": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + annotationConsulSidecarCPURequest: "invalid", + }, + expErr: "parsing annotation consul.hashicorp.com/consul-sidecar-cpu-request:\"invalid\": quantities must match the regular expression", + }, + "invalid cpu limit": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + annotationConsulSidecarCPULimit: "invalid", + }, + expErr: "parsing annotation consul.hashicorp.com/consul-sidecar-cpu-limit:\"invalid\": quantities must match the regular expression", + }, + "invalid memory request": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + annotationConsulSidecarMemoryRequest: "invalid", + }, + expErr: "parsing annotation consul.hashicorp.com/consul-sidecar-memory-request:\"invalid\": quantities must match the regular expression", + }, + "invalid memory limit": { + handler: Handler{ + Log: logrtest.TestLogger{T: t}, + ImageConsulK8S: "hashicorp/consul-k8s:9.9.9", + MetricsConfig: MetricsConfig{ + DefaultEnableMetrics: true, + DefaultEnableMetricsMerging: true, + }, + }, + annotations: map[string]string{ + annotationMergedMetricsPort: "20100", + annotationServiceMetricsPort: "8080", + annotationServiceMetricsPath: "/metrics", + annotationConsulSidecarMemoryLimit: "invalid", + }, + expErr: "parsing annotation consul.hashicorp.com/consul-sidecar-memory-limit:\"invalid\": quantities must match the regular expression", + }, + } + + for name, c := range cases { + t.Run(name, func(tt *testing.T) { + require := require.New(tt) + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: c.annotations, + }, + + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "web", + }, + }, + }, + } + container, err := c.handler.consulSidecar(pod) + if c.expErr != "" { + require.NotNil(err) + require.Contains(err.Error(), c.expErr) + } else { + require.NoError(err) + require.Equal(c.expResources, container.Resources) + } + }) + } +} diff --git a/control-plane/connect-inject/container_init.go b/control-plane/connect-inject/container_init.go index 4c2f44914e..526e473bfc 100644 --- a/control-plane/connect-inject/container_init.go +++ b/control-plane/connect-inject/container_init.go @@ -2,6 +2,8 @@ package connectinject import ( "bytes" + "fmt" + "os" "strconv" "strings" "text/template" @@ -16,12 +18,17 @@ const ( envoyUserAndGroupID = 5995 copyContainerUserAndGroupID = 5996 netAdminCapability = "NET_ADMIN" + dnsServiceHostEnvSuffix = "DNS_SERVICE_HOST" ) type initContainerCommandData struct { ServiceName string ServiceAccountName string AuthMethod string + // ConsulPartition is the Consul admin partition to register the service + // and proxy in. An empty string indicates partitions are not + // enabled in Consul (necessary for OSS). + ConsulPartition string // ConsulNamespace is the Consul namespace to register the service // and proxy in. An empty string indicates namespaces are not // enabled in Consul (necessary for OSS). @@ -62,6 +69,21 @@ type initContainerCommandData struct { // TProxyExcludeUIDs is a list of additional user IDs to exclude from traffic redirection via // the consul connect redirect-traffic command. TProxyExcludeUIDs []string + + // ConsulDNSClusterIP is the IP of the Consul DNS Service. + ConsulDNSClusterIP string + + // MultiPort determines whether this is a multi port Pod, which configures the init container to be specific to one + // of the services on the multi port Pod. + MultiPort bool + + // EnvoyAdminPort configures the admin port of the Envoy sidecar. This will be unique per service in a multi port + // Pod. + EnvoyAdminPort int + + // BearerTokenFile configures where the service account token can be found. This will be unique per service in a + // multi port Pod. + BearerTokenFile string } // initCopyContainer returns the init container spec for the copy container which places @@ -94,17 +116,36 @@ func (h *Handler) initCopyContainer() corev1.Container { return container } -// containerInit returns the init container spec for registering the Consul -// service, setting up the Envoy bootstrap, etc. -func (h *Handler) containerInit(namespace corev1.Namespace, pod corev1.Pod) (corev1.Container, error) { +// containerInit returns the init container spec for connect-init that polls for the service and the connect proxy service to be registered +// so that it can save the proxy service id to the shared volume and boostrap Envoy with the proxy-id. +func (h *Handler) containerInit(namespace corev1.Namespace, pod corev1.Pod, mpi multiPortInfo) (corev1.Container, error) { // Check if tproxy is enabled on this pod. tproxyEnabled, err := transparentProxyEnabled(namespace, pod, h.EnableTransparentProxy) if err != nil { return corev1.Container{}, err } + dnsEnabled, err := consulDNSEnabled(namespace, pod, h.EnableConsulDNS) + if err != nil { + return corev1.Container{}, err + } + + var consulDNSClusterIP string + if dnsEnabled { + // If Consul DNS is enabled, we find the environment variable that has the value + // of the ClusterIP of the Consul DNS Service. constructDNSServiceHostName returns + // the name of the env variable whose value is the ClusterIP of the Consul DNS Service. + consulDNSClusterIP = os.Getenv(h.constructDNSServiceHostName()) + if consulDNSClusterIP == "" { + return corev1.Container{}, fmt.Errorf("environment variable %s is not found", h.constructDNSServiceHostName()) + } + } + + multiPort := mpi.serviceName != "" + data := initContainerCommandData{ AuthMethod: h.AuthMethod, + ConsulPartition: h.ConsulPartition, ConsulNamespace: h.consulNamespace(namespace.Name), NamespaceMirroringEnabled: h.EnableK8SNSMirroring, ConsulCACert: h.ConsulCACert, @@ -113,13 +154,43 @@ func (h *Handler) containerInit(namespace corev1.Namespace, pod corev1.Pod) (cor TProxyExcludeOutboundPorts: splitCommaSeparatedItemsFromAnnotation(annotationTProxyExcludeOutboundPorts, pod), TProxyExcludeOutboundCIDRs: splitCommaSeparatedItemsFromAnnotation(annotationTProxyExcludeOutboundCIDRs, pod), TProxyExcludeUIDs: splitCommaSeparatedItemsFromAnnotation(annotationTProxyExcludeUIDs, pod), + ConsulDNSClusterIP: consulDNSClusterIP, EnvoyUID: envoyUserAndGroupID, + MultiPort: multiPort, + EnvoyAdminPort: 19000 + mpi.serviceIndex, } - if data.AuthMethod != "" { - data.ServiceAccountName = pod.Spec.ServiceAccountName + // Create expected volume mounts + volMounts := []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/consul/connect-inject", + }, + } + + if multiPort { + data.ServiceName = mpi.serviceName + } else { data.ServiceName = pod.Annotations[annotationService] } + if h.AuthMethod != "" { + if multiPort { + // If multi port then we require that the service account name + // matches the service name. + data.ServiceAccountName = mpi.serviceName + } else { + data.ServiceAccountName = pod.Spec.ServiceAccountName + } + // Extract the service account token's volume mount + saTokenVolumeMount, bearerTokenFile, err := findServiceAccountVolumeMount(pod, multiPort, mpi.serviceName) + if err != nil { + return corev1.Container{}, err + } + data.BearerTokenFile = bearerTokenFile + + // Append to volume mounts + volMounts = append(volMounts, saTokenVolumeMount) + } // This determines how to configure the consul connect envoy command: what // metrics backend to use and what path to expose on the @@ -138,25 +209,6 @@ func (h *Handler) containerInit(namespace corev1.Namespace, pod corev1.Pod) (cor data.PrometheusBackendPort = mergedMetricsPort } - // Create expected volume mounts - volMounts := []corev1.VolumeMount{ - { - Name: volumeName, - MountPath: "/consul/connect-inject", - }, - } - - if h.AuthMethod != "" { - // Extract the service account token's volume mount - saTokenVolumeMount, err := findServiceAccountVolumeMount(pod) - if err != nil { - return corev1.Container{}, err - } - - // Append to volume mounts - volMounts = append(volMounts, saTokenVolumeMount) - } - // Render the command var buf bytes.Buffer tpl := template.Must(template.New("root").Parse(strings.TrimSpace( @@ -166,8 +218,12 @@ func (h *Handler) containerInit(namespace corev1.Namespace, pod corev1.Pod) (cor return corev1.Container{}, err } + initContainerName := InjectInitContainerName + if multiPort { + initContainerName = fmt.Sprintf("%s-%s", InjectInitContainerName, mpi.serviceName) + } container := corev1.Container{ - Name: InjectInitContainerName, + Name: initContainerName, Image: h.ImageConsulK8S, Env: []corev1.EnvVar{ { @@ -218,6 +274,15 @@ func (h *Handler) containerInit(namespace corev1.Namespace, pod corev1.Pod) (cor return container, nil } +// constructDNSServiceHostName use the resource prefix and the DNS Service hostname suffix to construct the +// key of the env variable whose value is the cluster IP of the Consul DNS Service. +// It translates "resource-prefix" into "RESOURCE_PREFIX_DNS_SERVICE_HOST". +func (h *Handler) constructDNSServiceHostName() string { + upcaseResourcePrefix := strings.ToUpper(h.ResourcePrefix) + upcaseResourcePrefixWithUnderscores := strings.ReplaceAll(upcaseResourcePrefix, "-", "_") + return strings.Join([]string{upcaseResourcePrefixWithUnderscores, dnsServiceHostEnvSuffix}, "_") +} + // transparentProxyEnabled returns true if transparent proxy should be enabled for this pod. // It returns an error when the annotation value cannot be parsed by strconv.ParseBool or if we are unable // to read the pod's namespace label when it exists. @@ -234,6 +299,22 @@ func transparentProxyEnabled(namespace corev1.Namespace, pod corev1.Pod, globalE return globalEnabled, nil } +// consulDNSEnabled returns true if Consul DNS should be enabled for this pod. +// It returns an error when the annotation value cannot be parsed by strconv.ParseBool or if we are unable +// to read the pod's namespace label when it exists. +func consulDNSEnabled(namespace corev1.Namespace, pod corev1.Pod, globalEnabled bool) (bool, error) { + // First check to see if the pod annotation exists to override the namespace or global settings. + if raw, ok := pod.Annotations[keyConsulDNS]; ok { + return strconv.ParseBool(raw) + } + // Next see if the namespace has been defaulted. + if raw, ok := namespace.Labels[keyConsulDNS]; ok { + return strconv.ParseBool(raw) + } + // Else fall back to the global default. + return globalEnabled, nil +} + // pointerToInt64 takes an int64 and returns a pointer to it. func pointerToInt64(i int64) *int64 { return &i @@ -274,6 +355,10 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -acl-auth-method="{{ .AuthMethod }}" \ -service-account-name="{{ .ServiceAccountName }}" \ -service-name="{{ .ServiceName }}" \ + -bearer-token-file={{ .BearerTokenFile }} \ + {{- if .MultiPort }} + -acl-token-sink=/consul/connect-inject/acl-token-{{ .ServiceName }} \ + {{- end }} {{- if .ConsulNamespace }} {{- if .NamespaceMirroringEnabled }} {{- /* If namespace mirroring is enabled, the auth method is @@ -284,13 +369,27 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD {{- end }} {{- end }} {{- end }} + {{- if .MultiPort }} + -multiport=true \ + -proxy-id-file=/consul/connect-inject/proxyid-{{ .ServiceName }} \ + {{- if not .AuthMethod }} + -service-name="{{ .ServiceName }}" \ + {{- end }} + {{- end }} + {{- if .ConsulPartition }} + -partition="{{ .ConsulPartition }}" \ + {{- end }} {{- if .ConsulNamespace }} -consul-service-namespace="{{ .ConsulNamespace }}" \ {{- end }} # Generate the envoy bootstrap code /consul/connect-inject/consul connect envoy \ + {{- if .MultiPort }} + -proxy-id="$(cat /consul/connect-inject/proxyid-{{.ServiceName}})" \ + {{- else }} -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + {{- end }} {{- if .PrometheusScrapePath }} -prometheus-scrape-path="{{ .PrometheusScrapePath }}" \ {{- end }} @@ -298,12 +397,23 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -prometheus-backend-port="{{ .PrometheusBackendPort }}" \ {{- end }} {{- if .AuthMethod }} + {{- if .MultiPort }} + -token-file="/consul/connect-inject/acl-token-{{ .ServiceName }}" \ + {{- else }} -token-file="/consul/connect-inject/acl-token" \ {{- end }} + {{- end }} + {{- if .ConsulPartition }} + -partition="{{ .ConsulPartition }}" \ + {{- end }} {{- if .ConsulNamespace }} -namespace="{{ .ConsulNamespace }}" \ {{- end }} - -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml + {{- if .MultiPort }} + -admin-bind=127.0.0.1:{{ .EnvoyAdminPort }} \ + {{- end }} + -bootstrap > {{ if .MultiPort }}/consul/connect-inject/envoy-bootstrap-{{.ServiceName}}.yaml{{ else }}/consul/connect-inject/envoy-bootstrap.yaml{{ end }} + {{- if .EnableTransparentProxy }} {{- /* The newline below is intentional to allow extra space @@ -314,9 +424,15 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD {{- if .AuthMethod }} -token-file="/consul/connect-inject/acl-token" \ {{- end }} + {{- if .ConsulPartition }} + -partition="{{ .ConsulPartition }}" \ + {{- end }} {{- if .ConsulNamespace }} -namespace="{{ .ConsulNamespace }}" \ {{- end }} + {{- if .ConsulDNSClusterIP }} + -consul-dns-ip="{{ .ConsulDNSClusterIP }}" \ + {{- end }} {{- range .TProxyExcludeInboundPorts }} -exclude-inbound-port="{{ . }}" \ {{- end }} diff --git a/control-plane/connect-inject/container_init_test.go b/control-plane/connect-inject/container_init_test.go index 1603c82d12..2b8927f258 100644 --- a/control-plane/connect-inject/container_init_test.go +++ b/control-plane/connect-inject/container_init_test.go @@ -2,6 +2,7 @@ package connectinject import ( "fmt" + "os" "strings" "testing" @@ -130,7 +131,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD h := tt.Handler pod := *tt.Pod(minimal()) - container, err := h.containerInit(testNS, pod) + container, err := h.containerInit(testNS, pod, multiPortInfo{}) require.NoError(err) actual := strings.Join(container.Command, " ") require.Contains(actual, tt.Cmd) @@ -295,7 +296,7 @@ func TestHandlerContainerInit_transparentProxy(t *testing.T) { } ns := testNS ns.Labels = c.namespaceLabel - container, err := h.containerInit(ns, *pod) + container, err := h.containerInit(ns, *pod, multiPortInfo{}) require.NoError(t, err) actualCmd := strings.Join(container.Command, " ") @@ -310,7 +311,116 @@ func TestHandlerContainerInit_transparentProxy(t *testing.T) { } } -func TestHandlerContainerInit_namespacesEnabled(t *testing.T) { +func TestHandlerContainerInit_consulDNS(t *testing.T) { + cases := map[string]struct { + globalEnabled bool + annotations map[string]string + expectedContainsCmd string + namespaceLabel map[string]string + }{ + "enabled globally, ns not set, annotation not provided": { + globalEnabled: true, + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -consul-dns-ip="10.0.34.16" \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + }, + "enabled globally, ns not set, annotation is false": { + globalEnabled: true, + annotations: map[string]string{keyConsulDNS: "false"}, + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + }, + "enabled globally, ns not set, annotation is true": { + globalEnabled: true, + annotations: map[string]string{keyConsulDNS: "true"}, + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -consul-dns-ip="10.0.34.16" \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + }, + "disabled globally, ns not set, annotation not provided": { + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + }, + "disabled globally, ns not set, annotation is false": { + annotations: map[string]string{keyConsulDNS: "false"}, + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + }, + "disabled globally, ns not set, annotation is true": { + annotations: map[string]string{keyConsulDNS: "true"}, + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -consul-dns-ip="10.0.34.16" \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + }, + "disabled globally, ns enabled, annotation not set": { + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -consul-dns-ip="10.0.34.16" \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + namespaceLabel: map[string]string{keyConsulDNS: "true"}, + }, + "enabled globally, ns disabled, annotation not set": { + globalEnabled: true, + expectedContainsCmd: `/consul/connect-inject/consul connect redirect-traffic \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -proxy-uid=5995`, + namespaceLabel: map[string]string{keyConsulDNS: "false"}, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + h := Handler{EnableConsulDNS: c.globalEnabled, EnableTransparentProxy: true, ResourcePrefix: "consul-consul"} + os.Setenv("CONSUL_CONSUL_DNS_SERVICE_HOST", "10.0.34.16") + defer os.Unsetenv("CONSUL_CONSUL_DNS_SERVICE_HOST") + + pod := minimal() + pod.Annotations = c.annotations + + ns := testNS + ns.Labels = c.namespaceLabel + container, err := h.containerInit(ns, *pod, multiPortInfo{}) + require.NoError(t, err) + actualCmd := strings.Join(container.Command, " ") + + require.Contains(t, actualCmd, c.expectedContainsCmd) + }) + } +} + +func TestHandler_constructDNSServiceHostName(t *testing.T) { + cases := []struct { + prefix string + result string + }{ + { + prefix: "consul-consul", + result: "CONSUL_CONSUL_DNS_SERVICE_HOST", + }, + { + prefix: "release", + result: "RELEASE_DNS_SERVICE_HOST", + }, + { + prefix: "consul-dc1", + result: "CONSUL_DC1_DNS_SERVICE_HOST", + }, + } + + for _, c := range cases { + t.Run(c.prefix, func(t *testing.T) { + h := Handler{ResourcePrefix: c.prefix} + require.Equal(t, c.result, h.constructDNSServiceHostName()) + }) + } +} + +func TestHandlerContainerInit_namespacesAndPartitionsEnabled(t *testing.T) { minimal := func() *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -349,7 +459,7 @@ func TestHandlerContainerInit_namespacesEnabled(t *testing.T) { Cmd string // Strings.Contains test }{ { - "whole template, default namespace", + "whole template, default namespace, no partition", func(pod *corev1.Pod) *corev1.Pod { pod.Annotations[annotationService] = "web" return pod @@ -357,6 +467,7 @@ func TestHandlerContainerInit_namespacesEnabled(t *testing.T) { Handler{ EnableNamespaces: true, ConsulDestinationNamespace: "default", + ConsulPartition: "", }, `/bin/sh -ec export CONSUL_HTTP_ADDR="${HOST_IP}:8500" @@ -370,9 +481,33 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -namespace="default" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml`, }, + { + "whole template, default namespace, default partition", + func(pod *corev1.Pod) *corev1.Pod { + pod.Annotations[annotationService] = "web" + return pod + }, + Handler{ + EnableNamespaces: true, + ConsulDestinationNamespace: "default", + ConsulPartition: "default", + }, + `/bin/sh -ec +export CONSUL_HTTP_ADDR="${HOST_IP}:8500" +export CONSUL_GRPC_ADDR="${HOST_IP}:8502" +consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD_NAMESPACE} \ + -partition="default" \ + -consul-service-namespace="default" \ +# Generate the envoy bootstrap code +/consul/connect-inject/consul connect envoy \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -partition="default" \ + -namespace="default" \ + -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml`, + }, { - "whole template, non-default namespace", + "whole template, non-default namespace, no partition", func(pod *corev1.Pod) *corev1.Pod { pod.Annotations[annotationService] = "web" return pod @@ -380,6 +515,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD Handler{ EnableNamespaces: true, ConsulDestinationNamespace: "non-default", + ConsulPartition: "", }, `/bin/sh -ec export CONSUL_HTTP_ADDR="${HOST_IP}:8500" @@ -393,9 +529,33 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -namespace="non-default" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml`, }, + { + "whole template, non-default namespace, non-default partition", + func(pod *corev1.Pod) *corev1.Pod { + pod.Annotations[annotationService] = "web" + return pod + }, + Handler{ + EnableNamespaces: true, + ConsulDestinationNamespace: "non-default", + ConsulPartition: "non-default-part", + }, + `/bin/sh -ec +export CONSUL_HTTP_ADDR="${HOST_IP}:8500" +export CONSUL_GRPC_ADDR="${HOST_IP}:8502" +consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD_NAMESPACE} \ + -partition="non-default-part" \ + -consul-service-namespace="non-default" \ +# Generate the envoy bootstrap code +/consul/connect-inject/consul connect envoy \ + -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -partition="non-default-part" \ + -namespace="non-default" \ + -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml`, + }, { - "Whole template, auth method, non-default namespace, mirroring disabled", + "Whole template, auth method, non-default namespace, mirroring disabled, default partition", func(pod *corev1.Pod) *corev1.Pod { pod.Annotations[annotationService] = "" return pod @@ -404,6 +564,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD AuthMethod: "auth-method", EnableNamespaces: true, ConsulDestinationNamespace: "non-default", + ConsulPartition: "default", }, `/bin/sh -ec export CONSUL_HTTP_ADDR="${HOST_IP}:8500" @@ -412,18 +573,21 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -acl-auth-method="auth-method" \ -service-account-name="web" \ -service-name="" \ + -bearer-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token \ -auth-method-namespace="non-default" \ + -partition="default" \ -consul-service-namespace="non-default" \ # Generate the envoy bootstrap code /consul/connect-inject/consul connect envoy \ -proxy-id="$(cat /consul/connect-inject/proxyid)" \ -token-file="/consul/connect-inject/acl-token" \ + -partition="default" \ -namespace="non-default" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml`, }, { - "Whole template, auth method, non-default namespace, mirroring enabled", + "Whole template, auth method, non-default namespace, mirroring enabled, non-default partition", func(pod *corev1.Pod) *corev1.Pod { pod.Annotations[annotationService] = "" return pod @@ -433,6 +597,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD EnableNamespaces: true, ConsulDestinationNamespace: "non-default", // Overridden by mirroring EnableK8SNSMirroring: true, + ConsulPartition: "non-default", }, `/bin/sh -ec export CONSUL_HTTP_ADDR="${HOST_IP}:8500" @@ -441,18 +606,21 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -acl-auth-method="auth-method" \ -service-account-name="web" \ -service-name="" \ + -bearer-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token \ -auth-method-namespace="default" \ + -partition="non-default" \ -consul-service-namespace="k8snamespace" \ # Generate the envoy bootstrap code /consul/connect-inject/consul connect envoy \ -proxy-id="$(cat /consul/connect-inject/proxyid)" \ -token-file="/consul/connect-inject/acl-token" \ + -partition="non-default" \ -namespace="k8snamespace" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml`, }, { - "whole template, default namespace, tproxy enabled", + "whole template, default namespace, tproxy enabled, no partition", func(pod *corev1.Pod) *corev1.Pod { pod.Annotations[annotationService] = "web" return pod @@ -460,6 +628,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD Handler{ EnableNamespaces: true, ConsulDestinationNamespace: "default", + ConsulPartition: "", EnableTransparentProxy: true, }, `/bin/sh -ec @@ -480,15 +649,15 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -proxy-id="$(cat /consul/connect-inject/proxyid)" \ -proxy-uid=5995`, }, - { - "whole template, non-default namespace, tproxy enabled", + "whole template, non-default namespace, tproxy enabled, default partition", func(pod *corev1.Pod) *corev1.Pod { pod.Annotations[annotationService] = "web" return pod }, Handler{ EnableNamespaces: true, + ConsulPartition: "default", ConsulDestinationNamespace: "non-default", EnableTransparentProxy: true, }, @@ -496,23 +665,26 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD export CONSUL_HTTP_ADDR="${HOST_IP}:8500" export CONSUL_GRPC_ADDR="${HOST_IP}:8502" consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD_NAMESPACE} \ + -partition="default" \ -consul-service-namespace="non-default" \ # Generate the envoy bootstrap code /consul/connect-inject/consul connect envoy \ -proxy-id="$(cat /consul/connect-inject/proxyid)" \ + -partition="default" \ -namespace="non-default" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml # Apply traffic redirection rules. /consul/connect-inject/consul connect redirect-traffic \ + -partition="default" \ -namespace="non-default" \ -proxy-id="$(cat /consul/connect-inject/proxyid)" \ -proxy-uid=5995`, }, { - "Whole template, auth method, non-default namespace, mirroring enabled, tproxy enabled", + "Whole template, auth method, non-default namespace, mirroring enabled, tproxy enabled, non-default partition", func(pod *corev1.Pod) *corev1.Pod { pod.Annotations[annotationService] = "web" return pod @@ -520,6 +692,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD Handler{ AuthMethod: "auth-method", EnableNamespaces: true, + ConsulPartition: "non-default", ConsulDestinationNamespace: "non-default", // Overridden by mirroring EnableK8SNSMirroring: true, EnableTransparentProxy: true, @@ -531,19 +704,23 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD -acl-auth-method="auth-method" \ -service-account-name="web" \ -service-name="web" \ + -bearer-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token \ -auth-method-namespace="default" \ + -partition="non-default" \ -consul-service-namespace="k8snamespace" \ # Generate the envoy bootstrap code /consul/connect-inject/consul connect envoy \ -proxy-id="$(cat /consul/connect-inject/proxyid)" \ -token-file="/consul/connect-inject/acl-token" \ + -partition="non-default" \ -namespace="k8snamespace" \ -bootstrap > /consul/connect-inject/envoy-bootstrap.yaml # Apply traffic redirection rules. /consul/connect-inject/consul connect redirect-traffic \ -token-file="/consul/connect-inject/acl-token" \ + -partition="non-default" \ -namespace="k8snamespace" \ -proxy-id="$(cat /consul/connect-inject/proxyid)" \ -proxy-uid=5995`, @@ -555,7 +732,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD require := require.New(t) h := tt.Handler - container, err := h.containerInit(testNS, *tt.Pod(minimal())) + container, err := h.containerInit(testNS, *tt.Pod(minimal()), multiPortInfo{}) require.NoError(err) actual := strings.Join(container.Command, " ") require.Equal(tt.Cmd, actual) @@ -563,6 +740,178 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD } } +func TestHandlerContainerInit_Multiport(t *testing.T) { + minimal := func() *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationService: "web,web-admin", + }, + }, + + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "web-admin-service-account", + }, + }, + Containers: []corev1.Container{ + { + Name: "web", + }, + { + Name: "web-side", + }, + { + Name: "web-admin", + }, + { + Name: "web-admin-side", + }, + { + Name: "auth-method-secret", + VolumeMounts: []corev1.VolumeMount{ + { + Name: "service-account-secret", + MountPath: "/var/run/secrets/kubernetes.io/serviceaccount", + }, + }, + }, + }, + ServiceAccountName: "web", + }, + } + } + + cases := []struct { + Name string + Pod func(*corev1.Pod) *corev1.Pod + Handler Handler + NumInitContainers int + MultiPortInfos []multiPortInfo + Cmd []string // Strings.Contains test + }{ + { + "Whole template, multiport", + func(pod *corev1.Pod) *corev1.Pod { + return pod + }, + Handler{}, + 2, + []multiPortInfo{ + { + serviceIndex: 0, + serviceName: "web", + }, + { + serviceIndex: 1, + serviceName: "web-admin", + }, + }, + []string{`/bin/sh -ec +export CONSUL_HTTP_ADDR="${HOST_IP}:8500" +export CONSUL_GRPC_ADDR="${HOST_IP}:8502" +consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD_NAMESPACE} \ + -multiport=true \ + -proxy-id-file=/consul/connect-inject/proxyid-web \ + -service-name="web" \ + +# Generate the envoy bootstrap code +/consul/connect-inject/consul connect envoy \ + -proxy-id="$(cat /consul/connect-inject/proxyid-web)" \ + -admin-bind=127.0.0.1:19000 \ + -bootstrap > /consul/connect-inject/envoy-bootstrap-web.yaml`, + + `/bin/sh -ec +export CONSUL_HTTP_ADDR="${HOST_IP}:8500" +export CONSUL_GRPC_ADDR="${HOST_IP}:8502" +consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD_NAMESPACE} \ + -multiport=true \ + -proxy-id-file=/consul/connect-inject/proxyid-web-admin \ + -service-name="web-admin" \ + +# Generate the envoy bootstrap code +/consul/connect-inject/consul connect envoy \ + -proxy-id="$(cat /consul/connect-inject/proxyid-web-admin)" \ + -admin-bind=127.0.0.1:19001 \ + -bootstrap > /consul/connect-inject/envoy-bootstrap-web-admin.yaml`, + }, + }, + { + "Whole template, multiport, auth method", + func(pod *corev1.Pod) *corev1.Pod { + return pod + }, + Handler{ + AuthMethod: "auth-method", + }, + 2, + []multiPortInfo{ + { + serviceIndex: 0, + serviceName: "web", + }, + { + serviceIndex: 1, + serviceName: "web-admin", + }, + }, + []string{`/bin/sh -ec +export CONSUL_HTTP_ADDR="${HOST_IP}:8500" +export CONSUL_GRPC_ADDR="${HOST_IP}:8502" +consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD_NAMESPACE} \ + -acl-auth-method="auth-method" \ + -service-account-name="web" \ + -service-name="web" \ + -bearer-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token \ + -acl-token-sink=/consul/connect-inject/acl-token-web \ + -multiport=true \ + -proxy-id-file=/consul/connect-inject/proxyid-web \ + +# Generate the envoy bootstrap code +/consul/connect-inject/consul connect envoy \ + -proxy-id="$(cat /consul/connect-inject/proxyid-web)" \ + -token-file="/consul/connect-inject/acl-token-web" \ + -admin-bind=127.0.0.1:19000 \ + -bootstrap > /consul/connect-inject/envoy-bootstrap-web.yaml`, + + `/bin/sh -ec +export CONSUL_HTTP_ADDR="${HOST_IP}:8500" +export CONSUL_GRPC_ADDR="${HOST_IP}:8502" +consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD_NAMESPACE} \ + -acl-auth-method="auth-method" \ + -service-account-name="web-admin" \ + -service-name="web-admin" \ + -bearer-token-file=/consul/serviceaccount-web-admin/token \ + -acl-token-sink=/consul/connect-inject/acl-token-web-admin \ + -multiport=true \ + -proxy-id-file=/consul/connect-inject/proxyid-web-admin \ + +# Generate the envoy bootstrap code +/consul/connect-inject/consul connect envoy \ + -proxy-id="$(cat /consul/connect-inject/proxyid-web-admin)" \ + -token-file="/consul/connect-inject/acl-token-web-admin" \ + -admin-bind=127.0.0.1:19001 \ + -bootstrap > /consul/connect-inject/envoy-bootstrap-web-admin.yaml`, + }, + }, + } + + for _, tt := range cases { + t.Run(tt.Name, func(t *testing.T) { + require := require.New(t) + + h := tt.Handler + for i := 0; i < tt.NumInitContainers; i++ { + container, err := h.containerInit(testNS, *tt.Pod(minimal()), tt.MultiPortInfos[i]) + require.NoError(err) + actual := strings.Join(container.Command, " ") + require.Equal(tt.Cmd[i], actual) + } + }) + } +} + func TestHandlerContainerInit_authMethod(t *testing.T) { require := require.New(t) h := Handler{ @@ -591,7 +940,7 @@ func TestHandlerContainerInit_authMethod(t *testing.T) { ServiceAccountName: "foo", }, } - container, err := h.containerInit(testNS, *pod) + container, err := h.containerInit(testNS, *pod, multiPortInfo{}) require.NoError(err) actual := strings.Join(container.Command, " ") require.Contains(actual, ` @@ -607,7 +956,7 @@ consul-k8s-control-plane connect-init -pod-name=${POD_NAME} -pod-namespace=${POD // If Consul CA cert is set, // Consul addresses should use HTTPS -// and CA cert should be set as env variable +// and CA cert should be set as env variable. func TestHandlerContainerInit_WithTLS(t *testing.T) { require := require.New(t) h := Handler{ @@ -628,7 +977,7 @@ func TestHandlerContainerInit_WithTLS(t *testing.T) { }, }, } - container, err := h.containerInit(testNS, *pod) + container, err := h.containerInit(testNS, *pod, multiPortInfo{}) require.NoError(err) actual := strings.Join(container.Command, " ") require.Contains(actual, ` @@ -672,7 +1021,7 @@ func TestHandlerContainerInit_Resources(t *testing.T) { }, }, } - container, err := h.containerInit(testNS, *pod) + container, err := h.containerInit(testNS, *pod, multiPortInfo{}) require.NoError(err) require.Equal(corev1.ResourceRequirements{ Limits: corev1.ResourceList{ diff --git a/control-plane/connect-inject/endpoints_controller.go b/control-plane/connect-inject/endpoints_controller.go index 104c6cb1c1..8213fa2278 100644 --- a/control-plane/connect-inject/endpoints_controller.go +++ b/control-plane/connect-inject/endpoints_controller.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "regexp" + "strconv" "strings" mapset "github.com/deckarep/golang-set" @@ -72,6 +73,9 @@ type EndpointsController struct { AllowK8sNamespacesSet mapset.Set // Endpoints in the DenyK8sNamespacesSet are ignored. DenyK8sNamespacesSet mapset.Set + // EnableConsulPartitions indicates that a user is running Consul Enterprise + // with version 1.11+ which supports Admin Partitions. + EnableConsulPartitions bool // EnableConsulNamespaces indicates that a user is running Consul Enterprise // with version 1.7+ which supports namespaces. EnableConsulNamespaces bool @@ -114,10 +118,13 @@ type EndpointsController struct { context.Context } +// Reconcile reads the state of an Endpoints object for a Kubernetes Service and reconciles Consul services which +// correspond to the Kubernetes Service. These events are driven by changes to the Pods backing the Kube service. func (r *EndpointsController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var errs error var serviceEndpoints corev1.Endpoints + // Ignore the request if the namespace of the endpoint is not allowed. if shouldIgnore(req.Namespace, r.DenyK8sNamespacesSet, r.AllowK8sNamespacesSet) { return ctrl.Result{}, nil } @@ -134,10 +141,8 @@ func (r *EndpointsController) Reconcile(ctx context.Context, req ctrl.Request) ( if k8serrors.IsNotFound(err) { // Deregister all instances in Consul for this service. The function deregisterServiceOnAllAgents handles // the case where the Consul service name is different from the Kubernetes service name. - if err = r.deregisterServiceOnAllAgents(ctx, req.Name, req.Namespace, nil, endpointPods); err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{}, nil + err = r.deregisterServiceOnAllAgents(ctx, req.Name, req.Namespace, nil) + return ctrl.Result{}, err } else if err != nil { r.Log.Error(err, "failed to get Endpoints", "name", req.Name, "ns", req.Namespace) return ctrl.Result{}, err @@ -145,28 +150,42 @@ func (r *EndpointsController) Reconcile(ctx context.Context, req ctrl.Request) ( r.Log.Info("retrieved", "name", serviceEndpoints.Name, "ns", serviceEndpoints.Namespace) + // If the endpoints object has the label "consul.hashicorp.com/service-ignore" set to true, deregister all instances in Consul for this service. + // It is possible that the endpoints object has never been registered, in which case deregistration is a no-op. + if isLabeledIgnore(serviceEndpoints.Labels) { + // We always deregister the service to handle the case where a user has registered the service, then added the label later. + r.Log.Info("Ignoring endpoint labeled with `consul.hashicorp.com/service-ignore: \"true\"`", "name", req.Name, "namespace", req.Namespace) + err = r.deregisterServiceOnAllAgents(ctx, req.Name, req.Namespace, nil) + return ctrl.Result{}, err + } + // endpointAddressMap stores every IP that corresponds to a Pod in the Endpoints object. It is used to compare // against service instances in Consul to deregister them if they are not in the map. endpointAddressMap := map[string]bool{} // Register all addresses of this Endpoints object as service instances in Consul. for _, subset := range serviceEndpoints.Subsets { - // Combine all addresses to a map, with a value indicating whether an address is ready or not. - allAddresses := make(map[corev1.EndpointAddress]string) - for _, readyAddress := range subset.Addresses { - allAddresses[readyAddress] = api.HealthPassing - } - - for _, notReadyAddress := range subset.NotReadyAddresses { - allAddresses[notReadyAddress] = api.HealthCritical - } - - for address, healthStatus := range allAddresses { + for address, healthStatus := range mapAddresses(subset) { if address.TargetRef != nil && address.TargetRef.Kind == "Pod" { - endpointPods.Add(address.TargetRef.Name) - if err := r.registerServicesAndHealthCheck(ctx, serviceEndpoints, address, healthStatus, endpointAddressMap); err != nil { - r.Log.Error(err, "failed to register services or health check", "name", serviceEndpoints.Name, "ns", serviceEndpoints.Namespace) + var pod corev1.Pod + objectKey := types.NamespacedName{Name: address.TargetRef.Name, Namespace: address.TargetRef.Namespace} + if err := r.Client.Get(ctx, objectKey, &pod); err != nil { + r.Log.Error(err, "failed to get pod", "name", address.TargetRef.Name) errs = multierror.Append(errs, err) + continue + } + + if hasBeenInjected(pod) { + endpointPods.Add(address.TargetRef.Name) + if err := r.registerServicesAndHealthCheck(pod, serviceEndpoints, healthStatus, endpointAddressMap); err != nil { + r.Log.Error(err, "failed to register services or health check", "name", serviceEndpoints.Name, "ns", serviceEndpoints.Namespace) + errs = multierror.Append(errs, err) + } + } else { + // If this endpoints object points to a pod that has injection disabled, + // then we want to ignore it for any further processing and exit early. + r.Log.Info("ignoring because endpoints pods have not been injected", "name", serviceEndpoints.Name, "ns", serviceEndpoints.Namespace) + return ctrl.Result{}, nil } } } @@ -175,7 +194,7 @@ func (r *EndpointsController) Reconcile(ctx context.Context, req ctrl.Request) ( // Compare service instances in Consul with addresses in Endpoints. If an address is not in Endpoints, deregister // from Consul. This uses endpointAddressMap which is populated with the addresses in the Endpoints object during // the registration codepath. - if err = r.deregisterServiceOnAllAgents(ctx, serviceEndpoints.Name, serviceEndpoints.Namespace, endpointAddressMap, endpointPods); err != nil { + if err = r.deregisterServiceOnAllAgents(ctx, serviceEndpoints.Name, serviceEndpoints.Namespace, endpointAddressMap); err != nil { r.Log.Error(err, "failed to deregister endpoints on all agents", "name", serviceEndpoints.Name, "ns", serviceEndpoints.Namespace) errs = multierror.Append(errs, err) } @@ -197,16 +216,9 @@ func (r *EndpointsController) SetupWithManager(mgr ctrl.Manager) error { ).Complete(r) } -// registerServicesAndHealthCheck creates Consul registrations for the service and proxy and register them with Consul. +// registerServicesAndHealthCheck creates Consul registrations for the service and proxy and registers them with Consul. // It also upserts a Kubernetes health check for the service based on whether the endpoint address is ready. -func (r *EndpointsController) registerServicesAndHealthCheck(ctx context.Context, serviceEndpoints corev1.Endpoints, address corev1.EndpointAddress, healthStatus string, endpointAddressMap map[string]bool) error { - // Get pod associated with this address. - var pod corev1.Pod - objectKey := types.NamespacedName{Name: address.TargetRef.Name, Namespace: address.TargetRef.Namespace} - if err := r.Client.Get(ctx, objectKey, &pod); err != nil { - r.Log.Error(err, "failed to get pod", "name", address.TargetRef.Name) - return err - } +func (r *EndpointsController) registerServicesAndHealthCheck(pod corev1.Pod, serviceEndpoints corev1.Endpoints, healthStatus string, endpointAddressMap map[string]bool) error { podHostIP := pod.Status.HostIP if hasBeenInjected(pod) { @@ -232,27 +244,6 @@ func (r *EndpointsController) registerServicesAndHealthCheck(ctx context.Context return err } - // When Consul namespaces are not enabled, we check that the service with the same name but in a different namespace - // is already registered with Consul, and if it is, we skip the registration to avoid service name collisions. - if !r.EnableConsulNamespaces { - services, _, err := client.Catalog().Service(serviceRegistration.Name, "", nil) - if err != nil { - r.Log.Error(err, "failed to get service from the Consul catalog", "name", serviceRegistration.Name) - return err - } - for _, service := range services { - if existingNS, ok := service.ServiceMeta[MetaKeyKubeNS]; ok && existingNS != serviceEndpoints.Namespace { - // Log but don't return an error because we don't want to reconcile this endpoints object again. - r.Log.Info("Skipping service registration because a service with the same name "+ - "but a different Kubernetes namespace is already registered with Consul", - "name", serviceRegistration.Name, - MetaKeyKubeNS, serviceEndpoints.Namespace, - "existing-k8s-namespace", existingNS) - return nil - } - } - } - // Register the service instance with the local agent. // Note: the order of how we register services is important, // and the connect-proxy service should come after the "main" service @@ -376,9 +367,14 @@ func (r *EndpointsController) upsertHealthCheck(pod corev1.Pod, client *api.Clie return nil } +// getServiceName computes the service name to register with Consul from the pod and endpoints object. In a single port +// service, it defaults to the endpoints name, but can be overridden by a pod annotation. In a multi port service, the +// endpoints name is always used since the pod annotation will have multiple service names listed (one per port). +// Changing the Consul service name via annotations is not supported for multi port services. func getServiceName(pod corev1.Pod, serviceEndpoints corev1.Endpoints) string { serviceName := serviceEndpoints.Name - if serviceNameFromAnnotation, ok := pod.Annotations[annotationService]; ok && serviceNameFromAnnotation != "" { + // If the annotation has a comma, it is a multi port Pod. In that case we always use the name of the endpoint. + if serviceNameFromAnnotation, ok := pod.Annotations[annotationService]; ok && serviceNameFromAnnotation != "" && !strings.Contains(serviceNameFromAnnotation, ",") { serviceName = serviceNameFromAnnotation } return serviceName @@ -406,6 +402,11 @@ func (r *EndpointsController) createServiceRegistrations(pod corev1.Pod, service // The handler will always set the port annotation if one is not provided on the pod. var consulServicePort int if raw, ok := pod.Annotations[annotationPort]; ok && raw != "" { + if multiPort := strings.Split(raw, ","); len(multiPort) > 1 { + // Figure out which index of the ports annotation to use by + // finding the index of the service names annotation. + raw = multiPort[getMultiPortIdx(pod, serviceEndpoints)] + } if port, err := portValue(pod, raw); port > 0 { if err != nil { return nil, nil, err @@ -430,18 +431,14 @@ func (r *EndpointsController) createServiceRegistrations(pod corev1.Pod, service } for k, v := range pod.Annotations { if strings.HasPrefix(k, annotationMeta) && strings.TrimPrefix(k, annotationMeta) != "" { - meta[strings.TrimPrefix(k, annotationMeta)] = v + if v == "$POD_NAME" { + meta[strings.TrimPrefix(k, annotationMeta)] = pod.Name + } else { + meta[strings.TrimPrefix(k, annotationMeta)] = v + } } } - - var tags []string - if raw, ok := pod.Annotations[annotationTags]; ok && raw != "" { - tags = strings.Split(raw, ",") - } - // Get the tags from the deprecated tags annotation and combine. - if raw, ok := pod.Annotations[annotationConnectTags]; ok && raw != "" { - tags = append(tags, strings.Split(raw, ",")...) - } + tags := consulTags(pod) service := &api.AgentServiceRegistration{ ID: serviceID, @@ -450,9 +447,7 @@ func (r *EndpointsController) createServiceRegistrations(pod corev1.Pod, service Address: pod.Status.PodIP, Meta: meta, Namespace: r.consulNamespace(pod.Namespace), - } - if len(tags) > 0 { - service.Tags = tags + Tags: tags, } proxyServiceName := getProxyServiceName(pod, serviceEndpoints) @@ -486,17 +481,21 @@ func (r *EndpointsController) createServiceRegistrations(pod corev1.Pod, service proxyConfig.LocalServicePort = consulServicePort } - upstreams, err := r.processUpstreams(pod) + upstreams, err := r.processUpstreams(pod, serviceEndpoints) if err != nil { return nil, nil, err } proxyConfig.Upstreams = upstreams + proxyPort := 20000 + if idx := getMultiPortIdx(pod, serviceEndpoints); idx >= 0 { + proxyPort += idx + } proxyService := &api.AgentServiceRegistration{ Kind: api.ServiceKindConnectProxy, ID: proxyServiceID, Name: proxyServiceName, - Port: 20000, + Port: proxyPort, Address: pod.Status.PodIP, Meta: meta, Namespace: r.consulNamespace(pod.Namespace), @@ -504,7 +503,7 @@ func (r *EndpointsController) createServiceRegistrations(pod corev1.Pod, service Checks: api.AgentServiceChecks{ { Name: "Proxy Public Listener", - TCP: fmt.Sprintf("%s:20000", pod.Status.PodIP), + TCP: fmt.Sprintf("%s:%d", pod.Status.PodIP, proxyPort), Interval: "10s", DeregisterCriticalServiceAfter: "10m", }, @@ -513,9 +512,7 @@ func (r *EndpointsController) createServiceRegistrations(pod corev1.Pod, service AliasService: serviceID, }, }, - } - if len(tags) > 0 { - proxyService.Tags = tags + Tags: tags, } // A user can enable/disable tproxy for an entire namespace. @@ -679,7 +676,7 @@ func getHealthCheckStatusReason(healthCheckStatus, podName, podNamespace string) // The argument endpointsAddressesMap decides whether to deregister *all* service instances or selectively deregister // them only if they are not in endpointsAddressesMap. If the map is nil, it will deregister all instances. If the map // has addresses, it will only deregister instances not in the map. -func (r *EndpointsController) deregisterServiceOnAllAgents(ctx context.Context, k8sSvcName, k8sSvcNamespace string, endpointsAddressesMap map[string]bool, endpointPods mapset.Set) error { +func (r *EndpointsController) deregisterServiceOnAllAgents(ctx context.Context, k8sSvcName, k8sSvcNamespace string, endpointsAddressesMap map[string]bool) error { // Get all agents by getting pods with label component=client, app=consul and release= agents := corev1.PodList{} listOptions := client.ListOptions{ @@ -697,6 +694,18 @@ func (r *EndpointsController) deregisterServiceOnAllAgents(ctx context.Context, // On each agent, we need to get services matching "k8s-service-name" and "k8s-namespace" metadata. for _, agent := range agents.Items { + ready := false + for _, status := range agent.Status.Conditions { + if status.Type == corev1.PodReady { + ready = status.Status == corev1.ConditionTrue + } + } + if !ready { + // We can ignore this client agent here because once it switches its status from not-ready to ready, + // we will reconcile all services as part of that event. + r.Log.Info("Consul client agent is not ready, skipping deregistration", "consul-agent", agent.Name, "svc", k8sSvcName) + continue + } client, err := r.remoteConsulClient(agent.Status.PodIP, r.consulNamespace(k8sSvcNamespace)) if err != nil { r.Log.Error(err, "failed to create a new Consul client", "address", agent.Status.PodIP) @@ -744,6 +753,7 @@ func (r *EndpointsController) deregisterServiceOnAllAgents(ctx context.Context, } } } + return nil } @@ -820,13 +830,20 @@ func serviceInstancesForK8SServiceNameAndNamespace(k8sServiceName, k8sServiceNam // processUpstreams reads the list of upstreams from the Pod annotation and converts them into a list of api.Upstream // objects. -func (r *EndpointsController) processUpstreams(pod corev1.Pod) ([]api.Upstream, error) { +func (r *EndpointsController) processUpstreams(pod corev1.Pod, endpoints corev1.Endpoints) ([]api.Upstream, error) { + // In a multiport pod, only the first service's proxy should have upstreams configured. This skips configuring + // upstreams on additional services on the pod. + mpIdx := getMultiPortIdx(pod, endpoints) + if mpIdx > 0 { + return []api.Upstream{}, nil + } + var upstreams []api.Upstream if raw, ok := pod.Annotations[annotationUpstreams]; ok && raw != "" { for _, raw := range strings.Split(raw, ",") { parts := strings.SplitN(raw, ":", 3) - var datacenter, serviceName, preparedQuery, namespace string + var datacenter, serviceName, preparedQuery, namespace, partition string var port int32 if strings.TrimSpace(parts[0]) == "prepared_query" { port, _ = portValue(pod, strings.TrimSpace(parts[2])) @@ -834,13 +851,19 @@ func (r *EndpointsController) processUpstreams(pod corev1.Pod) ([]api.Upstream, } else { port, _ = portValue(pod, strings.TrimSpace(parts[1])) - // If Consul Namespaces are enabled, attempt to parse the + // If Consul Namespaces or Admin Partitions are enabled, attempt to parse the // upstream for a namespace. - if r.EnableConsulNamespaces { - pieces := strings.SplitN(parts[0], ".", 2) - serviceName = strings.TrimSpace(pieces[0]) - if len(pieces) > 1 { + if r.EnableConsulNamespaces || r.EnableConsulPartitions { + pieces := strings.SplitN(parts[0], ".", 3) + switch len(pieces) { + case 3: + partition = strings.TrimSpace(pieces[2]) + fallthrough + case 2: namespace = strings.TrimSpace(pieces[1]) + fallthrough + default: + serviceName = strings.TrimSpace(pieces[0]) } } else { serviceName = strings.TrimSpace(parts[0]) @@ -873,6 +896,7 @@ func (r *EndpointsController) processUpstreams(pod corev1.Pod) ([]api.Upstream, if port > 0 { upstream := api.Upstream{ DestinationType: api.UpstreamDestTypeService, + DestinationPartition: partition, DestinationNamespace: namespace, DestinationName: serviceName, Datacenter: datacenter, @@ -1015,10 +1039,66 @@ func (r *EndpointsController) consulNamespace(namespace string) string { // hasBeenInjected checks the value of the status annotation and returns true if the Pod has been injected. func hasBeenInjected(pod corev1.Pod) bool { - if anno, ok := pod.Annotations[keyInjectStatus]; ok { - if anno == injected { - return true - } + if anno, ok := pod.Annotations[keyInjectStatus]; ok && anno == injected { + return true } return false } + +// mapAddresses combines all addresses to a mapping of address to its health status. +func mapAddresses(addresses corev1.EndpointSubset) map[corev1.EndpointAddress]string { + m := make(map[corev1.EndpointAddress]string) + for _, readyAddress := range addresses.Addresses { + m[readyAddress] = api.HealthPassing + } + + for _, notReadyAddress := range addresses.NotReadyAddresses { + m[notReadyAddress] = api.HealthCritical + } + + return m +} + +// isLabeledIgnore checks the value of the label `consul.hashicorp.com/service-ignore` and returns true if the +// label exists and is "truthy". Otherwise, it returns false. +func isLabeledIgnore(labels map[string]string) bool { + value, labelExists := labels[labelServiceIgnore] + shouldIgnore, err := strconv.ParseBool(value) + + return shouldIgnore && labelExists && err == nil +} + +// consulTags returns tags that should be added to the Consul service and proxy registrations. +func consulTags(pod corev1.Pod) []string { + var tags []string + if raw, ok := pod.Annotations[annotationTags]; ok && raw != "" { + tags = strings.Split(raw, ",") + } + // Get the tags from the deprecated tags annotation and combine. + if raw, ok := pod.Annotations[annotationConnectTags]; ok && raw != "" { + tags = append(tags, strings.Split(raw, ",")...) + } + + var interpolatedTags []string + for _, t := range tags { + // Support light interpolation to preserve backwards compatibility where tags could + // be environment variables. + // Right now the only string we interpolate is $POD_NAME since that's all + // users have asked for as of now. More can be added here in the future. + if t == "$POD_NAME" { + t = pod.Name + } + interpolatedTags = append(interpolatedTags, t) + } + + return interpolatedTags +} + +func getMultiPortIdx(pod corev1.Pod, serviceEndpoints corev1.Endpoints) int { + for i, name := range strings.Split(pod.Annotations[annotationService], ",") { + if name == getServiceName(pod, serviceEndpoints) { + return i + } + } + return -1 +} diff --git a/control-plane/connect-inject/endpoints_controller_ent_test.go b/control-plane/connect-inject/endpoints_controller_ent_test.go index 1ab5cfe254..5859bd9206 100644 --- a/control-plane/connect-inject/endpoints_controller_ent_test.go +++ b/control-plane/connect-inject/endpoints_controller_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package connectinject @@ -308,7 +308,7 @@ func TestReconcileCreateEndpointWithNamespaces(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 1, len(check)) // Ignoring Namespace because the response from ENT includes it and OSS does not. - var ignoredFields = []string{"Node", "Definition", "Namespace"} + var ignoredFields = []string{"Node", "Definition", "Namespace", "Partition"} require.True(t, cmp.Equal(check[setup.expectedAgentHealthChecks[i].CheckID], setup.expectedAgentHealthChecks[i], cmpopts.IgnoreFields(api.AgentCheck{}, ignoredFields...))) } } @@ -1180,7 +1180,7 @@ func TestReconcileUpdateEndpointWithNamespaces(t *testing.T) { consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { if tt.enableACLs { c.ACL.Enabled = true - c.ACL.Tokens.Master = adminToken + c.ACL.Tokens.InitialManagement = adminToken } c.NodeName = nodeName }) @@ -1308,7 +1308,7 @@ func TestReconcileUpdateEndpointWithNamespaces(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 1, len(check)) // Ignoring Namespace because the response from ENT includes it and OSS does not. - var ignoredFields = []string{"Node", "Definition", "Namespace"} + var ignoredFields = []string{"Node", "Definition", "Namespace", "Partition"} require.True(t, cmp.Equal(check[tt.expectedAgentHealthChecks[i].CheckID], tt.expectedAgentHealthChecks[i], cmpopts.IgnoreFields(api.AgentCheck{}, ignoredFields...))) } } @@ -1514,7 +1514,7 @@ func TestReconcileDeleteEndpointWithNamespaces(t *testing.T) { consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { if tt.enableACLs { c.ACL.Enabled = true - c.ACL.Tokens.Master = adminToken + c.ACL.Tokens.InitialManagement = adminToken } c.NodeName = nodeName }) diff --git a/control-plane/connect-inject/endpoints_controller_test.go b/control-plane/connect-inject/endpoints_controller_test.go index 049864099b..9b25c149b0 100644 --- a/control-plane/connect-inject/endpoints_controller_test.go +++ b/control-plane/connect-inject/endpoints_controller_test.go @@ -3,6 +3,9 @@ package connectinject import ( "context" "fmt" + "net/http" + "net/http/httptest" + "net/url" "strings" "testing" @@ -127,7 +130,7 @@ func TestProcessUpstreamsTLSandACLs(t *testing.T) { consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { c.ACL.Enabled = true c.ACL.DefaultPolicy = "deny" - c.ACL.Tokens.Master = masterToken + c.ACL.Tokens.InitialManagement = masterToken c.CAFile = caFile c.CertFile = certFile c.KeyFile = keyFile @@ -168,7 +171,14 @@ func TestProcessUpstreamsTLSandACLs(t *testing.T) { pod := createPod("pod1", "1.2.3.4", true, true) pod.Annotations[annotationUpstreams] = "upstream1:1234:dc1" - upstreams, err := ep.processUpstreams(*pod) + upstreams, err := ep.processUpstreams(*pod, corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svcname", + Namespace: "default", + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }) require.NoError(t, err) expected := []api.Upstream{ @@ -193,6 +203,7 @@ func TestProcessUpstreams(t *testing.T) { configEntry func() api.ConfigEntry consulUnavailable bool consulNamespacesEnabled bool + consulPartitionsEnabled bool }{ { name: "upstream with datacenter without ProxyDefaults", @@ -203,6 +214,7 @@ func TestProcessUpstreams(t *testing.T) { }, expErr: "upstream \"upstream1:1234:dc1\" is invalid: there is no ProxyDefaults config to set mesh gateway mode", consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, { name: "upstream with datacenter with ProxyDefaults whose mesh gateway mode is not local or remote", @@ -219,6 +231,7 @@ func TestProcessUpstreams(t *testing.T) { return pd }, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, { name: "upstream with datacenter with ProxyDefaults and mesh gateway is in local mode", @@ -242,6 +255,7 @@ func TestProcessUpstreams(t *testing.T) { return pd }, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, { name: "upstream with datacenter with ProxyDefaults and mesh gateway in remote mode", @@ -265,6 +279,7 @@ func TestProcessUpstreams(t *testing.T) { return pd }, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, { name: "when consul is unavailable, we don't return an error", @@ -290,6 +305,7 @@ func TestProcessUpstreams(t *testing.T) { }, consulUnavailable: true, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, { name: "single upstream", @@ -306,6 +322,7 @@ func TestProcessUpstreams(t *testing.T) { }, }, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, { name: "single upstream with namespace", @@ -323,6 +340,26 @@ func TestProcessUpstreams(t *testing.T) { }, }, consulNamespacesEnabled: true, + consulPartitionsEnabled: false, + }, + { + name: "single upstream with namespace and partition", + pod: func() *corev1.Pod { + pod1 := createPod("pod1", "1.2.3.4", true, true) + pod1.Annotations[annotationUpstreams] = "upstream.foo.bar:1234" + return pod1 + }, + expected: []api.Upstream{ + { + DestinationType: api.UpstreamDestTypeService, + DestinationName: "upstream", + LocalBindPort: 1234, + DestinationNamespace: "foo", + DestinationPartition: "bar", + }, + }, + consulNamespacesEnabled: true, + consulPartitionsEnabled: true, }, { name: "multiple upstreams", @@ -344,6 +381,43 @@ func TestProcessUpstreams(t *testing.T) { }, }, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, + }, + { + name: "multiple upstreams with consul namespaces, partitions and datacenters", + pod: func() *corev1.Pod { + pod1 := createPod("pod1", "1.2.3.4", true, true) + pod1.Annotations[annotationUpstreams] = "upstream1:1234, upstream2.bar:2234, upstream3.foo.baz:3234:dc2" + return pod1 + }, + configEntry: func() api.ConfigEntry { + ce, _ := api.MakeConfigEntry(api.ProxyDefaults, "pd") + pd := ce.(*api.ProxyConfigEntry) + pd.MeshGateway.Mode = "remote" + return pd + }, + expected: []api.Upstream{ + { + DestinationType: api.UpstreamDestTypeService, + DestinationName: "upstream1", + LocalBindPort: 1234, + }, + { + DestinationType: api.UpstreamDestTypeService, + DestinationName: "upstream2", + DestinationNamespace: "bar", + LocalBindPort: 2234, + }, { + DestinationType: api.UpstreamDestTypeService, + DestinationName: "upstream3", + DestinationNamespace: "foo", + DestinationPartition: "baz", + LocalBindPort: 3234, + Datacenter: "dc2", + }, + }, + consulNamespacesEnabled: true, + consulPartitionsEnabled: true, }, { name: "multiple upstreams with consul namespaces and datacenters", @@ -394,6 +468,7 @@ func TestProcessUpstreams(t *testing.T) { }, }, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, { name: "prepared query and non-query upstreams", @@ -420,6 +495,7 @@ func TestProcessUpstreams(t *testing.T) { }, }, consulNamespacesEnabled: false, + consulPartitionsEnabled: false, }, } for _, tt := range cases { @@ -455,9 +531,17 @@ func TestProcessUpstreams(t *testing.T) { AllowK8sNamespacesSet: mapset.NewSetWith("*"), DenyK8sNamespacesSet: mapset.NewSetWith(), EnableConsulNamespaces: tt.consulNamespacesEnabled, + EnableConsulPartitions: tt.consulPartitionsEnabled, } - upstreams, err := ep.processUpstreams(*tt.pod()) + upstreams, err := ep.processUpstreams(*tt.pod(), corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svcname", + Namespace: "default", + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }) if tt.expErr != "" { require.EqualError(t, err, tt.expErr) } else { @@ -468,6 +552,376 @@ func TestProcessUpstreams(t *testing.T) { } } +func TestGetServiceName(t *testing.T) { + t.Parallel() + cases := []struct { + name string + pod func() *corev1.Pod + endpoint *corev1.Endpoints + expSvcName string + }{ + { + name: "single port, with annotation", + pod: func() *corev1.Pod { + pod1 := createPod("pod1", "1.2.3.4", true, true) + pod1.Annotations[annotationService] = "web" + return pod1 + }, + endpoint: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-web", + Namespace: "default", + }, + }, + expSvcName: "web", + }, + { + name: "single port, without annotation", + pod: func() *corev1.Pod { + pod1 := createPod("pod1", "1.2.3.4", true, true) + return pod1 + }, + endpoint: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ep-name", + Namespace: "default", + }, + }, + expSvcName: "ep-name", + }, + { + name: "multi port, with annotation", + pod: func() *corev1.Pod { + pod1 := createPod("pod1", "1.2.3.4", true, true) + pod1.Annotations[annotationService] = "web,web-admin" + return pod1 + }, + endpoint: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ep-name-multiport", + Namespace: "default", + }, + }, + expSvcName: "ep-name-multiport", + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + + svcName := getServiceName(*tt.pod(), *tt.endpoint) + require.Equal(t, tt.expSvcName, svcName) + + }) + } +} + +func TestReconcileCreateEndpoint_MultiportService(t *testing.T) { + t.Parallel() + nodeName := "test-node" + cases := []struct { + name string + consulSvcName string + k8sObjects func() []runtime.Object + initialConsulSvcs []*api.AgentServiceRegistration + expectedNumSvcInstances int + expectedConsulSvcInstancesMap map[string][]*api.CatalogService + expectedProxySvcInstancesMap map[string][]*api.CatalogService + expectedAgentHealthChecks []*api.AgentCheck + }{ + { + name: "Multiport service", + consulSvcName: "web,web-admin", + k8sObjects: func() []runtime.Object { + pod1 := createPod("pod1", "1.2.3.4", true, true) + pod1.Annotations[annotationPort] = "8080,9090" + pod1.Annotations[annotationService] = "web,web-admin" + pod1.Annotations[annotationUpstreams] = "upstream1:1234" + endpoint1 := &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "web", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.2.3.4", + NodeName: &nodeName, + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod1", + Namespace: "default", + }, + }, + }, + }, + }, + } + endpoint2 := &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "web-admin", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.2.3.4", + NodeName: &nodeName, + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod1", + Namespace: "default", + }, + }, + }, + }, + }, + } + return []runtime.Object{pod1, endpoint1, endpoint2} + }, + initialConsulSvcs: []*api.AgentServiceRegistration{}, + expectedNumSvcInstances: 1, + expectedConsulSvcInstancesMap: map[string][]*api.CatalogService{ + "web": { + { + ServiceID: "pod1-web", + ServiceName: "web", + ServiceAddress: "1.2.3.4", + ServicePort: 8080, + ServiceMeta: map[string]string{ + MetaKeyPodName: "pod1", + MetaKeyKubeServiceName: "web", + MetaKeyKubeNS: "default", + MetaKeyManagedBy: managedByValue, + }, + ServiceTags: []string{}, + }, + }, + "web-admin": { + { + ServiceID: "pod1-web-admin", + ServiceName: "web-admin", + ServiceAddress: "1.2.3.4", + ServicePort: 9090, + ServiceMeta: map[string]string{ + MetaKeyPodName: "pod1", + MetaKeyKubeServiceName: "web-admin", + MetaKeyKubeNS: "default", + MetaKeyManagedBy: managedByValue, + }, + ServiceTags: []string{}, + }, + }, + }, + expectedProxySvcInstancesMap: map[string][]*api.CatalogService{ + "web": { + { + ServiceID: "pod1-web-sidecar-proxy", + ServiceName: "web-sidecar-proxy", + ServiceAddress: "1.2.3.4", + ServicePort: 20000, + ServiceProxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "web", + DestinationServiceID: "pod1-web", + LocalServiceAddress: "127.0.0.1", + LocalServicePort: 8080, + Upstreams: []api.Upstream{ + { + DestinationType: api.UpstreamDestTypeService, + DestinationName: "upstream1", + LocalBindPort: 1234, + }, + }, + }, + ServiceMeta: map[string]string{ + MetaKeyPodName: "pod1", + MetaKeyKubeServiceName: "web", + MetaKeyKubeNS: "default", + MetaKeyManagedBy: managedByValue, + }, + ServiceTags: []string{}, + }, + }, + "web-admin": { + { + ServiceID: "pod1-web-admin-sidecar-proxy", + ServiceName: "web-admin-sidecar-proxy", + ServiceAddress: "1.2.3.4", + ServicePort: 20001, + ServiceProxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "web-admin", + DestinationServiceID: "pod1-web-admin", + LocalServiceAddress: "127.0.0.1", + LocalServicePort: 9090, + }, + ServiceMeta: map[string]string{ + MetaKeyPodName: "pod1", + MetaKeyKubeServiceName: "web-admin", + MetaKeyKubeNS: "default", + MetaKeyManagedBy: managedByValue, + }, + ServiceTags: []string{}, + }, + }, + }, + expectedAgentHealthChecks: []*api.AgentCheck{ + { + CheckID: "default/pod1-web/kubernetes-health-check", + ServiceName: "web", + ServiceID: "pod1-web", + Name: "Kubernetes Health Check", + Status: api.HealthPassing, + Output: kubernetesSuccessReasonMsg, + Type: ttl, + }, + { + CheckID: "default/pod1-web-admin/kubernetes-health-check", + ServiceName: "web-admin", + ServiceID: "pod1-web-admin", + Name: "Kubernetes Health Check", + Status: api.HealthPassing, + Output: kubernetesSuccessReasonMsg, + Type: ttl, + }, + }, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + // The agent pod needs to have the address 127.0.0.1 so when the + // code gets the agent pods via the label component=client, and + // makes requests against the agent API, it will actually hit the + // test server we have on localhost. + fakeClientPod := createPod("fake-consul-client", "127.0.0.1", false, true) + fakeClientPod.Labels = map[string]string{"component": "client", "app": "consul", "release": "consul"} + + // Add the default namespace. + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}} + // Create fake k8s client + k8sObjects := append(tt.k8sObjects(), fakeClientPod, &ns) + + fakeClient := fake.NewClientBuilder().WithRuntimeObjects(k8sObjects...).Build() + + // Create test consul server + consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.NodeName = nodeName + }) + require.NoError(t, err) + defer consul.Stop() + consul.WaitForServiceIntentions(t) + + cfg := &api.Config{ + Address: consul.HTTPAddr, + } + consulClient, err := api.NewClient(cfg) + require.NoError(t, err) + addr := strings.Split(consul.HTTPAddr, ":") + consulPort := addr[1] + + // Register service and proxy in consul. + for _, svc := range tt.initialConsulSvcs { + err = consulClient.Agent().ServiceRegister(svc) + require.NoError(t, err) + } + + // Create the endpoints controller + ep := &EndpointsController{ + Client: fakeClient, + Log: logrtest.TestLogger{T: t}, + ConsulClient: consulClient, + ConsulPort: consulPort, + ConsulScheme: "http", + AllowK8sNamespacesSet: mapset.NewSetWith("*"), + DenyK8sNamespacesSet: mapset.NewSetWith(), + ReleaseName: "consul", + ReleaseNamespace: "default", + ConsulClientCfg: cfg, + } + namespacedName := types.NamespacedName{ + Namespace: "default", + Name: "web", + } + namespacedName2 := types.NamespacedName{ + Namespace: "default", + Name: "web-admin", + } + + resp, err := ep.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: namespacedName, + }) + require.NoError(t, err) + require.False(t, resp.Requeue) + resp, err = ep.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: namespacedName2, + }) + require.NoError(t, err) + require.False(t, resp.Requeue) + + // After reconciliation, Consul should have the service with the correct number of instances + svcs := strings.Split(tt.consulSvcName, ",") + for _, service := range svcs { + serviceInstances, _, err := consulClient.Catalog().Service(service, "", nil) + require.NoError(t, err) + require.Len(t, serviceInstances, tt.expectedNumSvcInstances) + for i, instance := range serviceInstances { + require.Equal(t, tt.expectedConsulSvcInstancesMap[service][i].ServiceID, instance.ServiceID) + require.Equal(t, tt.expectedConsulSvcInstancesMap[service][i].ServiceName, instance.ServiceName) + require.Equal(t, tt.expectedConsulSvcInstancesMap[service][i].ServiceAddress, instance.ServiceAddress) + require.Equal(t, tt.expectedConsulSvcInstancesMap[service][i].ServicePort, instance.ServicePort) + require.Equal(t, tt.expectedConsulSvcInstancesMap[service][i].ServiceMeta, instance.ServiceMeta) + require.Equal(t, tt.expectedConsulSvcInstancesMap[service][i].ServiceTags, instance.ServiceTags) + } + proxyServiceInstances, _, err := consulClient.Catalog().Service(fmt.Sprintf("%s-sidecar-proxy", service), "", nil) + require.NoError(t, err) + require.Len(t, proxyServiceInstances, tt.expectedNumSvcInstances) + for i, instance := range proxyServiceInstances { + require.Equal(t, tt.expectedProxySvcInstancesMap[service][i].ServiceID, instance.ServiceID) + require.Equal(t, tt.expectedProxySvcInstancesMap[service][i].ServiceName, instance.ServiceName) + require.Equal(t, tt.expectedProxySvcInstancesMap[service][i].ServiceAddress, instance.ServiceAddress) + require.Equal(t, tt.expectedProxySvcInstancesMap[service][i].ServicePort, instance.ServicePort) + require.Equal(t, tt.expectedProxySvcInstancesMap[service][i].ServiceMeta, instance.ServiceMeta) + require.Equal(t, tt.expectedProxySvcInstancesMap[service][i].ServiceTags, instance.ServiceTags) + + // When comparing the ServiceProxy field we ignore the DestinationNamespace + // field within that struct because on Consul OSS it's set to "" but on Consul Enterprise + // it's set to "default" and we want to re-use this test for both OSS and Ent. + // This does mean that we don't test that field but that's okay because + // it's not getting set specifically in this test. + // To do the comparison that ignores that field we use go-cmp instead + // of the regular require.Equal call since it supports ignoring certain + // fields. + diff := cmp.Diff(tt.expectedProxySvcInstancesMap[service][i].ServiceProxy, instance.ServiceProxy, + cmpopts.IgnoreFields(api.Upstream{}, "DestinationNamespace", "DestinationPartition")) + require.Empty(t, diff, "expected objects to be equal") + } + _, checkInfos, err := consulClient.Agent().AgentHealthServiceByName(fmt.Sprintf("%s-sidecar-proxy", service)) + expectedChecks := []string{"Proxy Public Listener", "Destination Alias"} + require.NoError(t, err) + require.Len(t, checkInfos, tt.expectedNumSvcInstances) + for _, checkInfo := range checkInfos { + checks := checkInfo.Checks + require.Contains(t, expectedChecks, checks[0].Name) + require.Contains(t, expectedChecks, checks[1].Name) + } + } + + // Check that the Consul health check was created for the k8s pod. + if tt.expectedAgentHealthChecks != nil { + for i := range tt.expectedAgentHealthChecks { + filter := fmt.Sprintf("CheckID == `%s`", tt.expectedAgentHealthChecks[i].CheckID) + check, err := consulClient.Agent().ChecksWithFilter(filter) + require.NoError(t, err) + require.EqualValues(t, len(check), 1) + // Ignoring Namespace because the response from ENT includes it and OSS does not. + var ignoredFields = []string{"Node", "Definition", "Namespace", "Partition"} + require.True(t, cmp.Equal(check[tt.expectedAgentHealthChecks[i].CheckID], tt.expectedAgentHealthChecks[i], cmpopts.IgnoreFields(api.AgentCheck{}, ignoredFields...))) + } + } + }) + } +} + // TestReconcileCreateEndpoint tests the logic to create service instances in Consul from the addresses in the Endpoints // object. The cases test an empty endpoints object, a basic endpoints object with one address, a basic endpoints object // with two addresses, and an endpoints object with every possible customization. @@ -819,8 +1273,9 @@ func TestReconcileCreateEndpoint(t *testing.T) { pod1.Annotations[annotationService] = "different-consul-svc-name" pod1.Annotations[fmt.Sprintf("%sname", annotationMeta)] = "abc" pod1.Annotations[fmt.Sprintf("%sversion", annotationMeta)] = "2" - pod1.Annotations[annotationTags] = "abc,123" - pod1.Annotations[annotationConnectTags] = "def,456" + pod1.Annotations[fmt.Sprintf("%spod_name", annotationMeta)] = "$POD_NAME" + pod1.Annotations[annotationTags] = "abc,123,$POD_NAME" + pod1.Annotations[annotationConnectTags] = "def,456,$POD_NAME" pod1.Annotations[annotationUpstreams] = "upstream1:1234" pod1.Annotations[annotationEnableMetrics] = "true" pod1.Annotations[annotationPrometheusScrapePort] = "12345" @@ -858,12 +1313,13 @@ func TestReconcileCreateEndpoint(t *testing.T) { ServiceMeta: map[string]string{ "name": "abc", "version": "2", + "pod_name": "pod1", MetaKeyPodName: "pod1", MetaKeyKubeServiceName: "service-created", MetaKeyKubeNS: "default", MetaKeyManagedBy: managedByValue, }, - ServiceTags: []string{"abc", "123", "def", "456"}, + ServiceTags: []string{"abc", "123", "pod1", "def", "456", "pod1"}, }, }, expectedProxySvcInstances: []*api.CatalogService{ @@ -891,12 +1347,13 @@ func TestReconcileCreateEndpoint(t *testing.T) { ServiceMeta: map[string]string{ "name": "abc", "version": "2", + "pod_name": "pod1", MetaKeyPodName: "pod1", MetaKeyKubeServiceName: "service-created", MetaKeyKubeNS: "default", MetaKeyManagedBy: managedByValue, }, - ServiceTags: []string{"abc", "123", "def", "456"}, + ServiceTags: []string{"abc", "123", "pod1", "def", "456", "pod1"}, }, }, expectedAgentHealthChecks: []*api.AgentCheck{ @@ -998,9 +1455,20 @@ func TestReconcileCreateEndpoint(t *testing.T) { require.Equal(t, tt.expectedProxySvcInstances[i].ServiceName, instance.ServiceName) require.Equal(t, tt.expectedProxySvcInstances[i].ServiceAddress, instance.ServiceAddress) require.Equal(t, tt.expectedProxySvcInstances[i].ServicePort, instance.ServicePort) - require.Equal(t, tt.expectedProxySvcInstances[i].ServiceProxy, instance.ServiceProxy) require.Equal(t, tt.expectedProxySvcInstances[i].ServiceMeta, instance.ServiceMeta) require.Equal(t, tt.expectedProxySvcInstances[i].ServiceTags, instance.ServiceTags) + + // When comparing the ServiceProxy field we ignore the DestinationNamespace + // field within that struct because on Consul OSS it's set to "" but on Consul Enterprise + // it's set to "default" and we want to re-use this test for both OSS and Ent. + // This does mean that we don't test that field but that's okay because + // it's not getting set specifically in this test. + // To do the comparison that ignores that field we use go-cmp instead + // of the regular require.Equal call since it supports ignoring certain + // fields. + diff := cmp.Diff(tt.expectedProxySvcInstances[i].ServiceProxy, instance.ServiceProxy, + cmpopts.IgnoreFields(api.Upstream{}, "DestinationNamespace", "DestinationPartition")) + require.Empty(t, diff, "expected objects to be equal") } _, checkInfos, err := consulClient.Agent().AgentHealthServiceByName(fmt.Sprintf("%s-sidecar-proxy", tt.consulSvcName)) @@ -1021,7 +1489,7 @@ func TestReconcileCreateEndpoint(t *testing.T) { require.NoError(t, err) require.EqualValues(t, len(check), 1) // Ignoring Namespace because the response from ENT includes it and OSS does not. - var ignoredFields = []string{"Node", "Definition", "Namespace"} + var ignoredFields = []string{"Node", "Definition", "Namespace", "Partition"} require.True(t, cmp.Equal(check[tt.expectedAgentHealthChecks[i].CheckID], tt.expectedAgentHealthChecks[i], cmpopts.IgnoreFields(api.AgentCheck{}, ignoredFields...))) } } @@ -2262,7 +2730,7 @@ func TestReconcileUpdateEndpoint(t *testing.T) { consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { if tt.enableACLs { c.ACL.Enabled = tt.enableACLs - c.ACL.Tokens.Master = adminToken + c.ACL.Tokens.InitialManagement = adminToken } c.NodeName = nodeName }) @@ -2367,7 +2835,7 @@ func TestReconcileUpdateEndpoint(t *testing.T) { require.NoError(t, err) require.EqualValues(t, len(check), 1) // Ignoring Namespace because the response from ENT includes it and OSS does not. - var ignoredFields = []string{"Node", "Definition", "Namespace"} + var ignoredFields = []string{"Node", "Definition", "Namespace", "Partition"} require.True(t, cmp.Equal(check[tt.expectedAgentHealthChecks[i].CheckID], tt.expectedAgentHealthChecks[i], cmpopts.IgnoreFields(api.AgentCheck{}, ignoredFields...))) } } @@ -2411,16 +2879,17 @@ func TestReconcileDeleteEndpoint(t *testing.T) { t.Parallel() nodeName := "test-node" cases := []struct { - name string - consulSvcName string - legacyService bool - initialConsulSvcs []*api.AgentServiceRegistration - enableACLs bool + name string + consulSvcName string + expectServicesToBeDeleted bool + initialConsulSvcs []*api.AgentServiceRegistration + enableACLs bool + consulClientReady bool }{ { - name: "Legacy service: does not delete", - consulSvcName: "service-deleted", - legacyService: true, + name: "Legacy service: does not delete", + consulSvcName: "service-deleted", + expectServicesToBeDeleted: false, initialConsulSvcs: []*api.AgentServiceRegistration{ { ID: "pod1-service-deleted", @@ -2442,10 +2911,12 @@ func TestReconcileDeleteEndpoint(t *testing.T) { Meta: map[string]string{"k8s-service-name": "service-deleted", "k8s-namespace": "default"}, }, }, + consulClientReady: true, }, { - name: "Consul service name matches K8s service name", - consulSvcName: "service-deleted", + name: "Consul service name matches K8s service name", + consulSvcName: "service-deleted", + expectServicesToBeDeleted: true, initialConsulSvcs: []*api.AgentServiceRegistration{ { ID: "pod1-service-deleted", @@ -2467,10 +2938,12 @@ func TestReconcileDeleteEndpoint(t *testing.T) { Meta: map[string]string{"k8s-service-name": "service-deleted", "k8s-namespace": "default", MetaKeyManagedBy: managedByValue}, }, }, + consulClientReady: true, }, { - name: "Consul service name does not match K8s service name", - consulSvcName: "different-consul-svc-name", + name: "Consul service name does not match K8s service name", + consulSvcName: "different-consul-svc-name", + expectServicesToBeDeleted: true, initialConsulSvcs: []*api.AgentServiceRegistration{ { ID: "pod1-different-consul-svc-name", @@ -2492,10 +2965,12 @@ func TestReconcileDeleteEndpoint(t *testing.T) { Meta: map[string]string{"k8s-service-name": "service-deleted", "k8s-namespace": "default", MetaKeyManagedBy: managedByValue}, }, }, + consulClientReady: true, }, { - name: "When ACLs are enabled, the token should be deleted", - consulSvcName: "service-deleted", + name: "When ACLs are enabled, the token should be deleted", + consulSvcName: "service-deleted", + expectServicesToBeDeleted: true, initialConsulSvcs: []*api.AgentServiceRegistration{ { ID: "pod1-service-deleted", @@ -2527,7 +3002,45 @@ func TestReconcileDeleteEndpoint(t *testing.T) { }, }, }, - enableACLs: true, + enableACLs: true, + consulClientReady: true, + }, + { + name: "When Consul client pod is not ready, services are not deleted", + consulSvcName: "service-deleted", + expectServicesToBeDeleted: false, + initialConsulSvcs: []*api.AgentServiceRegistration{ + { + ID: "pod1-service-deleted", + Name: "service-deleted", + Port: 80, + Address: "1.2.3.4", + Meta: map[string]string{ + MetaKeyKubeServiceName: "service-deleted", + MetaKeyKubeNS: "default", + MetaKeyManagedBy: managedByValue, + MetaKeyPodName: "pod1", + }, + }, + { + Kind: api.ServiceKindConnectProxy, + ID: "pod1-service-deleted-sidecar-proxy", + Name: "service-deleted-sidecar-proxy", + Port: 20000, + Address: "1.2.3.4", + Proxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "service-deleted", + DestinationServiceID: "pod1-service-deleted", + }, + Meta: map[string]string{ + MetaKeyKubeServiceName: "service-deleted", + MetaKeyKubeNS: "default", + MetaKeyManagedBy: managedByValue, + MetaKeyPodName: "pod1", + }, + }, + }, + consulClientReady: false, }, } for _, tt := range cases { @@ -2538,6 +3051,9 @@ func TestReconcileDeleteEndpoint(t *testing.T) { // test server we have on localhost. fakeClientPod := createPod("fake-consul-client", "127.0.0.1", false, true) fakeClientPod.Labels = map[string]string{"component": "client", "app": "consul", "release": "consul"} + if !tt.consulClientReady { + fakeClientPod.Status.Conditions = []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionFalse}} + } // Add the default namespace. ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}} @@ -2549,7 +3065,7 @@ func TestReconcileDeleteEndpoint(t *testing.T) { consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { if tt.enableACLs { c.ACL.Enabled = true - c.ACL.Tokens.Master = adminToken + c.ACL.Tokens.InitialManagement = adminToken } c.NodeName = nodeName }) @@ -2620,16 +3136,16 @@ func TestReconcileDeleteEndpoint(t *testing.T) { // After reconciliation, Consul should not have any instances of service-deleted serviceInstances, _, err := consulClient.Catalog().Service(tt.consulSvcName, "", nil) // If it's not managed by endpoints controller (legacy service), Consul should have service instances - if tt.legacyService { + if tt.expectServicesToBeDeleted { + require.NoError(t, err) + require.Empty(t, serviceInstances) + proxyServiceInstances, _, err := consulClient.Catalog().Service(fmt.Sprintf("%s-sidecar-proxy", tt.consulSvcName), "", nil) + require.NoError(t, err) + require.Empty(t, proxyServiceInstances) + } else { require.NoError(t, err) require.NotEmpty(t, serviceInstances) - return } - require.NoError(t, err) - require.Empty(t, serviceInstances) - proxyServiceInstances, _, err := consulClient.Catalog().Service(fmt.Sprintf("%s-sidecar-proxy", tt.consulSvcName), "", nil) - require.NoError(t, err) - require.Empty(t, proxyServiceInstances) if tt.enableACLs { _, _, err = consulClient.ACL().TokenRead(token.AccessorID, nil) @@ -2639,6 +3155,224 @@ func TestReconcileDeleteEndpoint(t *testing.T) { } } +// TestReconcileIgnoresServiceIgnoreLabel tests that the endpoints controller correctly ignores services +// with the service-ignore label and deregisters services previously registered if the service-ignore +// label is added. +func TestReconcileIgnoresServiceIgnoreLabel(t *testing.T) { + t.Parallel() + nodeName := "test-node" + serviceName := "service-ignored" + namespace := "default" + + cases := map[string]struct { + svcInitiallyRegistered bool + serviceLabels map[string]string + expectedNumSvcInstances int + }{ + "Registered endpoint with label is deregistered.": { + svcInitiallyRegistered: true, + serviceLabels: map[string]string{ + labelServiceIgnore: "true", + }, + expectedNumSvcInstances: 0, + }, + "Not registered endpoint with label is never registered": { + svcInitiallyRegistered: false, + serviceLabels: map[string]string{ + labelServiceIgnore: "true", + }, + expectedNumSvcInstances: 0, + }, + "Registered endpoint without label is unaffected": { + svcInitiallyRegistered: true, + serviceLabels: map[string]string{}, + expectedNumSvcInstances: 1, + }, + "Not registered endpoint without label is registered": { + svcInitiallyRegistered: false, + serviceLabels: map[string]string{}, + expectedNumSvcInstances: 1, + }, + } + + for name, tt := range cases { + t.Run(name, func(t *testing.T) { + // Set up the fake Kubernetes client with an endpoint, pod, consul client, and the default namespace. + endpoint := &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: namespace, + Labels: tt.serviceLabels, + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.2.3.4", + NodeName: &nodeName, + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod1", + Namespace: namespace, + }, + }, + }, + }, + }, + } + pod1 := createPod("pod1", "1.2.3.4", true, true) + fakeClientPod := createPod("fake-consul-client", "127.0.0.1", false, true) + fakeClientPod.Labels = map[string]string{"component": "client", "app": "consul", "release": "consul"} + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + k8sObjects := []runtime.Object{endpoint, pod1, fakeClientPod, &ns} + fakeClient := fake.NewClientBuilder().WithRuntimeObjects(k8sObjects...).Build() + + // Create test Consul server. + consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { c.NodeName = nodeName }) + require.NoError(t, err) + defer consul.Stop() + consul.WaitForServiceIntentions(t) + cfg := &api.Config{Address: consul.HTTPAddr} + consulClient, err := api.NewClient(cfg) + require.NoError(t, err) + addr := strings.Split(consul.HTTPAddr, ":") + consulPort := addr[1] + + // Set up the initial Consul services. + if tt.svcInitiallyRegistered { + err = consulClient.Agent().ServiceRegister(&api.AgentServiceRegistration{ + ID: "pod1-" + serviceName, + Name: serviceName, + Port: 0, + Address: "1.2.3.4", + Meta: map[string]string{ + "k8s-namespace": namespace, + "k8s-service-name": serviceName, + "managed-by": "consul-k8s-endpoints-controller", + "pod-name": "pod1", + }, + }) + require.NoError(t, err) + err = consulClient.Agent().ServiceRegister(&api.AgentServiceRegistration{ + ID: "pod1-sidecar-proxy-" + serviceName, + Name: serviceName + "-sidecar-proxy", + Port: 0, + Meta: map[string]string{ + "k8s-namespace": namespace, + "k8s-service-name": serviceName, + "managed-by": "consul-k8s-endpoints-controller", + "pod-name": "pod1", + }, + }) + require.NoError(t, err) + } + + // Create the endpoints controller. + ep := &EndpointsController{ + Client: fakeClient, + Log: logrtest.TestLogger{T: t}, + ConsulClient: consulClient, + ConsulPort: consulPort, + ConsulScheme: "http", + AllowK8sNamespacesSet: mapset.NewSetWith("*"), + DenyK8sNamespacesSet: mapset.NewSetWith(), + ReleaseName: "consul", + ReleaseNamespace: namespace, + ConsulClientCfg: cfg, + } + + // Run the reconcile process to deregister the service if it was registered before. + namespacedName := types.NamespacedName{Namespace: namespace, Name: serviceName} + resp, err := ep.Reconcile(context.Background(), ctrl.Request{NamespacedName: namespacedName}) + require.NoError(t, err) + require.False(t, resp.Requeue) + + // Check that the correct number of services are registered with Consul. + serviceInstances, _, err := consulClient.Catalog().Service(serviceName, "", nil) + require.NoError(t, err) + require.Len(t, serviceInstances, tt.expectedNumSvcInstances) + proxyServiceInstances, _, err := consulClient.Catalog().Service(serviceName+"-sidecar-proxy", "", nil) + require.NoError(t, err) + require.Len(t, proxyServiceInstances, tt.expectedNumSvcInstances) + }) + } +} + +// Test that when endpoints pods have not been connect-injected (i.e. not in the service mesh) +// we don't make any API calls to Consul. +// This is because we want to avoid any unnecessary calls. Especially if the client agent is unreachable +// and any calls to it will result in an i/o timeout errors, it will +// slow down processing of the events by the endpoints controller making unnecessary calls and waiting for ~30sec. +func TestReconcile_endpointsIgnoredWhenNotInjected(t *testing.T) { + nodeName := "test-node" + namespace := "default" + + // Set up the fake Kubernetes client with an endpoint, pod, consul client, and the default namespace. + endpoint := &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-in-mesh", + Namespace: namespace, + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.2.3.4", + NodeName: &nodeName, + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod1", + Namespace: namespace, + }, + }, + }, + }, + }, + } + pod1 := createPod("pod1", "1.2.3.4", false, true) + fakeClientPod := createPod("fake-consul-client", "127.0.0.1", false, true) + fakeClientPod.Labels = map[string]string{"component": "client", "app": "consul", "release": "consul"} + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + k8sObjects := []runtime.Object{endpoint, pod1, fakeClientPod, &ns} + fakeClient := fake.NewClientBuilder().WithRuntimeObjects(k8sObjects...).Build() + + // Create test Consul server. + consul := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + if r != nil { + t.Fatalf("should not receive any calls to Consul client") + } + })) + t.Cleanup(consul.Close) + + cfg := &api.Config{Address: consul.URL} + consulClient, err := api.NewClient(cfg) + require.NoError(t, err) + parsedURL, err := url.Parse(consul.URL) + require.NoError(t, err) + consulPort := parsedURL.Port() + + // Create the endpoints controller. + ep := &EndpointsController{ + Client: fakeClient, + Log: logrtest.TestLogger{T: t}, + ConsulClient: consulClient, + ConsulPort: consulPort, + ConsulScheme: "http", + AllowK8sNamespacesSet: mapset.NewSetWith("*"), + DenyK8sNamespacesSet: mapset.NewSetWith(), + ReleaseName: "consul", + ReleaseNamespace: namespace, + ConsulClientCfg: cfg, + } + + // Run the reconcile process to deregister the service if it was registered before. + namespacedName := types.NamespacedName{Namespace: namespace, Name: "not-in-mesh"} + resp, err := ep.Reconcile(context.Background(), ctrl.Request{NamespacedName: namespacedName}) + require.NoError(t, err) + require.False(t, resp.Requeue) +} + func TestFilterAgentPods(t *testing.T) { t.Parallel() cases := map[string]struct { @@ -4658,84 +5392,6 @@ func TestCreateServiceRegistrations_withTransparentProxy(t *testing.T) { } } -func TestRegisterServicesAndHealthCheck_skipsWhenDuplicateServiceFound(t *testing.T) { - t.Parallel() - - nodeName := "test-node" - consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { - c.NodeName = nodeName - }) - require.NoError(t, err) - defer consul.Stop() - - consul.WaitForServiceIntentions(t) - httpAddr := consul.HTTPAddr - clientConfig := &api.Config{ - Address: httpAddr, - } - consulClient, err := api.NewClient(clientConfig) - require.NoError(t, err) - addr := strings.Split(httpAddr, ":") - consulPort := addr[1] - - existingService := &api.AgentServiceRegistration{ - ID: "test-service", - Name: "test-service", - Port: 1234, - Address: "1.2.3.4", - Meta: map[string]string{MetaKeyKubeNS: "some-other-ns"}, - } - err = consulClient.Agent().ServiceRegister(existingService) - require.NoError(t, err) - pod := createPod("test-pod", "1.1.1.1", true, true) - - endpointsAddress := corev1.EndpointAddress{ - IP: "1.2.3.4", - NodeName: &nodeName, - TargetRef: &corev1.ObjectReference{ - Kind: "Pod", - Name: pod.Name, - Namespace: pod.Namespace, - }, - } - endpoints := &corev1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-service", - Namespace: "default", - }, - Subsets: []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{endpointsAddress}, - }, - }, - } - ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}} - fakeClient := fake.NewClientBuilder().WithRuntimeObjects(ns, pod, endpoints).Build() - - ep := &EndpointsController{ - Log: logrtest.TestLogger{T: t}, - ConsulClient: consulClient, - ConsulPort: consulPort, - ConsulScheme: "http", - ConsulClientCfg: clientConfig, - AllowK8sNamespacesSet: mapset.NewSetWith("*"), - DenyK8sNamespacesSet: mapset.NewSetWith(), - Client: fakeClient, - } - - err = ep.registerServicesAndHealthCheck(context.Background(), *endpoints, endpointsAddress, api.HealthPassing, make(map[string]bool)) - require.NoError(t, err) - - // Check that the service is not registered with Consul. - _, _, err = consulClient.Agent().Service("test-pod-test-service", nil) - require.Error(t, err) - require.Contains(t, err.Error(), "Unexpected response code: 404 (unknown service ID") - - _, _, err = consulClient.Agent().Service("test-pod-test-service-sidecar-proxy", nil) - require.Error(t, err) - require.Contains(t, err.Error(), "Unexpected response code: 404 (unknown service ID") -} - func TestGetTokenMetaFromDescription(t *testing.T) { t.Parallel() cases := map[string]struct { @@ -4761,6 +5417,74 @@ func TestGetTokenMetaFromDescription(t *testing.T) { } } +func TestMapAddresses(t *testing.T) { + t.Parallel() + cases := map[string]struct { + addresses corev1.EndpointSubset + expected map[corev1.EndpointAddress]string + }{ + "ready and not ready addresses": { + addresses: corev1.EndpointSubset{ + Addresses: []corev1.EndpointAddress{ + {Hostname: "host1"}, + {Hostname: "host2"}, + }, + NotReadyAddresses: []corev1.EndpointAddress{ + {Hostname: "host3"}, + {Hostname: "host4"}, + }, + }, + expected: map[corev1.EndpointAddress]string{ + {Hostname: "host1"}: api.HealthPassing, + {Hostname: "host2"}: api.HealthPassing, + {Hostname: "host3"}: api.HealthCritical, + {Hostname: "host4"}: api.HealthCritical, + }, + }, + "ready addresses only": { + addresses: corev1.EndpointSubset{ + Addresses: []corev1.EndpointAddress{ + {Hostname: "host1"}, + {Hostname: "host2"}, + {Hostname: "host3"}, + {Hostname: "host4"}, + }, + NotReadyAddresses: []corev1.EndpointAddress{}, + }, + expected: map[corev1.EndpointAddress]string{ + {Hostname: "host1"}: api.HealthPassing, + {Hostname: "host2"}: api.HealthPassing, + {Hostname: "host3"}: api.HealthPassing, + {Hostname: "host4"}: api.HealthPassing, + }, + }, + "not ready addresses only": { + addresses: corev1.EndpointSubset{ + Addresses: []corev1.EndpointAddress{}, + NotReadyAddresses: []corev1.EndpointAddress{ + {Hostname: "host1"}, + {Hostname: "host2"}, + {Hostname: "host3"}, + {Hostname: "host4"}, + }, + }, + expected: map[corev1.EndpointAddress]string{ + {Hostname: "host1"}: api.HealthCritical, + {Hostname: "host2"}: api.HealthCritical, + {Hostname: "host3"}: api.HealthCritical, + {Hostname: "host4"}: api.HealthCritical, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actual := mapAddresses(c.addresses) + require.Equal(t, c.expected, actual) + }) + } +} + func createPod(name, ip string, inject bool, managedByEndpointsController bool) *corev1.Pod { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -4772,6 +5496,12 @@ func createPod(name, ip string, inject bool, managedByEndpointsController bool) Status: corev1.PodStatus{ PodIP: ip, HostIP: "127.0.0.1", + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }, + }, }, } if inject { diff --git a/control-plane/connect-inject/envoy_sidecar.go b/control-plane/connect-inject/envoy_sidecar.go index b521297c54..02d3869556 100644 --- a/control-plane/connect-inject/envoy_sidecar.go +++ b/control-plane/connect-inject/envoy_sidecar.go @@ -10,19 +10,25 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) -func (h *Handler) envoySidecar(namespace corev1.Namespace, pod corev1.Pod) (corev1.Container, error) { +func (h *Handler) envoySidecar(namespace corev1.Namespace, pod corev1.Pod, mpi multiPortInfo) (corev1.Container, error) { resources, err := h.envoySidecarResources(pod) if err != nil { return corev1.Container{}, err } - cmd, err := h.getContainerSidecarCommand(pod) + multiPort := mpi.serviceName != "" + cmd, err := h.getContainerSidecarCommand(pod, mpi.serviceName, mpi.serviceIndex) if err != nil { return corev1.Container{}, err } + containerName := envoySidecarContainer + if multiPort { + containerName = fmt.Sprintf("%s-%s", envoySidecarContainer, mpi.serviceName) + } + container := corev1.Container{ - Name: envoySidecarContainer, + Name: containerName, Image: h.ImageEnvoy, Env: []corev1.EnvVar{ { @@ -62,7 +68,7 @@ func (h *Handler) envoySidecar(namespace corev1.Namespace, pod corev1.Pod) (core // has only injected init containers so all containers defined in pod.Spec.Containers are from the user. for _, c := range pod.Spec.Containers { // User container and Envoy container cannot have the same UID. - if c.SecurityContext != nil && c.SecurityContext.RunAsUser != nil && *c.SecurityContext.RunAsUser == envoyUserAndGroupID { + if c.SecurityContext != nil && c.SecurityContext.RunAsUser != nil && *c.SecurityContext.RunAsUser == envoyUserAndGroupID && c.Image != h.ImageEnvoy { return corev1.Container{}, fmt.Errorf("container %q has runAsUser set to the same uid %q as envoy which is not allowed", c.Name, envoyUserAndGroupID) } } @@ -76,10 +82,18 @@ func (h *Handler) envoySidecar(namespace corev1.Namespace, pod corev1.Pod) (core return container, nil } -func (h *Handler) getContainerSidecarCommand(pod corev1.Pod) ([]string, error) { +func (h *Handler) getContainerSidecarCommand(pod corev1.Pod, multiPortSvcName string, multiPortSvcIdx int) ([]string, error) { + bootstrapFile := "/consul/connect-inject/envoy-bootstrap.yaml" + if multiPortSvcName != "" { + bootstrapFile = fmt.Sprintf("/consul/connect-inject/envoy-bootstrap-%s.yaml", multiPortSvcName) + } cmd := []string{ "envoy", - "--config-path", "/consul/connect-inject/envoy-bootstrap.yaml", + "--config-path", bootstrapFile, + } + if multiPortSvcName != "" { + // --base-id is needed so multiple Envoy proxies can run on the same host. + cmd = append(cmd, "--base-id", fmt.Sprintf("%d", multiPortSvcIdx)) } extraArgs, annotationSet := pod.Annotations[annotationEnvoyExtraArgs] diff --git a/control-plane/connect-inject/envoy_sidecar_test.go b/control-plane/connect-inject/envoy_sidecar_test.go index 269581c4b8..56af91ab3e 100644 --- a/control-plane/connect-inject/envoy_sidecar_test.go +++ b/control-plane/connect-inject/envoy_sidecar_test.go @@ -28,7 +28,7 @@ func TestHandlerEnvoySidecar(t *testing.T) { }, }, } - container, err := h.envoySidecar(testNS, pod) + container, err := h.envoySidecar(testNS, pod, multiPortInfo{}) require.NoError(err) require.Equal(container.Command, []string{ "envoy", @@ -43,6 +43,55 @@ func TestHandlerEnvoySidecar(t *testing.T) { }) } +func TestHandlerEnvoySidecar_Multiport(t *testing.T) { + require := require.New(t) + h := Handler{} + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationService: "web,web-admin", + }, + }, + + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "web", + }, + { + Name: "web-admin", + }, + }, + }, + } + multiPortInfos := []multiPortInfo{ + { + serviceIndex: 0, + serviceName: "web", + }, + { + serviceIndex: 1, + serviceName: "web-admin", + }, + } + expCommand := map[int][]string{ + 0: {"envoy", "--config-path", "/consul/connect-inject/envoy-bootstrap-web.yaml", "--base-id", "0"}, + 1: {"envoy", "--config-path", "/consul/connect-inject/envoy-bootstrap-web-admin.yaml", "--base-id", "1"}, + } + for i := 0; i < 2; i++ { + container, err := h.envoySidecar(testNS, pod, multiPortInfos[i]) + require.NoError(err) + require.Equal(expCommand[i], container.Command) + + require.Equal(container.VolumeMounts, []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/consul/connect-inject", + }, + }) + } +} + func TestHandlerEnvoySidecar_withSecurityContext(t *testing.T) { cases := map[string]struct { tproxyEnabled bool @@ -106,7 +155,7 @@ func TestHandlerEnvoySidecar_withSecurityContext(t *testing.T) { }, }, } - ec, err := h.envoySidecar(testNS, pod) + ec, err := h.envoySidecar(testNS, pod, multiPortInfo{}) require.NoError(t, err) require.Equal(t, c.expSecurityContext, ec.SecurityContext) }) @@ -130,37 +179,88 @@ func TestHandlerEnvoySidecar_FailsWithDuplicatePodSecurityContextUID(t *testing. }, }, } - _, err := h.envoySidecar(testNS, pod) + _, err := h.envoySidecar(testNS, pod, multiPortInfo{}) require.Error(err, fmt.Sprintf("pod security context cannot have the same uid as envoy: %v", envoyUserAndGroupID)) } -// Test that if the user specifies a container with security context with the same uid as `envoyUserAndGroupID` -// that we return an error to the handler. +// Test that if the user specifies a container with security context with the same uid as `envoyUserAndGroupID` that we +// return an error to the handler. If a container using the envoy image has the same uid, we don't return an error +// because in multiport pod there can be multiple envoy sidecars. func TestHandlerEnvoySidecar_FailsWithDuplicateContainerSecurityContextUID(t *testing.T) { - require := require.New(t) - h := Handler{} - pod := corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "web", - // Setting RunAsUser: 1 should succeed. - SecurityContext: &corev1.SecurityContext{ - RunAsUser: pointerToInt64(1), + cases := []struct { + name string + pod corev1.Pod + handler Handler + expErr bool + expErrMessage error + }{ + { + name: "fails with non envoy image", + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "web", + // Setting RunAsUser: 1 should succeed. + SecurityContext: &corev1.SecurityContext{ + RunAsUser: pointerToInt64(1), + }, + }, + { + Name: "app", + // Setting RunAsUser: 5995 should fail. + SecurityContext: &corev1.SecurityContext{ + RunAsUser: pointerToInt64(envoyUserAndGroupID), + }, + Image: "not-envoy", + }, }, }, - { - Name: "app", - // Setting RunAsUser: 5995 should fail. - SecurityContext: &corev1.SecurityContext{ - RunAsUser: pointerToInt64(envoyUserAndGroupID), + }, + handler: Handler{}, + expErr: true, + expErrMessage: fmt.Errorf("container app has runAsUser set to the same uid %q as envoy which is not allowed", envoyUserAndGroupID), + }, + { + name: "doesn't fail with envoy image", + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "web", + // Setting RunAsUser: 1 should succeed. + SecurityContext: &corev1.SecurityContext{ + RunAsUser: pointerToInt64(1), + }, + }, + { + Name: "sidecar", + // Setting RunAsUser: 5995 should succeed if the image matches h.ImageEnvoy. + SecurityContext: &corev1.SecurityContext{ + RunAsUser: pointerToInt64(envoyUserAndGroupID), + }, + Image: "envoy", + }, }, }, }, + handler: Handler{ + ImageEnvoy: "envoy", + }, + expErr: false, }, } - _, err := h.envoySidecar(testNS, pod) - require.Error(err, fmt.Sprintf("container %q has runAsUser set to the same uid %q as envoy which is not allowed", pod.Spec.Containers[1].Name, envoyUserAndGroupID)) + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.handler.envoySidecar(testNS, tc.pod, multiPortInfo{}) + if tc.expErr { + require.Error(t, err, tc.expErrMessage) + } else { + require.NoError(t, err) + } + }) + } } // Test that we can pass extra args to envoy via the extraEnvoyArgs flag @@ -247,7 +347,7 @@ func TestHandlerEnvoySidecar_EnvoyExtraArgs(t *testing.T) { EnvoyExtraArgs: tc.envoyExtraArgs, } - c, err := h.envoySidecar(testNS, *tc.pod) + c, err := h.envoySidecar(testNS, *tc.pod, multiPortInfo{}) require.NoError(t, err) require.Equal(t, tc.expectedContainerCommand, c.Command) }) @@ -421,7 +521,7 @@ func TestHandlerEnvoySidecar_Resources(t *testing.T) { }, }, } - container, err := c.handler.envoySidecar(testNS, pod) + container, err := c.handler.envoySidecar(testNS, pod, multiPortInfo{}) if c.expErr != "" { require.NotNil(err) require.Contains(err.Error(), c.expErr) diff --git a/control-plane/connect-inject/handler.go b/control-plane/connect-inject/handler.go index 4e4587d2f4..00f3918f5e 100644 --- a/control-plane/connect-inject/handler.go +++ b/control-plane/connect-inject/handler.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" "net/http" + "path/filepath" "strconv" + "strings" mapset "github.com/deckarep/golang-set" "github.com/go-logr/logr" @@ -61,6 +63,11 @@ type Handler struct { // If not set, will use HTTP. ConsulCACert string + // ConsulPartition is the name of the Admin Partition that the controller + // is deployed in. It is an enterprise feature requiring Consul Enterprise 1.11+. + // Its value is an empty string if partitions aren't enabled. + ConsulPartition string + // EnableNamespaces indicates that a user is running Consul Enterprise // with version 1.7+ which is namespace aware. It enables Consul namespaces, // with injection into either a single Consul namespace or mirrored from @@ -118,7 +125,7 @@ type Handler struct { // Resource settings for Consul sidecar. All of these fields // will be populated by the defaults provided in the initial flags. - ConsulSidecarResources corev1.ResourceRequirements + DefaultConsulSidecarResources corev1.ResourceRequirements // EnableTransparentProxy enables transparent proxy mode. // This means that the injected init container will apply traffic redirection rules @@ -129,6 +136,14 @@ type Handler struct { // to point them to the Envoy proxy. TProxyOverwriteProbes bool + // EnableConsulDNS enables traffic redirection so that DNS requests are directed to Consul + // from mesh services. + EnableConsulDNS bool + + // ResourcePrefix is the prefix used for the installation which is used to determine the Service + // name of the Consul DNS service. + ResourcePrefix string + // EnableOpenShift indicates that when tproxy is enabled, the security context for the Envoy and init // containers should not be added because OpenShift sets a random user for those and will not allow // those containers to be created otherwise. @@ -142,6 +157,10 @@ type Handler struct { decoder *admission.Decoder } +type multiPortInfo struct { + serviceIndex int + serviceName string +} // Handle is the admission.Handler implementation that actually handles the // webhook request for admission control. This should be registered or @@ -184,7 +203,7 @@ func (h *Handler) Handle(ctx context.Context, req admission.Request) admission.R return admission.Allowed(fmt.Sprintf("%s %s does not require injection", pod.Kind, pod.Name)) } - h.Log.Info("received pod", "name", pod.Name, "ns", pod.Namespace) + h.Log.Info("received pod", "name", req.Name, "ns", req.Namespace) // Add our volume that will be shared by the init container and // the sidecar for passing data in the pod. @@ -215,21 +234,91 @@ func (h *Handler) Handle(ctx context.Context, req admission.Request) admission.R return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error getting namespace metadata for container: %s", err)) } - // Add the init container that registers the service and sets up the Envoy configuration. - initContainer, err := h.containerInit(*ns, pod) - if err != nil { - h.Log.Error(err, "error configuring injection init container", "request name", req.Name) - return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection init container: %s", err)) - } - pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer) + // Get service names from the annotation. If theres 0-1 service names, it's a single port pod, otherwise it's multi + // port. + annotatedSvcNames := h.annotatedServiceNames(pod) + multiPort := len(annotatedSvcNames) > 1 - // Add the Envoy sidecar. - envoySidecar, err := h.envoySidecar(*ns, pod) - if err != nil { - h.Log.Error(err, "error configuring injection sidecar container", "request name", req.Name) - return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection sidecar container: %s", err)) + // For single port pods, add the single init container and envoy sidecar. + if !multiPort { + // Add the init container that registers the service and sets up the Envoy configuration. + initContainer, err := h.containerInit(*ns, pod, multiPortInfo{}) + if err != nil { + h.Log.Error(err, "error configuring injection init container", "request name", req.Name) + return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection init container: %s", err)) + } + pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer) + + // Add the Envoy sidecar. + envoySidecar, err := h.envoySidecar(*ns, pod, multiPortInfo{}) + if err != nil { + h.Log.Error(err, "error configuring injection sidecar container", "request name", req.Name) + return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection sidecar container: %s", err)) + } + pod.Spec.Containers = append(pod.Spec.Containers, envoySidecar) + } else { + // For multi port pods, check for unsupported cases, mount all relevant service account tokens, and mount an init + // container and envoy sidecar per port. Tproxy, metrics, and metrics merging are not supported for multi port pods. + // In a single port pod, the service account specified in the pod is sufficient for mounting the service account + // token to the pod. In a multi port pod, where multiple services are registered with Consul, we also require a + // service account per service. So, this will look for service accounts whose name matches the service and mount + // those tokens if not already specified via the pod's serviceAccountName. + + h.Log.Info("processing multiport pod") + err := h.checkUnsupportedMultiPortCases(*ns, pod) + if err != nil { + h.Log.Error(err, "checking unsupported cases for multi port pods") + return admission.Errored(http.StatusInternalServerError, err) + } + for i, svc := range annotatedSvcNames { + h.Log.Info(fmt.Sprintf("service: %s", svc)) + if h.AuthMethod != "" { + if svc != "" && pod.Spec.ServiceAccountName != svc { + sa, err := h.Clientset.CoreV1().ServiceAccounts(req.Namespace).Get(ctx, svc, metav1.GetOptions{}) + if err != nil { + h.Log.Error(err, "couldn't get service accounts") + return admission.Errored(http.StatusInternalServerError, err) + } + if len(sa.Secrets) == 0 { + h.Log.Info(fmt.Sprintf("service account %s has zero secrets exp at least 1", svc)) + return admission.Errored(http.StatusInternalServerError, fmt.Errorf("service account %s has zero secrets, expected at least one", svc)) + } + saSecret := sa.Secrets[0].Name + h.Log.Info("found service account, mounting service account secret to Pod", "serviceAccountName", sa.Name) + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: fmt.Sprintf("%s-service-account", svc), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: saSecret, + }, + }, + }) + } + } + + // This will get passed to the init and sidecar containers so they are configured correctly. + mpi := multiPortInfo{ + serviceIndex: i, + serviceName: svc, + } + + // Add the init container that registers the service and sets up the Envoy configuration. + initContainer, err := h.containerInit(*ns, pod, mpi) + if err != nil { + h.Log.Error(err, "error configuring injection init container", "request name", req.Name) + return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection init container: %s", err)) + } + pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer) + + // Add the Envoy sidecar. + envoySidecar, err := h.envoySidecar(*ns, pod, mpi) + if err != nil { + h.Log.Error(err, "error configuring injection sidecar container", "request name", req.Name) + return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection sidecar container: %s", err)) + } + pod.Spec.Containers = append(pod.Spec.Containers, envoySidecar) + } } - pod.Spec.Containers = append(pod.Spec.Containers, envoySidecar) // Now that the consul-sidecar no longer needs to re-register services periodically // (that functionality lives in the endpoints-controller), @@ -463,6 +552,7 @@ func (h *Handler) validatePod(pod corev1.Pod) error { } func portValue(pod corev1.Pod, value string) (int32, error) { + value = strings.Split(value, ",")[0] // First search for the named port. for _, c := range pod.Spec.Containers { for _, p := range c.Ports { @@ -477,7 +567,23 @@ func portValue(pod corev1.Pod, value string) (int32, error) { return int32(raw), err } -func findServiceAccountVolumeMount(pod corev1.Pod) (corev1.VolumeMount, error) { +func findServiceAccountVolumeMount(pod corev1.Pod, multiPort bool, multiPortSvcName string) (corev1.VolumeMount, string, error) { + // In the case of a multiPort pod, there may be another service account + // token mounted as a different volume. Its name must be -serviceaccount. + // If not we'll fall back to the service account for the pod. + if multiPort { + for _, v := range pod.Spec.Volumes { + if v.Name == fmt.Sprintf("%s-service-account", multiPortSvcName) { + mountPath := fmt.Sprintf("/consul/serviceaccount-%s", multiPortSvcName) + return corev1.VolumeMount{ + Name: v.Name, + ReadOnly: true, + MountPath: mountPath, + }, filepath.Join(mountPath, "token"), nil + } + } + } + // Find the volume mount that is mounted at the known // service account token location var volumeMount corev1.VolumeMount @@ -492,10 +598,43 @@ func findServiceAccountVolumeMount(pod corev1.Pod) (corev1.VolumeMount, error) { // Return an error if volumeMount is still empty if (corev1.VolumeMount{}) == volumeMount { - return volumeMount, errors.New("unable to find service account token volumeMount") + return volumeMount, "", errors.New("unable to find service account token volumeMount") } - return volumeMount, nil + return volumeMount, "/var/run/secrets/kubernetes.io/serviceaccount/token", nil +} + +func (h *Handler) annotatedServiceNames(pod corev1.Pod) []string { + var annotatedSvcNames []string + if anno, ok := pod.Annotations[annotationService]; ok { + annotatedSvcNames = strings.Split(anno, ",") + } + return annotatedSvcNames +} + +func (h *Handler) checkUnsupportedMultiPortCases(ns corev1.Namespace, pod corev1.Pod) error { + tproxyEnabled, err := transparentProxyEnabled(ns, pod, h.EnableTransparentProxy) + if err != nil { + return fmt.Errorf("couldn't check if tproxy is enabled: %s", err) + } + metricsEnabled, err := h.MetricsConfig.enableMetrics(pod) + if err != nil { + return fmt.Errorf("couldn't check if metrics is enabled: %s", err) + } + metricsMergingEnabled, err := h.MetricsConfig.enableMetricsMerging(pod) + if err != nil { + return fmt.Errorf("couldn't check if metrics merging is enabled: %s", err) + } + if tproxyEnabled { + return fmt.Errorf("multi port services are not compatible with transparent proxy") + } + if metricsEnabled { + return fmt.Errorf("multi port services are not compatible with metrics") + } + if metricsMergingEnabled { + return fmt.Errorf("multi port services are not compatible with metrics merging") + } + return nil } func (h *Handler) InjectDecoder(d *admission.Decoder) error { diff --git a/control-plane/connect-inject/handler_ent_test.go b/control-plane/connect-inject/handler_ent_test.go index f99cde6f0e..e35fd7aaad 100644 --- a/control-plane/connect-inject/handler_ent_test.go +++ b/control-plane/connect-inject/handler_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package connectinject diff --git a/control-plane/connect-inject/handler_test.go b/control-plane/connect-inject/handler_test.go index b4cbf2edb2..9eaf5d89d9 100644 --- a/control-plane/connect-inject/handler_test.go +++ b/control-plane/connect-inject/handler_test.go @@ -646,6 +646,60 @@ func TestHandlerHandle(t *testing.T) { }, }, }, + { + "multi port pod", + Handler{ + Log: logrtest.TestLogger{T: t}, + AllowK8sNamespacesSet: mapset.NewSetWith("*"), + DenyK8sNamespacesSet: mapset.NewSet(), + decoder: decoder, + Clientset: defaultTestClientWithNamespace(), + }, + admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Namespace: namespaces.DefaultNamespace, + Object: encodeRaw(t, &corev1.Pod{ + Spec: basicSpec, + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationService: "web, web-admin", + }, + }, + }), + }, + }, + "", + []jsonpatch.Operation{ + { + Operation: "add", + Path: "/spec/volumes", + }, + { + Operation: "add", + Path: "/spec/initContainers", + }, + { + Operation: "add", + Path: "/spec/containers/1", + }, + { + Operation: "add", + Path: "/spec/containers/2", + }, + { + Operation: "add", + Path: "/metadata/annotations/" + escapeJSONPointer(keyInjectStatus), + }, + { + Operation: "add", + Path: "/metadata/annotations/" + escapeJSONPointer(annotationOriginalPod), + }, + { + Operation: "add", + Path: "/metadata/labels", + }, + }, + }, } for _, tt := range cases { @@ -930,7 +984,7 @@ func TestHandlerPrometheusAnnotations(t *testing.T) { } } -// Test portValue function +// Test portValue function. func TestHandlerPortValue(t *testing.T) { cases := []struct { Name string @@ -1017,7 +1071,7 @@ func TestHandlerPortValue(t *testing.T) { } } -// Test consulNamespace function +// Test consulNamespace function. func TestConsulNamespace(t *testing.T) { cases := []struct { Name string @@ -1733,6 +1787,42 @@ func TestOverwriteProbes(t *testing.T) { } } +func TestHandler_checkUnsupportedMultiPortCases(t *testing.T) { + cases := []struct { + name string + annotations map[string]string + expErr string + }{ + { + name: "tproxy", + annotations: map[string]string{keyTransparentProxy: "true"}, + expErr: "multi port services are not compatible with transparent proxy", + }, + { + name: "metrics", + annotations: map[string]string{annotationEnableMetrics: "true"}, + expErr: "multi port services are not compatible with metrics", + }, + { + name: "metrics merging", + annotations: map[string]string{annotationEnableMetricsMerging: "true"}, + expErr: "multi port services are not compatible with metrics merging", + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + h := Handler{} + pod := minimal() + pod.Annotations = tt.annotations + err := h.checkUnsupportedMultiPortCases(corev1.Namespace{}, *pod) + require.Error(t, err) + require.Equal(t, tt.expErr, err.Error()) + }) + + } + +} + // encodeRaw is a helper to encode some data into a RawExtension. func encodeRaw(t *testing.T, input interface{}) runtime.RawExtension { data, err := json.Marshal(input) diff --git a/control-plane/controller/configentry_controller.go b/control-plane/controller/configentry_controller.go index 995c8138ed..d45340b151 100644 --- a/control-plane/controller/configentry_controller.go +++ b/control-plane/controller/configentry_controller.go @@ -11,12 +11,16 @@ import ( "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/hashicorp/consul-k8s/control-plane/namespaces" capi "github.com/hashicorp/consul/api" + "golang.org/x/time/rate" corev1 "k8s.io/api/core/v1" k8serr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( @@ -246,6 +250,37 @@ func (r *ConfigEntryController) ReconcileEntry(ctx context.Context, crdCtrl Cont return ctrl.Result{}, nil } +// setupWithManager sets up the controller manager for the given resource +// with our default options. +func setupWithManager(mgr ctrl.Manager, resource client.Object, reconciler reconcile.Reconciler) error { + options := controller.Options{ + // Taken from https://github.com/kubernetes/client-go/blob/master/util/workqueue/default_rate_limiters.go#L39 + // and modified from a starting backoff of 5ms and max of 1000s to a + // starting backoff of 200ms and a max of 5s to better fit our most + // common error cases and performance characteristics. + // + // One common error case is that a config entry is applied that requires + // a protocol like http or grpc. Often the user will apply a new config + // entry to set the protocol in a minute or two. During this time, the + // default backoff could then be set up to 5m or more which means the + // original config entry takes a long time to re-sync. + // + // In terms of performance, Consul servers can handle tens of thousands + // of writes per second, so retrying at max every 5s isn't an issue and + // provides a better UX. + RateLimiter: workqueue.NewMaxOfRateLimiter( + workqueue.NewItemExponentialFailureRateLimiter(200*time.Millisecond, 5*time.Second), + // 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item) + &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)}, + ), + } + + return ctrl.NewControllerManagedBy(mgr). + For(resource). + WithOptions(options). + Complete(reconciler) +} + func (r *ConfigEntryController) consulNamespace(configEntry capi.ConfigEntry, namespace string, globalResource bool) string { // ServiceIntentions have the appropriate Consul Namespace set on them as the value // is defaulted by the webhook. These are then set on the ServiceIntentions config entry diff --git a/control-plane/controller/configentry_controller_ent_test.go b/control-plane/controller/configentry_controller_ent_test.go index 8291276d02..11bdaa33bb 100644 --- a/control-plane/controller/configentry_controller_ent_test.go +++ b/control-plane/controller/configentry_controller_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package controller_test @@ -120,7 +120,7 @@ func TestConfigEntryController_createsConfigEntry_consulNamespaces(tt *testing.T ConsulKind: capi.ProxyDefaults, KubeResource: &v1alpha1.ProxyDefaults{ ObjectMeta: metav1.ObjectMeta{ - Name: "global", + Name: common.Global, Namespace: c.SourceKubeNS, }, Spec: v1alpha1.ProxyDefaultsSpec{ diff --git a/control-plane/controller/configentry_controller_test.go b/control-plane/controller/configentry_controller_test.go index 9ed1c324ac..74787beb12 100644 --- a/control-plane/controller/configentry_controller_test.go +++ b/control-plane/controller/configentry_controller_test.go @@ -1468,7 +1468,7 @@ func TestConfigEntryControllers_setsSyncedToTrue(t *testing.T) { } // Test that if the config entry exists in Consul but is not managed by the -// controller, creating/updating the resource fails +// controller, creating/updating the resource fails. func TestConfigEntryControllers_doesNotCreateUnownedConfigEntry(t *testing.T) { t.Parallel() kubeNS := "default" @@ -1566,7 +1566,7 @@ func TestConfigEntryControllers_doesNotCreateUnownedConfigEntry(t *testing.T) { } // Test that if the config entry exists in Consul but is not managed by the -// controller, deleting the resource does not delete the Consul config entry +// controller, deleting the resource does not delete the Consul config entry. func TestConfigEntryControllers_doesNotDeleteUnownedConfig(t *testing.T) { t.Parallel() kubeNS := "default" diff --git a/control-plane/controller/exportedservices_controller.go b/control-plane/controller/exportedservices_controller.go new file mode 100644 index 0000000000..84e767d6dc --- /dev/null +++ b/control-plane/controller/exportedservices_controller.go @@ -0,0 +1,40 @@ +package controller + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + consulv1alpha1 "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" +) + +// ExportedServicesController reconciles a ExportedServices object. +type ExportedServicesController struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + ConfigEntryController *ConfigEntryController +} + +// +kubebuilder:rbac:groups=consul.hashicorp.com,resources=exportedservices,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=consul.hashicorp.com,resources=exportedservices/status,verbs=get;update;patch + +func (r *ExportedServicesController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return r.ConfigEntryController.ReconcileEntry(ctx, r, req, &consulv1alpha1.ExportedServices{}) +} + +func (r *ExportedServicesController) Logger(name types.NamespacedName) logr.Logger { + return r.Log.WithValues("request", name) +} + +func (r *ExportedServicesController) UpdateStatus(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return r.Status().Update(ctx, obj, opts...) +} + +func (r *ExportedServicesController) SetupWithManager(mgr ctrl.Manager) error { + return setupWithManager(mgr, &consulv1alpha1.ExportedServices{}, r) +} diff --git a/control-plane/controller/exportedservices_controller_ent_test.go b/control-plane/controller/exportedservices_controller_ent_test.go new file mode 100644 index 0000000000..ec8f771586 --- /dev/null +++ b/control-plane/controller/exportedservices_controller_ent_test.go @@ -0,0 +1,420 @@ +//go:build enterprise + +package controller_test + +import ( + "context" + "fmt" + "testing" + "time" + + logrtest "github.com/go-logr/logr/testing" + "github.com/hashicorp/consul-k8s/control-plane/api/common" + "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" + "github.com/hashicorp/consul-k8s/control-plane/controller" + capi "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +// This tests explicitly tests ExportedServicesController instead of using the existing +// pattern of adding tests for the controller to configentry_controller test. That is because +// unlike the other CRDs, ExportedServices are only supported in Consul Enterprise. But the +// test pattern of the enterprise tests already covers a config-entry similar to partition-exports +// ie a "global" configentry. Hence a separate file has been created to test this controller. + +func TestExportedServicesController_createsExportedServices(tt *testing.T) { + tt.Parallel() + + cases := map[string]struct { + Mirror bool + MirrorPrefix string + SourceKubeNS string + DestConsulNS string + }{ + "SourceKubeNS=default, DestConsulNS=default": { + SourceKubeNS: "default", + DestConsulNS: "default", + }, + "SourceKubeNS=kube, DestConsulNS=default": { + SourceKubeNS: "kube", + DestConsulNS: "default", + }, + "SourceKubeNS=default, DestConsulNS=other": { + SourceKubeNS: "default", + DestConsulNS: "other", + }, + "SourceKubeNS=kube, DestConsulNS=other": { + SourceKubeNS: "kube", + DestConsulNS: "other", + }, + "SourceKubeNS=default, Mirror=true": { + SourceKubeNS: "default", + Mirror: true, + }, + "SourceKubeNS=kube, Mirror=true": { + SourceKubeNS: "kube", + Mirror: true, + }, + "SourceKubeNS=default, Mirror=true, Prefix=prefix": { + SourceKubeNS: "default", + Mirror: true, + MirrorPrefix: "prefix-", + }, + } + + for name, c := range cases { + tt.Run(name, func(t *testing.T) { + req := require.New(t) + s := runtime.NewScheme() + exportedServices := &v1alpha1.ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: c.SourceKubeNS, + }, + Spec: v1alpha1.ExportedServicesSpec{ + Services: []v1alpha1.ExportedService{ + { + Name: "frontend", + Namespace: "front", + Consumers: []v1alpha1.ServiceConsumer{ + {Partition: "foo"}, + {Partition: "bar"}, + }, + }, + }, + }, + } + s.AddKnownTypes(v1alpha1.GroupVersion, exportedServices) + ctx := context.Background() + + consul, err := testutil.NewTestServerConfigT(t, nil) + req.NoError(err) + defer consul.Stop() + consul.WaitForServiceIntentions(t) + consulClient, err := capi.NewClient(&capi.Config{ + Address: consul.HTTPAddr, + }) + req.NoError(err) + + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(exportedServices).Build() + + controller := &controller.ExportedServicesController{ + Client: fakeClient, + Log: logrtest.TestLogger{T: t}, + Scheme: s, + ConfigEntryController: &controller.ConfigEntryController{ + ConsulClient: consulClient, + EnableConsulNamespaces: true, + EnableNSMirroring: c.Mirror, + NSMirroringPrefix: c.MirrorPrefix, + ConsulDestinationNamespace: c.DestConsulNS, + }, + } + + resp, err := controller.Reconcile(ctx, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: c.SourceKubeNS, + Name: exportedServices.KubernetesName(), + }, + }) + req.NoError(err) + req.False(resp.Requeue) + + cfg, _, err := consulClient.ConfigEntries().Get(capi.ExportedServices, exportedServices.ConsulName(), &capi.QueryOptions{ + Namespace: common.DefaultConsulNamespace, + }) + req.NoError(err) + + configEntry, ok := cfg.(*capi.ExportedServicesConfigEntry) + req.True(ok) + req.Equal(configEntry.Services[0].Name, "frontend") + + // Check that the status is "synced". + err = fakeClient.Get(ctx, types.NamespacedName{ + Namespace: c.SourceKubeNS, + Name: exportedServices.KubernetesName(), + }, exportedServices) + req.NoError(err) + conditionSynced := exportedServices.SyncedConditionStatus() + req.Equal(conditionSynced, corev1.ConditionTrue) + }) + } +} + +func TestExportedServicesController_updatesExportedServices(tt *testing.T) { + tt.Parallel() + + cases := map[string]struct { + Mirror bool + MirrorPrefix string + SourceKubeNS string + DestConsulNS string + }{ + "SourceKubeNS=default, DestConsulNS=default": { + SourceKubeNS: "default", + DestConsulNS: "default", + }, + "SourceKubeNS=kube, DestConsulNS=default": { + SourceKubeNS: "kube", + DestConsulNS: "default", + }, + "SourceKubeNS=default, DestConsulNS=other": { + SourceKubeNS: "default", + DestConsulNS: "other", + }, + "SourceKubeNS=kube, DestConsulNS=other": { + SourceKubeNS: "kube", + DestConsulNS: "other", + }, + "SourceKubeNS=default, Mirror=true": { + SourceKubeNS: "default", + Mirror: true, + }, + "SourceKubeNS=kube, Mirror=true": { + SourceKubeNS: "kube", + Mirror: true, + }, + "SourceKubeNS=default, Mirror=true, Prefix=prefix": { + SourceKubeNS: "default", + Mirror: true, + MirrorPrefix: "prefix-", + }, + } + + for name, c := range cases { + tt.Run(name, func(t *testing.T) { + req := require.New(t) + s := runtime.NewScheme() + exportedServices := &v1alpha1.ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: c.SourceKubeNS, + Finalizers: []string{controller.FinalizerName}, + }, + Spec: v1alpha1.ExportedServicesSpec{ + Services: []v1alpha1.ExportedService{ + { + Name: "frontend", + Namespace: "front", + Consumers: []v1alpha1.ServiceConsumer{ + {Partition: "foo"}, + {Partition: "bar"}, + }, + }, + }, + }, + } + s.AddKnownTypes(v1alpha1.GroupVersion, exportedServices) + ctx := context.Background() + + consul, err := testutil.NewTestServerConfigT(t, nil) + req.NoError(err) + defer consul.Stop() + consul.WaitForServiceIntentions(t) + consulClient, err := capi.NewClient(&capi.Config{ + Address: consul.HTTPAddr, + }) + req.NoError(err) + + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(exportedServices).Build() + + controller := &controller.ExportedServicesController{ + Client: fakeClient, + Log: logrtest.TestLogger{T: t}, + Scheme: s, + ConfigEntryController: &controller.ConfigEntryController{ + ConsulClient: consulClient, + EnableConsulNamespaces: true, + EnableNSMirroring: c.Mirror, + NSMirroringPrefix: c.MirrorPrefix, + ConsulDestinationNamespace: c.DestConsulNS, + }, + } + + // We haven't run reconcile yet so ensure it's created in Consul. + { + _, _, err := consulClient.ConfigEntries().Set(&capi.ExportedServicesConfigEntry{ + Name: "default", + Services: []capi.ExportedService{ + { + Name: "frontend", + Namespace: "front", + Consumers: []capi.ServiceConsumer{ + {Partition: "foo"}, + {Partition: "bar"}, + }, + }, + }, + }, &capi.WriteOptions{Namespace: common.DefaultConsulNamespace}) + req.NoError(err) + } + + // Now update it. + { + // First get it so we have the latest revision number. + err = fakeClient.Get(ctx, types.NamespacedName{ + Namespace: c.SourceKubeNS, + Name: exportedServices.KubernetesName(), + }, exportedServices) + req.NoError(err) + + // Update the resource. + exportedServices.Spec.Services[0].Name = "backend" + err := fakeClient.Update(ctx, exportedServices) + req.NoError(err) + + resp, err := controller.Reconcile(ctx, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: c.SourceKubeNS, + Name: exportedServices.KubernetesName(), + }, + }) + req.NoError(err) + req.False(resp.Requeue) + + cfg, _, err := consulClient.ConfigEntries().Get(capi.ExportedServices, exportedServices.ConsulName(), &capi.QueryOptions{ + Namespace: common.DefaultConsulNamespace, + }) + req.NoError(err) + entry := cfg.(*capi.ExportedServicesConfigEntry) + req.Equal("backend", entry.Services[0].Name) + } + }) + } +} + +func TestExportedServicesController_deletesExportedServices(tt *testing.T) { + tt.Parallel() + + cases := map[string]struct { + Mirror bool + MirrorPrefix string + SourceKubeNS string + DestConsulNS string + }{ + "SourceKubeNS=default, DestConsulNS=default": { + SourceKubeNS: "default", + DestConsulNS: "default", + }, + "SourceKubeNS=kube, DestConsulNS=default": { + SourceKubeNS: "kube", + DestConsulNS: "default", + }, + "SourceKubeNS=default, DestConsulNS=other": { + SourceKubeNS: "default", + DestConsulNS: "other", + }, + "SourceKubeNS=kube, DestConsulNS=other": { + SourceKubeNS: "kube", + DestConsulNS: "other", + }, + "SourceKubeNS=default, Mirror=true": { + SourceKubeNS: "default", + Mirror: true, + }, + "SourceKubeNS=kube, Mirror=true": { + SourceKubeNS: "kube", + Mirror: true, + }, + "SourceKubeNS=default, Mirror=true, Prefix=prefix": { + SourceKubeNS: "default", + Mirror: true, + MirrorPrefix: "prefix-", + }, + } + + for name, c := range cases { + tt.Run(name, func(t *testing.T) { + req := require.New(t) + s := runtime.NewScheme() + exportedServices := &v1alpha1.ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: c.SourceKubeNS, + Finalizers: []string{controller.FinalizerName}, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }, + Spec: v1alpha1.ExportedServicesSpec{ + Services: []v1alpha1.ExportedService{ + { + Name: "frontend", + Namespace: "front", + Consumers: []v1alpha1.ServiceConsumer{ + {Partition: "foo"}, + {Partition: "bar"}, + }, + }, + }, + }, + } + s.AddKnownTypes(v1alpha1.GroupVersion, exportedServices) + + consul, err := testutil.NewTestServerConfigT(t, nil) + req.NoError(err) + defer consul.Stop() + consul.WaitForServiceIntentions(t) + consulClient, err := capi.NewClient(&capi.Config{ + Address: consul.HTTPAddr, + }) + req.NoError(err) + + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(exportedServices).Build() + + controller := &controller.ExportedServicesController{ + Client: fakeClient, + Log: logrtest.TestLogger{T: t}, + Scheme: s, + ConfigEntryController: &controller.ConfigEntryController{ + ConsulClient: consulClient, + EnableConsulNamespaces: true, + EnableNSMirroring: c.Mirror, + NSMirroringPrefix: c.MirrorPrefix, + ConsulDestinationNamespace: c.DestConsulNS, + }, + } + + // We haven't run reconcile yet so ensure it's created in Consul. + { + _, _, err := consulClient.ConfigEntries().Set(&capi.ExportedServicesConfigEntry{ + Name: "default", + Services: []capi.ExportedService{ + { + Name: "frontend", + Namespace: "front", + Consumers: []capi.ServiceConsumer{ + {Partition: "foo"}, + {Partition: "bar"}, + }, + }, + }, + }, + &capi.WriteOptions{Namespace: common.DefaultConsulNamespace}) + req.NoError(err) + } + + // Now run reconcile. It's marked for deletion so this should delete it. + { + resp, err := controller.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: c.SourceKubeNS, + Name: exportedServices.KubernetesName(), + }, + }) + req.NoError(err) + req.False(resp.Requeue) + + _, _, err = consulClient.ConfigEntries().Get(capi.ExportedServices, exportedServices.ConsulName(), &capi.QueryOptions{ + Namespace: common.DefaultConsulNamespace, + }) + req.EqualError(err, fmt.Sprintf(`Unexpected response code: 404 (Config entry not found for "%s" / "%s")`, capi.ExportedServices, exportedServices.ConsulName())) + } + }) + } +} diff --git a/control-plane/controller/ingressgateway_controller.go b/control-plane/controller/ingressgateway_controller.go index aedb2ff697..7e656b3d29 100644 --- a/control-plane/controller/ingressgateway_controller.go +++ b/control-plane/controller/ingressgateway_controller.go @@ -36,7 +36,5 @@ func (r *IngressGatewayController) UpdateStatus(ctx context.Context, obj client. } func (r *IngressGatewayController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.IngressGateway{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.IngressGateway{}, r) } diff --git a/control-plane/controller/mesh_controller.go b/control-plane/controller/mesh_controller.go index 4b4e1257bc..e15f49fca0 100644 --- a/control-plane/controller/mesh_controller.go +++ b/control-plane/controller/mesh_controller.go @@ -12,7 +12,7 @@ import ( consulv1alpha1 "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" ) -// MeshController reconciles a Mesh object +// MeshController reconciles a Mesh object. type MeshController struct { client.Client Log logr.Logger @@ -36,7 +36,5 @@ func (r *MeshController) UpdateStatus(ctx context.Context, obj client.Object, op } func (r *MeshController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.Mesh{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.Mesh{}, r) } diff --git a/control-plane/controller/proxydefaults_controller.go b/control-plane/controller/proxydefaults_controller.go index 0b4a11c001..a63e121522 100644 --- a/control-plane/controller/proxydefaults_controller.go +++ b/control-plane/controller/proxydefaults_controller.go @@ -12,7 +12,7 @@ import ( consulv1alpha1 "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" ) -// ProxyDefaultsController reconciles a ProxyDefaults object +// ProxyDefaultsController reconciles a ProxyDefaults object. type ProxyDefaultsController struct { client.Client Log logr.Logger @@ -36,7 +36,5 @@ func (r *ProxyDefaultsController) UpdateStatus(ctx context.Context, obj client.O } func (r *ProxyDefaultsController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.ProxyDefaults{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.ProxyDefaults{}, r) } diff --git a/control-plane/controller/servicedefaults_controller.go b/control-plane/controller/servicedefaults_controller.go index f71afed09c..7dd73914e4 100644 --- a/control-plane/controller/servicedefaults_controller.go +++ b/control-plane/controller/servicedefaults_controller.go @@ -36,7 +36,5 @@ func (r *ServiceDefaultsController) UpdateStatus(ctx context.Context, obj client } func (r *ServiceDefaultsController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.ServiceDefaults{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.ServiceDefaults{}, r) } diff --git a/control-plane/controller/serviceintentions_controller.go b/control-plane/controller/serviceintentions_controller.go index b6db47dfd0..3b70447517 100644 --- a/control-plane/controller/serviceintentions_controller.go +++ b/control-plane/controller/serviceintentions_controller.go @@ -12,7 +12,7 @@ import ( consulv1alpha1 "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" ) -// ServiceIntentionsController reconciles a ServiceIntentions object +// ServiceIntentionsController reconciles a ServiceIntentions object. type ServiceIntentionsController struct { client.Client Log logr.Logger @@ -36,7 +36,5 @@ func (r *ServiceIntentionsController) UpdateStatus(ctx context.Context, obj clie } func (r *ServiceIntentionsController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.ServiceIntentions{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.ServiceIntentions{}, r) } diff --git a/control-plane/controller/serviceresolver_controller.go b/control-plane/controller/serviceresolver_controller.go index 96f068441c..3e01e680ea 100644 --- a/control-plane/controller/serviceresolver_controller.go +++ b/control-plane/controller/serviceresolver_controller.go @@ -36,7 +36,5 @@ func (r *ServiceResolverController) UpdateStatus(ctx context.Context, obj client } func (r *ServiceResolverController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.ServiceResolver{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.ServiceResolver{}, r) } diff --git a/control-plane/controller/servicerouter_controller.go b/control-plane/controller/servicerouter_controller.go index e32c3d175a..7db983dec2 100644 --- a/control-plane/controller/servicerouter_controller.go +++ b/control-plane/controller/servicerouter_controller.go @@ -36,7 +36,5 @@ func (r *ServiceRouterController) UpdateStatus(ctx context.Context, obj client.O } func (r *ServiceRouterController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.ServiceRouter{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.ServiceRouter{}, r) } diff --git a/control-plane/controller/servicesplitter_controller.go b/control-plane/controller/servicesplitter_controller.go index 537fbef969..9d07845dbb 100644 --- a/control-plane/controller/servicesplitter_controller.go +++ b/control-plane/controller/servicesplitter_controller.go @@ -12,7 +12,7 @@ import ( consulv1alpha1 "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" ) -// ServiceSplitterReconciler reconciles a ServiceSplitter object +// ServiceSplitterReconciler reconciles a ServiceSplitter object. type ServiceSplitterController struct { client.Client Log logr.Logger @@ -36,7 +36,5 @@ func (r *ServiceSplitterController) UpdateStatus(ctx context.Context, obj client } func (r *ServiceSplitterController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.ServiceSplitter{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.ServiceSplitter{}, r) } diff --git a/control-plane/controller/terminatinggateway_controller.go b/control-plane/controller/terminatinggateway_controller.go index 43b4b3a0df..a8db2d851e 100644 --- a/control-plane/controller/terminatinggateway_controller.go +++ b/control-plane/controller/terminatinggateway_controller.go @@ -36,7 +36,5 @@ func (r *TerminatingGatewayController) UpdateStatus(ctx context.Context, obj cli } func (r *TerminatingGatewayController) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&consulv1alpha1.TerminatingGateway{}). - Complete(r) + return setupWithManager(mgr, &consulv1alpha1.TerminatingGateway{}, r) } diff --git a/control-plane/go.mod b/control-plane/go.mod index cff0ca7070..e8842f4643 100644 --- a/control-plane/go.mod +++ b/control-plane/go.mod @@ -1,43 +1,134 @@ module github.com/hashicorp/consul-k8s/control-plane require ( - github.com/armon/go-metrics v0.3.9 // indirect github.com/cenkalti/backoff v2.1.1+incompatible github.com/deckarep/golang-set v1.7.1 - github.com/digitalocean/godo v1.10.0 // indirect - github.com/fatih/color v1.12.0 // indirect github.com/go-logr/logr v0.4.0 github.com/google/go-cmp v0.5.6 - github.com/google/go-querystring v1.0.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hashicorp/consul/api v1.9.0 - github.com/hashicorp/consul/sdk v0.8.0 - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/consul/api v1.12.0 + github.com/hashicorp/consul/sdk v0.9.0 github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f github.com/hashicorp/go-hclog v0.16.1 - github.com/hashicorp/go-immutable-radix v1.3.0 // indirect - github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-multierror v1.1.0 - github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/serf v0.9.5 - github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect + github.com/hashicorp/serf v0.9.6 github.com/kr/text v0.2.0 - github.com/mattn/go-isatty v0.0.13 // indirect github.com/mitchellh/cli v1.1.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/go-testing-interface v1.14.0 // indirect github.com/mitchellh/mapstructure v1.4.1 github.com/stretchr/testify v1.7.0 - go.uber.org/zap v1.17.0 - golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect - golang.org/x/tools v0.1.2 // indirect + go.uber.org/zap v1.19.0 + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac gomodules.xyz/jsonpatch/v2 v2.2.0 - k8s.io/api v0.21.1 - k8s.io/apimachinery v0.21.1 - k8s.io/client-go v0.21.1 - k8s.io/klog/v2 v2.8.0 - sigs.k8s.io/controller-runtime v0.9.0 + k8s.io/api v0.22.2 + k8s.io/apimachinery v0.22.2 + k8s.io/client-go v0.22.2 + k8s.io/klog/v2 v2.9.0 + sigs.k8s.io/controller-runtime v0.10.2 +) + +require ( + cloud.google.com/go v0.54.0 // indirect + github.com/Azure/azure-sdk-for-go v44.0.0+incompatible // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.18 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.0 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/armon/go-metrics v0.3.9 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/aws/aws-sdk-go v1.25.41 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661 // indirect + github.com/digitalocean/godo v1.10.0 // indirect + github.com/dimchansky/utfbom v1.1.0 // indirect + github.com/evanphx/json-patch v4.11.0+incompatible // indirect + github.com/fatih/color v1.12.0 // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/go-logr/zapr v0.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-querystring v1.0.0 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/googleapis/gnostic v0.5.5 // indirect + github.com/gophercloud/gophercloud v0.1.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.0 // indirect + github.com/hashicorp/go-msgpack v0.5.5 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/hashicorp/mdns v1.0.4 // indirect + github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect + github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/linode/linodego v0.7.1 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.13 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/miekg/dns v1.1.41 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 // indirect + github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/prometheus/client_golang v1.11.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.26.0 // indirect + github.com/prometheus/procfs v0.6.0 // indirect + github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.2.0 // indirect + github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible // indirect + github.com/vmware/govmomi v0.18.0 // indirect + go.opencensus.io v0.22.3 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect + golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 // indirect + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/api v0.20.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect + google.golang.org/grpc v1.38.0 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/resty.v1 v1.12.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/apiextensions-apiserver v0.22.2 // indirect + k8s.io/component-base v0.22.2 // indirect + k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect + k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect ) -go 1.16 +replace github.com/hashicorp/consul/sdk v0.9.0 => github.com/hashicorp/consul/sdk v0.4.1-0.20220120214936-7568f3a102a8 + +go 1.17 diff --git a/control-plane/go.sum b/control-plane/go.sum index 9b7dcb4397..42ee98bb0f 100644 --- a/control-plane/go.sum +++ b/control-plane/go.sum @@ -25,17 +25,18 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v44.0.0+incompatible h1:e82Yv2HNpS0kuyeCrV29OPKvEiqfs2/uJHic3/3iKdg= github.com/Azure/azure-sdk-for-go v44.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.11.0/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE= -github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/azure/auth v0.5.0 h1:nSMjYIe24eBYasAIxt859TxyXef/IqoH+8/g4+LmcVs= github.com/Azure/go-autorest/autorest/azure/auth v0.5.0/go.mod h1:QRTvSZQpxqm8mSErhnbI+tANIBAKP7B+UIE2z4ypUO0= github.com/Azure/go-autorest/autorest/azure/cli v0.4.0 h1:Ml+UCrnlKD+cJmSzrZ/RDcDw86NjkRUpnFh7V5JUhzU= @@ -53,8 +54,9 @@ github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcP github.com/Azure/go-autorest/autorest/validation v0.3.0 h1:3I9AAI63HfcLtphd9g39ruUwRI+Ca+z/f36KHPFRUss= github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= @@ -75,6 +77,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -86,6 +89,9 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.25.41 h1:/hj7nZ0586wFqpwjNpzWiUTwtaMgxAZNZKHay80MdXw= github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -97,6 +103,8 @@ github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= @@ -107,8 +115,12 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -116,13 +128,11 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -143,27 +153,32 @@ github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 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= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -182,32 +197,31 @@ github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -228,17 +242,18 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 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/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= @@ -258,7 +273,6 @@ 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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.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= @@ -268,26 +282,26 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.9.0 h1:T6dKIWcaihG2c21YUi0BMAHbJanVXiYuz+mPgqxY3N4= -github.com/hashicorp/consul/api v1.9.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= +github.com/hashicorp/consul/sdk v0.4.1-0.20220120214936-7568f3a102a8 h1:1O/CANaJGcL6urr47PLoPZ0oQcGLUlGpYoRLYAYFSDs= +github.com/hashicorp/consul/sdk v0.4.1-0.20220120214936-7568f3a102a8/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -323,20 +337,20 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1 h1:XFSOubp8KWB+Jd2PDyaX5xUd5bhSP/+pTDZVDMzZJM8= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw= github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -354,12 +368,13 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS 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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joyent/triton-go v0.0.0-20180628001255-830d2b111e62/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f h1:ENpDacvnr8faw5ugQmEF1QYk+f/Y9lXFvuYmRxykago= github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f/go.mod h1:KDSfL7qe5ZfQqvlDMkVjCztbmcpp/c8M77vhQP8ZPvk= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -380,7 +395,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -392,28 +406,27 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0 h1:tEElEatulEHDeedTxwckzyYMA5c86fbmNIUL1hBIiTg= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= @@ -421,8 +434,6 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk 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-testing-interface v1.14.0 h1:/x0XQ6h+3U3nAyk1yx+bHPURrKa9sVVvYbuqZ7pIAtI= -github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 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= @@ -431,7 +442,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh 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/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -451,21 +462,21 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c h1:vwpFWvAO8DeIZfFeqASzZfsxuWPno9ncAebBEP0N3uE= github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -507,13 +518,13 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 h1:Wdi9nwnhFNAlseAOekn6B5G/+GMtks9UKbvRU/CMM/o= github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03/go.mod h1:gRAiPF5C5Nd0eyyRdqIu9qTiFSoZzpTq727b5B8fkkU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/zerolog v1.4.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -531,24 +542,24 @@ github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjM github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d h1:bVQRCxQvfjNUeRqaY/uT0tFuvuFY0ulgnczuR684Xic= github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d/go.mod h1:Cw4GTlQccdRGSEf6KiMju767x0NEHE0YIVPJSaXjlsw= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -570,12 +581,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible h1:8uRvJleFpqLsO77WaAh2UrasMOzd8MxXrNj20e7El+Q= github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vmware/govmomi v0.18.0 h1:f7QxSmP7meCtoAmiKZogvVbLInT+CZx6Px6K5rYsJZo= github.com/vmware/govmomi v0.18.0/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -584,15 +594,31 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= 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.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -602,8 +628,9 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 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= @@ -640,8 +667,9 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -650,7 +678,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -671,7 +698,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -684,11 +710,16 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -724,10 +755,8 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -749,21 +778,24 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ= -golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= @@ -774,15 +806,15 @@ 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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -797,7 +829,6 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -826,7 +857,6 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -872,19 +902,27 @@ google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -905,7 +943,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= @@ -921,6 +958,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD 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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -940,20 +978,20 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh 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.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= -k8s.io/api v0.21.1 h1:94bbZ5NTjdINJEdzOkpS4vdPhkb1VFpTYC9zh43f75c= -k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= -k8s.io/apiextensions-apiserver v0.21.1 h1:AA+cnsb6w7SZ1vD32Z+zdgfXdXY8X9uGX5bN6EoPEIo= -k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= +k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= +k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= +k8s.io/apiextensions-apiserver v0.22.2 h1:zK7qI8Ery7j2CaN23UCFaC1hj7dMiI87n01+nKuewd4= +k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA= k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= -k8s.io/apimachinery v0.21.1 h1:Q6XuHGlj2xc+hlMCvqyYfbv3H7SRGn2c8NycxJquDVs= -k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= -k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= +k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= +k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= -k8s.io/client-go v0.21.1 h1:bhblWYLZKUu+pm50plvQF8WpY6TXdRRtcS/K9WauOj4= -k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= -k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= -k8s.io/component-base v0.21.1 h1:iLpj2btXbR326s/xNQWmPNGu0gaYSjzn7IN/5i28nQw= -k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= +k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= +k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= +k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= +k8s.io/component-base v0.22.2 h1:vNIvE0AIrLhjX8drH0BgCNJcR4QZxMXcJzBsDplDx9M= +k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -963,26 +1001,25 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= -k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210527160623-6fdb442a123b h1:MSqsVQ3pZvPGTqCjptfimO2WjG7A9un2zcpiHkA6M/s= -k8s.io/utils v0.0.0-20210527160623-6fdb442a123b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/controller-runtime v0.9.0 h1:ZIZ/dtpboPSbZYY7uUz2OzrkaBTOThx2yekLtpGB+zY= -sigs.k8s.io/controller-runtime v0.9.0/go.mod h1:TgkfvrhhEw3PlI0BRL/5xM+89y3/yc0ZDfdbTl84si8= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/controller-runtime v0.10.2 h1:jW8qiY+yMnnPx6O9hu63tgcwaKzd1yLYui+mpvClOOc= +sigs.k8s.io/controller-runtime v0.10.2/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQkmzDXZXGwH8= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= 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/control-plane/helper/cert/source_gen.go b/control-plane/helper/cert/source_gen.go index 8226e9ce72..e9c79ed390 100644 --- a/control-plane/helper/cert/source_gen.go +++ b/control-plane/helper/cert/source_gen.go @@ -34,7 +34,7 @@ type GenSource struct { caSigner crypto.Signer } -// Certificate implements Source +// Certificate implements Source. func (s *GenSource) Certificate(ctx context.Context, last *Bundle) (Bundle, error) { s.mu.Lock() defer s.mu.Unlock() diff --git a/control-plane/helper/cert/source_gen_test.go b/control-plane/helper/cert/source_gen_test.go index b654f10d0f..8926e89174 100644 --- a/control-plane/helper/cert/source_gen_test.go +++ b/control-plane/helper/cert/source_gen_test.go @@ -20,7 +20,7 @@ func init() { hasOpenSSL = err == nil } -// Test that valid certificates are generated +// Test that valid certificates are generated. func TestGenSource_valid(t *testing.T) { t.Parallel() @@ -36,7 +36,7 @@ func TestGenSource_valid(t *testing.T) { testBundleVerify(t, &bundle) } -// Test that certs are regenerated near expiry +// Test that certs are regenerated near expiry. func TestGenSource_expiry(t *testing.T) { t.Parallel() diff --git a/control-plane/helper/cert/tls_util.go b/control-plane/helper/cert/tls_util.go index d56bcc9989..37e2f4ea97 100644 --- a/control-plane/helper/cert/tls_util.go +++ b/control-plane/helper/cert/tls_util.go @@ -17,6 +17,9 @@ import ( "time" ) +// NOTE: A lot of this code is taken from +// https://github.com/hashicorp/consul/blob/44c023a3020fdd139c5be330f318a3c12339f08e/agent/connect/parsing.go. + // GenerateCA generates a CA with the provided // common name valid for 10 years. It returns the private key as // a crypto.Signer and a PEM string and certificate @@ -162,6 +165,22 @@ func ParseSigner(pemValue string) (crypto.Signer, error) { switch block.Type { case "EC PRIVATE KEY": return x509.ParseECPrivateKey(block.Bytes) + + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(block.Bytes) + + case "PRIVATE KEY": + signer, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + pk, ok := signer.(crypto.Signer) + if !ok { + return nil, fmt.Errorf("private key is not a valid format") + } + + return pk, nil + default: return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type) } diff --git a/control-plane/helper/coalesce/coalesce_test.go b/control-plane/helper/coalesce/coalesce_test.go index 2cfe7bd65c..8489fed8b4 100644 --- a/control-plane/helper/coalesce/coalesce_test.go +++ b/control-plane/helper/coalesce/coalesce_test.go @@ -46,11 +46,11 @@ func TestCoalesce_max(t *testing.T) { testSummer(&total, deltaCh)) duration := time.Since(start) if total < 4 || total > 6 { - // 4 to 6 to account for CI weirdness + // 4 to 6 to account for CI weirdness. t.Fatalf("total should be 4 to 6: %d", total) } - // We should complete in the max period + // We should complete in the max period. if duration < 500*time.Millisecond { t.Fatalf("duration should be greater than max: %s", duration) } @@ -58,7 +58,7 @@ func TestCoalesce_max(t *testing.T) { // Test that if the cancel function is called, Coalesce exits. // We test this via having a function that just sleeps and then calling -// cancel on it. We expect that Coalesce exits +// cancel on it. We expect that Coalesce exits. func TestCoalesce_cancel(t *testing.T) { total := 0 deltaCh := make(chan int, 10) diff --git a/control-plane/helper/controller/controller.go b/control-plane/helper/controller/controller.go index 66a1d3b005..6edfb9d89c 100644 --- a/control-plane/helper/controller/controller.go +++ b/control-plane/helper/controller/controller.go @@ -134,7 +134,7 @@ func (c *Controller) Run(stopCh <-chan struct{}) { }, time.Second, stopCh) } -// HasSynced implements cache.Controller +// HasSynced implements cache.Controller. func (c *Controller) HasSynced() bool { if c.informer == nil { return false @@ -143,7 +143,7 @@ func (c *Controller) HasSynced() bool { return c.informer.HasSynced() } -// LastSyncResourceVersion implements cache.Controller +// LastSyncResourceVersion implements cache.Controller. func (c *Controller) LastSyncResourceVersion() string { if c.informer == nil { return "" diff --git a/control-plane/helper/controller/controller_test.go b/control-plane/helper/controller/controller_test.go index 2f83bc5705..43fe677280 100644 --- a/control-plane/helper/controller/controller_test.go +++ b/control-plane/helper/controller/controller_test.go @@ -22,7 +22,7 @@ func TestController_impl(t *testing.T) { var _ cache.Controller = &Controller{} } -// Test that data that exists before is synced +// Test that data that exists before is synced. func TestController_initialData(t *testing.T) { t.Parallel() require := require.New(t) @@ -46,7 +46,7 @@ func TestController_initialData(t *testing.T) { require.Len(deleted, 0) } -// Test that created data after starting is loaded +// Test that created data after starting is loaded. func TestController_create(t *testing.T) { t.Parallel() require := require.New(t) diff --git a/control-plane/namespaces/namespaces_test.go b/control-plane/namespaces/namespaces_test.go index 4e0c25884e..7b6a061a65 100644 --- a/control-plane/namespaces/namespaces_test.go +++ b/control-plane/namespaces/namespaces_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package namespaces @@ -33,7 +33,7 @@ func TestEnsureExists_AlreadyExists(tt *testing.T) { consul, err := testutil.NewTestServerConfigT(t, func(cfg *testutil.TestServerConfig) { cfg.ACL.Enabled = c.ACLsEnabled cfg.ACL.DefaultPolicy = "deny" - cfg.ACL.Tokens.Master = masterToken + cfg.ACL.Tokens.InitialManagement = masterToken }) req.NoError(err) defer consul.Stop() @@ -104,7 +104,7 @@ func TestEnsureExists_CreatesNS(tt *testing.T) { consul, err := testutil.NewTestServerConfigT(t, func(cfg *testutil.TestServerConfig) { cfg.ACL.Enabled = c.ACLsEnabled cfg.ACL.DefaultPolicy = "deny" - cfg.ACL.Tokens.Master = masterToken + cfg.ACL.Tokens.InitialManagement = masterToken }) req.NoError(err) defer consul.Stop() diff --git a/control-plane/subcommand/acl-init/command.go b/control-plane/subcommand/acl-init/command.go index 8bb7aee4b2..6017137bbf 100644 --- a/control-plane/subcommand/acl-init/command.go +++ b/control-plane/subcommand/acl-init/command.go @@ -34,6 +34,8 @@ type Command struct { once sync.Once help string + + ctx context.Context } func (c *Command) init() { @@ -64,6 +66,10 @@ func (c *Command) Run(args []string) int { return 1 } + if c.ctx == nil { + c.ctx = context.Background() + } + // Create the Kubernetes clientset if c.k8sClient == nil { config, err := subcommand.K8SConfig(c.k8s.KubeConfig()) @@ -128,7 +134,7 @@ func (c *Command) Run(args []string) int { } func (c *Command) getSecret(secretName string) (string, error) { - secret, err := c.k8sClient.CoreV1().Secrets(c.flagNamespace).Get(context.TODO(), secretName, metav1.GetOptions{}) + secret, err := c.k8sClient.CoreV1().Secrets(c.flagNamespace).Get(c.ctx, secretName, metav1.GetOptions{}) if err != nil { return "", err } diff --git a/control-plane/subcommand/acl-init/command_test.go b/control-plane/subcommand/acl-init/command_test.go index 441f858a25..0a3a7ab8bf 100644 --- a/control-plane/subcommand/acl-init/command_test.go +++ b/control-plane/subcommand/acl-init/command_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" "github.com/mitchellh/cli" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" @@ -31,7 +32,8 @@ func TestRun_TokenSinkFile(t *testing.T) { context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + Name: secretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ "token": []byte(token), @@ -72,7 +74,8 @@ func TestRun_TokenSinkFileErr(t *testing.T) { context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + Name: secretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ "token": []byte(token), @@ -118,7 +121,8 @@ func TestRun_TokenSinkFileTwice(t *testing.T) { context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + Name: secretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ "token": []byte(token), diff --git a/control-plane/subcommand/common/common.go b/control-plane/subcommand/common/common.go index 6eaa14a78a..bd60b5822c 100644 --- a/control-plane/subcommand/common/common.go +++ b/control-plane/subcommand/common/common.go @@ -9,7 +9,9 @@ import ( "strings" "github.com/go-logr/logr" + godiscover "github.com/hashicorp/consul-k8s/control-plane/helper/go-discover" "github.com/hashicorp/consul/api" + "github.com/hashicorp/go-discover" "github.com/hashicorp/go-hclog" "go.uber.org/zap/zapcore" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -24,6 +26,11 @@ const ( // ACLTokenSecretKey is the key that we store the ACL tokens in when we // create Kubernetes secrets. ACLTokenSecretKey = "token" + + // CLILabelKey and CLILabelValue are added to each secret on creation so the CLI knows + // which secrets to delete on an uninstall. + CLILabelKey = "managed-by" + CLILabelValue = "consul-k8s" ) // Logger returns an hclog instance with log level set and JSON logging enabled/disabled, or an error if level is invalid. @@ -115,3 +122,12 @@ func WriteFileWithPerms(outputFile, payload string, mode os.FileMode) error { } return os.Chmod(outputFile, mode) } + +// GetResolvedServerAddresses resolves the Consul server address if it has been provided a provider else it returns the server addresses that were input to it. +// It attempts to use go-discover iff there is a single server address, the value of which begins with "provider=", else it returns the server addresses as is. +func GetResolvedServerAddresses(serverAddresses []string, providers map[string]discover.Provider, logger hclog.Logger) ([]string, error) { + if len(serverAddresses) != 1 || !strings.Contains(serverAddresses[0], "provider=") { + return serverAddresses, nil + } + return godiscover.ConsulServerAddresses(serverAddresses[0], providers, logger) +} diff --git a/control-plane/subcommand/common/common_test.go b/control-plane/subcommand/common/common_test.go index 65268defe1..179d10a114 100644 --- a/control-plane/subcommand/common/common_test.go +++ b/control-plane/subcommand/common/common_test.go @@ -11,7 +11,11 @@ import ( "testing" "time" + "github.com/hashicorp/consul-k8s/control-plane/helper/go-discover/mocks" "github.com/hashicorp/consul/api" + "github.com/hashicorp/go-discover" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -25,7 +29,7 @@ func TestZapLogger_InvalidLogLevel(t *testing.T) { require.EqualError(t, err, "unknown log level \"invalid\": unrecognized level: \"invalid\"") } -// ZapLogger should convert "trace" log level to "debug" +// ZapLogger should convert "trace" log level to "debug". func TestZapLogger_TraceLogLevel(t *testing.T) { _, err := ZapLogger("trace", false) require.NoError(t, err) @@ -168,6 +172,45 @@ func TestWriteFileWithPerms(t *testing.T) { require.Equal(t, payload, string(data)) } +func TestGetResolvedServerAddresses(t *testing.T) { + cases := map[string]struct { + inputServerAddresses []string + providerMap func() map[string]discover.Provider + expectedServerAddresses []string + }{ + "without providers and single address": { + inputServerAddresses: []string{"foo.bar"}, + providerMap: func() map[string]discover.Provider { + return nil + }, + expectedServerAddresses: []string{"foo.bar"}, + }, + "without providers and multiple addresses": { + inputServerAddresses: []string{"foo.bar", "hello.car"}, + providerMap: func() map[string]discover.Provider { + return nil + }, + expectedServerAddresses: []string{"foo.bar", "hello.car"}, + }, + "mock provider": { + inputServerAddresses: []string{"provider=mock"}, + providerMap: func() map[string]discover.Provider { + provider := new(mocks.MockProvider) + provider.On("Addrs", mock.Anything, mock.Anything).Return([]string{"127.0.0.1", "foo.bar"}, nil) + providers := make(map[string]discover.Provider) + providers["mock"] = provider + return providers + }, + expectedServerAddresses: []string{"127.0.0.1", "foo.bar"}, + }, + } + for _, testCase := range cases { + addresses, err := GetResolvedServerAddresses(testCase.inputServerAddresses, testCase.providerMap(), hclog.NewNullLogger()) + require.NoError(t, err) + require.Equal(t, testCase.expectedServerAddresses, addresses) + } +} + // startMockServer starts an httptest server used to mock a Consul server's // /v1/acl/login endpoint. apiCallCounter will be incremented on each call to /v1/acl/login. // It returns a consul client pointing at the server. diff --git a/control-plane/subcommand/connect-init/command.go b/control-plane/subcommand/connect-init/command.go index d8158b5f20..01a23e9c2c 100644 --- a/control-plane/subcommand/connect-init/command.go +++ b/control-plane/subcommand/connect-init/command.go @@ -26,6 +26,9 @@ const ( numLoginRetries = 3 // The number of times to attempt to read this service (120s). defaultServicePollingRetries = 120 + + raftReplicationTimeout = 2 * time.Second + tokenReadPollingInterval = 100 * time.Millisecond ) type Command struct { @@ -41,9 +44,10 @@ type Command struct { flagLogLevel string flagLogJSON bool - bearerTokenFile string // Location of the bearer token. Default is /var/run/secrets/kubernetes.io/serviceaccount/token. - tokenSinkFile string // Location to write the output token. Default is defaultTokenSinkFile. - proxyIDFile string // Location to write the output proxyID. Default is defaultProxyIDFile. + flagBearerTokenFile string // Location of the bearer token. Default is /var/run/secrets/kubernetes.io/serviceaccount/token. + flagACLTokenSink string // Location to write the output token. Default is defaultTokenSinkFile. + flagProxyIDFile string // Location to write the output proxyID. Default is defaultProxyIDFile. + flagMultiPort bool serviceRegistrationPollingAttempts uint64 // Number of times to poll for this service to be registered. flagSet *flag.FlagSet @@ -63,21 +67,16 @@ func (c *Command) init() { c.flagSet.StringVar(&c.flagConsulServiceNamespace, "consul-service-namespace", "", "Consul destination namespace of the service.") c.flagSet.StringVar(&c.flagServiceAccountName, "service-account-name", "", "Service account name on the pod.") c.flagSet.StringVar(&c.flagServiceName, "service-name", "", "Service name as specified via the pod annotation.") + c.flagSet.StringVar(&c.flagBearerTokenFile, "bearer-token-file", defaultBearerTokenFile, "Path to service account token file.") + c.flagSet.StringVar(&c.flagACLTokenSink, "acl-token-sink", defaultTokenSinkFile, "File name where where ACL token should be saved.") + c.flagSet.StringVar(&c.flagProxyIDFile, "proxy-id-file", defaultProxyIDFile, "File name where proxy's Consul service ID should be saved.") + c.flagSet.BoolVar(&c.flagMultiPort, "multiport", false, "If the pod is a multi port pod.") c.flagSet.StringVar(&c.flagLogLevel, "log-level", "info", "Log verbosity level. Supported values (in order of detail) are \"trace\", "+ "\"debug\", \"info\", \"warn\", and \"error\".") c.flagSet.BoolVar(&c.flagLogJSON, "log-json", false, "Enable or disable JSON output format for logging.") - if c.bearerTokenFile == "" { - c.bearerTokenFile = defaultBearerTokenFile - } - if c.tokenSinkFile == "" { - c.tokenSinkFile = defaultTokenSinkFile - } - if c.proxyIDFile == "" { - c.proxyIDFile = defaultProxyIDFile - } if c.serviceRegistrationPollingAttempts == 0 { c.serviceRegistrationPollingAttempts = defaultServicePollingRetries } @@ -131,24 +130,73 @@ func (c *Command) Run(args []string) int { // loginMeta is the default metadata that we pass to the consul login API. loginMeta := map[string]string{"pod": fmt.Sprintf("%s/%s", c.flagPodNamespace, c.flagPodName)} err = backoff.Retry(func() error { - err := common.ConsulLogin(consulClient, c.bearerTokenFile, c.flagACLAuthMethod, c.tokenSinkFile, c.flagAuthMethodNamespace, loginMeta) + err := common.ConsulLogin(consulClient, c.flagBearerTokenFile, c.flagACLAuthMethod, c.flagACLTokenSink, c.flagAuthMethodNamespace, loginMeta) if err != nil { c.logger.Error("Consul login failed; retrying", "error", err) } return err }, backoff.WithMaxRetries(backoff.NewConstantBackOff(1*time.Second), numLoginRetries)) if err != nil { + if c.flagServiceAccountName == "default" { + c.logger.Warn("The service account name for this Pod is \"default\"." + + " In default installations this is not a supported service account name." + + " The service account name must match the name of the Kubernetes Service" + + " or the consul.hashicorp.com/connect-service annotation.") + } c.logger.Error("Hit maximum retries for consul login", "error", err) return 1 } // Now update the client so that it will read the ACL token we just fetched. - cfg.TokenFile = c.tokenSinkFile + cfg.TokenFile = c.flagACLTokenSink consulClient, err = consul.NewClient(cfg) if err != nil { c.logger.Error("Unable to update client connection", "error", err) return 1 } c.logger.Info("Consul login complete") + + // A workaround to check that the ACL token is replicated to other Consul servers. + // + // A consul client may reach out to a follower instead of a leader to resolve the token during the + // call to get services below. This is because clients talk to servers in the stale consistency mode + // to decrease the load on the servers (see https://www.consul.io/docs/architecture/consensus#stale). + // In that case, it's possible that the token isn't replicated + // to that server instance yet. The client will then get an "ACL not found" error + // and subsequently cache this not found response. Then our call below + // to get services from the agent will keep hitting the same "ACL not found" error + // until the cache entry expires (determined by the `acl_token_ttl` which defaults to 30 seconds). + // This is not great because it will delay app start up time by 30 seconds in most cases + // (if you are running 3 servers, then the probability of ending up on a follower is close to 2/3). + // + // To help with that, we try to first read the token in the stale consistency mode until we + // get a successful response. This should not take more than 100ms because raft replication + // should in most cases take less than that (see https://www.consul.io/docs/install/performance#read-write-tuning) + // but we set the timeout to 2s to be sure. + // + // Note though that this workaround does not eliminate this problem completely. It's still possible + // for this call and the next call to reach different servers and those servers to have different + // states from each other. + // For example, this call can reach a leader and succeed, while the call below can go to a follower + // that is still behind the leader and get an "ACL not found" error. + // However, this is a pretty unlikely case because + // clients have sticky connections to a server, and those connections get rebalanced only every 2-3min. + // And so, this workaround should work in a vast majority of cases. + c.logger.Info("Checking that the ACL token exists when reading it in the stale consistency mode") + // Use raft timeout and polling interval to determine the number of retries. + numTokenReadRetries := uint64(raftReplicationTimeout.Milliseconds() / tokenReadPollingInterval.Milliseconds()) + err = backoff.Retry(func() error { + _, _, err := consulClient.ACL().TokenReadSelf(&api.QueryOptions{AllowStale: true}) + if err != nil { + c.logger.Error("Unable to read ACL token; retrying", "err", err) + } + return err + }, backoff.WithMaxRetries(backoff.NewConstantBackOff(tokenReadPollingInterval), numTokenReadRetries)) + if err != nil { + c.logger.Error("Unable to read ACL token from a Consul server; "+ + "please check that your server cluster is healthy", "err", err) + return 1 + } + c.logger.Info("Successfully read ACL token from the server") } // Now wait for the service to be registered. Do this by querying the Agent for a service @@ -158,7 +206,13 @@ func (c *Command) Run(args []string) int { var errServiceNameMismatch error err = backoff.Retry(func() error { registrationRetryCount++ - filter := fmt.Sprintf("Meta[%q] == %q and Meta[%q] == %q", connectinject.MetaKeyPodName, c.flagPodName, connectinject.MetaKeyKubeNS, c.flagPodNamespace) + filter := fmt.Sprintf("Meta[%q] == %q and Meta[%q] == %q ", + connectinject.MetaKeyPodName, c.flagPodName, connectinject.MetaKeyKubeNS, c.flagPodNamespace) + if c.flagMultiPort && c.flagServiceName != "" { + // If the service name is set and this is a multi-port pod there may be multiple services registered for + // this one Pod. If so, we want to ensure the service and proxy matching our expected name is registered. + filter += fmt.Sprintf(` and (Service == %q or Service == "%s-sidecar-proxy")`, c.flagServiceName, c.flagServiceName) + } serviceList, err := consulClient.Agent().ServicesWithFilter(filter) if err != nil { c.logger.Error("Unable to get Agent services", "error", err) @@ -173,7 +227,13 @@ func (c *Command) Run(args []string) int { c.logger.Info("Check to ensure a Kubernetes service has been created for this application." + " If your pod is not starting also check the connect-inject deployment logs.") } - return fmt.Errorf("did not find correct number of services: %d", len(serviceList)) + if len(serviceList) > 2 { + c.logger.Error("There are multiple Consul services registered for this pod when there must only be one." + + " Check if there are multiple Kubernetes services selecting this pod and add the label" + + " `consul.hashicorp.com/service-ignore: \"true\"` to all services except the one used by Consul for handling requests.") + } + + return fmt.Errorf("did not find correct number of services, found: %d, services: %+v", len(serviceList), serviceList) } for _, svc := range serviceList { c.logger.Info("Registered service has been detected", "service", svc.Service) @@ -213,7 +273,7 @@ func (c *Command) Run(args []string) int { return 1 } // Write the proxy ID to the shared volume so `consul connect envoy` can use it for bootstrapping. - err = common.WriteFileWithPerms(c.proxyIDFile, proxyID, os.FileMode(0444)) + err = common.WriteFileWithPerms(c.flagProxyIDFile, proxyID, os.FileMode(0444)) if err != nil { c.logger.Error("Unable to write proxy ID to file", "error", err) return 1 diff --git a/control-plane/subcommand/connect-init/command_ent_test.go b/control-plane/subcommand/connect-init/command_ent_test.go index 532a0e5066..891ee2ed32 100644 --- a/control-plane/subcommand/connect-init/command_ent_test.go +++ b/control-plane/subcommand/connect-init/command_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package connectinit @@ -26,84 +26,97 @@ func TestRun_ServicePollingWithACLsAndTLSWithNamespaces(t *testing.T) { consulServiceNamespace string acls bool authMethodNamespace string + adminPartition string }{ { - name: "ACLs enabled, no tls, serviceNS=default, authMethodNS=default", + name: "ACLs enabled, no tls, serviceNS=default, authMethodNS=default, partition=default", tls: false, consulServiceNamespace: "default", authMethodNamespace: "default", acls: true, + adminPartition: "default", }, { - name: "ACLs enabled, tls, serviceNS=default, authMethodNS=default", + name: "ACLs enabled, tls, serviceNS=default, authMethodNS=default, partition=default", tls: true, consulServiceNamespace: "default", authMethodNamespace: "default", acls: true, + adminPartition: "default", }, { - name: "ACLs enabled, no tls, serviceNS=default-ns, authMethodNS=default", + name: "ACLs enabled, no tls, serviceNS=default-ns, authMethodNS=default, partition=default", tls: false, consulServiceNamespace: "default-ns", authMethodNamespace: "default", acls: true, + adminPartition: "default", }, { - name: "ACLs enabled, tls, serviceNS=default-ns, authMethodNS=default", + name: "ACLs enabled, tls, serviceNS=default-ns, authMethodNS=default, partition=default", tls: true, consulServiceNamespace: "default-ns", authMethodNamespace: "default", acls: true, + adminPartition: "default", }, { - name: "ACLs enabled, no tls, serviceNS=other, authMethodNS=other", + name: "ACLs enabled, no tls, serviceNS=other, authMethodNS=other, partition=default", tls: false, consulServiceNamespace: "other", authMethodNamespace: "other", acls: true, + adminPartition: "default", }, { - name: "ACLs enabled, tls, serviceNS=other, authMethodNS=other", + name: "ACLs enabled, tls, serviceNS=other, authMethodNS=other, partition=default", tls: true, consulServiceNamespace: "other", authMethodNamespace: "other", acls: true, + adminPartition: "default", }, { - name: "ACLs disabled, no tls, serviceNS=default, authMethodNS=default", + name: "ACLs disabled, no tls, serviceNS=default, authMethodNS=default, partition=default", tls: false, consulServiceNamespace: "default", authMethodNamespace: "default", + adminPartition: "default", }, { - name: "ACLs disabled, tls, serviceNS=default, authMethodNS=default", + name: "ACLs disabled, tls, serviceNS=default, authMethodNS=default, partition=default", tls: true, consulServiceNamespace: "default", authMethodNamespace: "default", + adminPartition: "default", }, { - name: "ACLs disabled, no tls, serviceNS=default-ns, authMethodNS=default", + name: "ACLs disabled, no tls, serviceNS=default-ns, authMethodNS=default, partition=default", tls: false, consulServiceNamespace: "default-ns", authMethodNamespace: "default", + adminPartition: "default", }, { - name: "ACLs disabled, tls, serviceNS=default-ns, authMethodNS=default", + name: "ACLs disabled, tls, serviceNS=default-ns, authMethodNS=default, partition=default", tls: true, consulServiceNamespace: "default-ns", authMethodNamespace: "default", + adminPartition: "default", }, { - name: "ACLs disabled, no tls, serviceNS=other, authMethodNS=other", + name: "ACLs disabled, no tls, serviceNS=other, authMethodNS=other, partition=default", tls: false, consulServiceNamespace: "other", authMethodNamespace: "other", + adminPartition: "default", }, { - name: "ACLs disabled, tls, serviceNS=other, authMethodNS=other", + name: "ACLs disabled, tls, serviceNS=other, authMethodNS=other, partition=default", tls: true, consulServiceNamespace: "other", authMethodNamespace: "other", + adminPartition: "default", }, } for _, c := range cases { @@ -123,7 +136,7 @@ func TestRun_ServicePollingWithACLsAndTLSWithNamespaces(t *testing.T) { if c.acls { cfg.ACL.Enabled = true cfg.ACL.DefaultPolicy = "deny" - cfg.ACL.Tokens.Master = masterToken + cfg.ACL.Tokens.InitialManagement = masterToken } if c.tls { caFile, certFile, keyFile = test.GenerateServerCerts(t) @@ -139,6 +152,7 @@ func TestRun_ServicePollingWithACLsAndTLSWithNamespaces(t *testing.T) { Scheme: "http", Address: server.HTTPAddr, Namespace: c.consulServiceNamespace, + Partition: c.adminPartition, } if c.acls { cfg.Token = masterToken @@ -170,9 +184,6 @@ func TestRun_ServicePollingWithACLsAndTLSWithNamespaces(t *testing.T) { ui := cli.NewMockUi() cmd := Command{ UI: ui, - bearerTokenFile: bearerFile, - tokenSinkFile: tokenFile, - proxyIDFile: proxyFile, serviceRegistrationPollingAttempts: 5, } // We build the http-addr because normally it's defined by the init container setting @@ -182,6 +193,9 @@ func TestRun_ServicePollingWithACLsAndTLSWithNamespaces(t *testing.T) { "-service-account-name", testServiceAccountName, "-http-addr", fmt.Sprintf("%s://%s", cfg.Scheme, cfg.Address), "-consul-service-namespace", c.consulServiceNamespace, + "-acl-token-sink", tokenFile, + "-bearer-token-file", bearerFile, + "-proxy-id-file", proxyFile, } if c.acls { flags = append(flags, "-acl-auth-method", test.AuthMethod, "-auth-method-namespace", c.authMethodNamespace) diff --git a/control-plane/subcommand/connect-init/command_test.go b/control-plane/subcommand/connect-init/command_test.go index 704c7356a6..7965a5ea30 100644 --- a/control-plane/subcommand/connect-init/command_test.go +++ b/control-plane/subcommand/connect-init/command_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "net/url" "os" + "strconv" "testing" "time" @@ -68,6 +69,7 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { includeServiceAccountName bool serviceAccountNameMismatch bool expFail bool + multiport bool }{ { name: "ACLs enabled, no tls", @@ -91,6 +93,13 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { serviceAccountName: "web", serviceName: "web", }, + { + name: "ACLs enabled, multiport service", + tls: false, + serviceAccountName: "counting-admin", + serviceName: "counting-admin", + multiport: true, + }, { name: "ACLs enabled, service name annotation doesn't match service account name", tls: false, @@ -122,7 +131,7 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { server, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { c.ACL.Enabled = true c.ACL.DefaultPolicy = "deny" - c.ACL.Tokens.Master = masterToken + c.ACL.Tokens.InitialManagement = masterToken if tt.tls { caFile, certFile, keyFile = test.GenerateServerCerts(t) c.CAFile = caFile @@ -152,6 +161,9 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { // Register Consul services. testConsulServices := []api.AgentServiceRegistration{consulCountingSvc, consulCountingSvcSidecar} + if tt.multiport { + testConsulServices = append(testConsulServices, consulCountingSvcMultiport, consulCountingSvcSidecarMultiport) + } for _, svc := range testConsulServices { require.NoError(t, consulClient.Agent().ServiceRegister(&svc)) } @@ -159,9 +171,6 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { ui := cli.NewMockUi() cmd := Command{ UI: ui, - bearerTokenFile: bearerFile, - tokenSinkFile: tokenFile, - proxyIDFile: proxyFile, serviceRegistrationPollingAttempts: 3, } @@ -173,6 +182,10 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { "-service-account-name", tt.serviceAccountName, "-service-name", tt.serviceName, "-http-addr", fmt.Sprintf("%s://%s", cfg.Scheme, cfg.Address), + "-bearer-token-file", bearerFile, + "-acl-token-sink", tokenFile, + "-proxy-id-file", proxyFile, + "-multiport=" + strconv.FormatBool(tt.multiport), } // Add the CA File if necessary since we're not setting CONSUL_CACERT in tt ENV. if tt.tls { @@ -201,7 +214,11 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { // Validate contents of proxyFile. data, err := ioutil.ReadFile(proxyFile) require.NoError(t, err) - require.Contains(t, string(data), "counting-counting-sidecar-proxy") + if tt.multiport { + require.Contains(t, string(data), "counting-admin-sidecar-proxy-id") + } else { + require.Contains(t, string(data), "counting-counting-sidecar-proxy") + } }) } } @@ -210,8 +227,10 @@ func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) { func TestRun_ServicePollingOnly(t *testing.T) { t.Parallel() cases := []struct { - name string - tls bool + name string + tls bool + serviceName string + multiport bool }{ { name: "ACLs disabled, no tls", @@ -221,6 +240,12 @@ func TestRun_ServicePollingOnly(t *testing.T) { name: "ACLs disabled, tls", tls: true, }, + { + name: "Multiport, ACLs disabled, no tls", + tls: false, + serviceName: "counting-admin", + multiport: true, + }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { @@ -260,6 +285,9 @@ func TestRun_ServicePollingOnly(t *testing.T) { // Register Consul services. testConsulServices := []api.AgentServiceRegistration{consulCountingSvc, consulCountingSvcSidecar} + if tt.multiport { + testConsulServices = append(testConsulServices, consulCountingSvcMultiport, consulCountingSvcSidecarMultiport) + } for _, svc := range testConsulServices { require.NoError(t, consulClient.Agent().ServiceRegister(&svc)) } @@ -267,7 +295,6 @@ func TestRun_ServicePollingOnly(t *testing.T) { ui := cli.NewMockUi() cmd := Command{ UI: ui, - proxyIDFile: proxyFile, serviceRegistrationPollingAttempts: 3, } // We build the http-addr because normally it's defined by the init container setting @@ -275,7 +302,15 @@ func TestRun_ServicePollingOnly(t *testing.T) { flags := []string{ "-pod-name", testPodName, "-pod-namespace", testPodNamespace, + "-proxy-id-file", proxyFile, + "-multiport=" + strconv.FormatBool(tt.multiport), "-http-addr", fmt.Sprintf("%s://%s", cfg.Scheme, cfg.Address)} + + // In a multiport case, the service name will be passed in to the test. + if tt.serviceName != "" { + flags = append(flags, "-service-name", tt.serviceName) + } + // Add the CA File if necessary since we're not setting CONSUL_CACERT in tt ENV. if tt.tls { flags = append(flags, "-ca-file", caFile) @@ -288,7 +323,11 @@ func TestRun_ServicePollingOnly(t *testing.T) { // Validate contents of proxyFile. data, err := ioutil.ReadFile(proxyFile) require.NoError(t, err) - require.Contains(t, string(data), "counting-counting-sidecar-proxy") + if tt.multiport { + require.Contains(t, string(data), "counting-admin-sidecar-proxy-id") + } else { + require.Contains(t, string(data), "counting-counting-sidecar-proxy") + } }) } @@ -460,13 +499,13 @@ func TestRun_ServicePollingErrors(t *testing.T) { ui := cli.NewMockUi() cmd := Command{ UI: ui, - proxyIDFile: proxyFile, serviceRegistrationPollingAttempts: 1, } flags := []string{ "-http-addr", server.HTTPAddr, "-pod-name", testPodName, "-pod-namespace", testPodNamespace, + "-proxy-id-file", proxyFile, } code := cmd.Run(flags) @@ -504,13 +543,13 @@ func TestRun_RetryServicePolling(t *testing.T) { ui := cli.NewMockUi() cmd := Command{ UI: ui, - proxyIDFile: proxyFile, serviceRegistrationPollingAttempts: 10, } flags := []string{ "-pod-name", testPodName, "-pod-namespace", testPodNamespace, "-http-addr", server.HTTPAddr, + "-proxy-id-file", proxyFile, } code := cmd.Run(flags) require.Equal(t, 0, code) @@ -544,13 +583,13 @@ func TestRun_InvalidProxyFile(t *testing.T) { ui := cli.NewMockUi() cmd := Command{ UI: ui, - proxyIDFile: randFileName, serviceRegistrationPollingAttempts: 3, } flags := []string{ "-pod-name", testPodName, "-pod-namespace", testPodNamespace, "-http-addr", server.HTTPAddr, + "-proxy-id-file", randFileName, } code := cmd.Run(flags) require.Equal(t, 1, code) @@ -592,6 +631,10 @@ func TestRun_FailsWithBadServerResponses(t *testing.T) { if r != nil && r.URL.Path == "/v1/acl/login" && r.Method == "POST" { w.Write([]byte(c.loginResponse)) } + // Token read request. + if r != nil && r.URL.Path == "/v1/acl/token/self" && r.Method == "GET" { + w.Write([]byte(testTokenReadSelfResponse)) + } // Agent Services get. if r != nil && r.URL.Path == "/v1/agent/services" && r.Method == "GET" { servicesGetCounter++ @@ -600,12 +643,12 @@ func TestRun_FailsWithBadServerResponses(t *testing.T) { })) defer consulServer.Close() - // Setup the Command. + // Set up the Command. ui := cli.NewMockUi() cmd := Command{ UI: ui, - bearerTokenFile: bearerFile, - tokenSinkFile: tokenFile, + flagBearerTokenFile: bearerFile, + flagACLTokenSink: tokenFile, serviceRegistrationPollingAttempts: uint64(servicesGetRetries), } @@ -615,6 +658,8 @@ func TestRun_FailsWithBadServerResponses(t *testing.T) { "-pod-name", testPodName, "-pod-namespace", testPodNamespace, "-acl-auth-method", test.AuthMethod, "-service-account-name", testServiceAccountName, + "-bearer-token-file", bearerFile, + "-acl-token-sink", tokenFile, "-http-addr", serverURL.String()} code := cmd.Run(flags) require.Equal(t, 1, code) @@ -663,6 +708,10 @@ func TestRun_LoginWithRetries(t *testing.T) { w.Write([]byte(testLoginResponse)) } } + // Token read request. + if r != nil && r.URL.Path == "/v1/acl/token/self" && r.Method == "GET" { + w.Write([]byte(testTokenReadSelfResponse)) + } // Agent Services get. if r != nil && r.URL.Path == "/v1/agent/services" && r.Method == "GET" { w.Write([]byte(testServiceListResponse)) @@ -675,16 +724,16 @@ func TestRun_LoginWithRetries(t *testing.T) { ui := cli.NewMockUi() cmd := Command{ - UI: ui, - tokenSinkFile: tokenFile, - bearerTokenFile: bearerFile, - proxyIDFile: proxyFile, + UI: ui, } code := cmd.Run([]string{ "-pod-name", testPodName, "-pod-namespace", testPodNamespace, "-acl-auth-method", test.AuthMethod, "-service-account-name", testServiceAccountName, + "-acl-token-sink", tokenFile, + "-bearer-token-file", bearerFile, + "-proxy-id-file", proxyFile, "-http-addr", serverURL.String()}) fmt.Println(ui.ErrorWriter.String()) require.Equal(t, c.ExpCode, code) @@ -702,6 +751,79 @@ func TestRun_LoginWithRetries(t *testing.T) { } } +// Test that we check token exists when reading it in the stale consistency mode. +func TestRun_EnsureTokenExists(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + neverSucceed bool + }{ + "succeed after first retry": {neverSucceed: false}, + "never succeed": {neverSucceed: true}, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + // Create a fake input bearer token file and an output file. + bearerFile := common.WriteTempFile(t, "bearerTokenFile") + tokenFile := common.WriteTempFile(t, "") + proxyFile := common.WriteTempFile(t, "") + + // Start the mock Consul server. + counter := 0 + consulServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // ACL Login. + if r != nil && r.URL.Path == "/v1/acl/login" && r.Method == "POST" { + w.Write([]byte(testLoginResponse)) + } + // Token read request. + if r != nil && + r.URL.Path == "/v1/acl/token/self" && + r.Method == "GET" && + r.URL.Query().Has("stale") { + + // Fail the first request but succeed on the next. + if counter == 0 || c.neverSucceed { + counter++ + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("ACL not found")) + } else { + w.Write([]byte(testTokenReadSelfResponse)) + } + } + // Agent Services get. + if r != nil && r.URL.Path == "/v1/agent/services" && r.Method == "GET" { + w.Write([]byte(testServiceListResponse)) + } + })) + defer consulServer.Close() + + serverURL, err := url.Parse(consulServer.URL) + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + code := cmd.Run([]string{ + "-pod-name", testPodName, + "-pod-namespace", testPodNamespace, + "-acl-auth-method", test.AuthMethod, + "-service-account-name", testServiceAccountName, + "-acl-token-sink", tokenFile, + "-bearer-token-file", bearerFile, + "-proxy-id-file", proxyFile, + "-http-addr", serverURL.String()}) + if c.neverSucceed { + require.Equal(t, 1, code) + } else { + require.Equal(t, 0, code) + require.Equal(t, 1, counter) + } + }) + } +} + const ( metaKeyPodName = "pod-name" metaKeyKubeNS = "k8s-namespace" @@ -710,7 +832,7 @@ const ( testPodName = "counting-pod" testServiceAccountName = "counting" - // sample response from https://consul.io/api-docs/acl#sample-response + // Sample response from https://consul.io/api-docs/acl#sample-response. testLoginResponse = `{ "AccessorID": "926e2bd2-b344-d91b-0c83-ae89f372cd9b", "SecretID": "b78d37c7-0ca7-5f4d-99ee-6d9975ce4586", @@ -734,6 +856,30 @@ const ( "ModifyIndex": 36 }` + // Sample response from https://www.consul.io/api-docs/acl/tokens#read-self-token. + testTokenReadSelfResponse = ` +{ + "AccessorID": "6a1253d2-1785-24fd-91c2-f8e78c745511", + "SecretID": "45a3bd52-07c7-47a4-52fd-0745e0cfe967", + "Description": "Agent token for 'node1'", + "Policies": [ + { + "ID": "165d4317-e379-f732-ce70-86278c4558f7", + "Name": "node1-write" + }, + { + "ID": "e359bd81-baca-903e-7e64-1ccd9fdc78f5", + "Name": "node-read" + } + ], + "Local": false, + "CreateTime": "2018-10-24T12:25:06.921933-04:00", + "Hash": "UuiRkOQPRCvoRZHRtUxxbrmwZ5crYrOdZ0Z1FTFbTbA=", + "CreateIndex": 59, + "ModifyIndex": 59 +} +` + testServiceListResponse = `{ "counting-counting": { "ID": "counting-counting", @@ -832,4 +978,32 @@ var ( metaKeyKubeServiceName: "counting", }, } + consulCountingSvcMultiport = api.AgentServiceRegistration{ + ID: "counting-admin-id", + Name: "counting-admin", + Address: "127.0.0.1", + Meta: map[string]string{ + metaKeyPodName: "counting-pod", + metaKeyKubeNS: "default-ns", + metaKeyKubeServiceName: "counting-admin", + }, + } + consulCountingSvcSidecarMultiport = api.AgentServiceRegistration{ + ID: "counting-admin-sidecar-proxy-id", + Name: "counting-admin-sidecar-proxy", + Kind: "connect-proxy", + Proxy: &api.AgentServiceConnectProxyConfig{ + DestinationServiceName: "counting-admin", + DestinationServiceID: "counting-admin-id", + Config: nil, + Upstreams: nil, + }, + Port: 9999, + Address: "127.0.0.1", + Meta: map[string]string{ + metaKeyPodName: "counting-pod", + metaKeyKubeNS: "default-ns", + metaKeyKubeServiceName: "counting-admin", + }, + } ) diff --git a/control-plane/subcommand/consul-sidecar/command.go b/control-plane/subcommand/consul-sidecar/command.go index dcb05199ba..509696378d 100644 --- a/control-plane/subcommand/consul-sidecar/command.go +++ b/control-plane/subcommand/consul-sidecar/command.go @@ -21,8 +21,13 @@ import ( "github.com/mitchellh/cli" ) -const metricsServerShutdownTimeout = 5 * time.Second -const envoyMetricsAddr = "http://127.0.0.1:19000/stats/prometheus" +const ( + metricsServerShutdownTimeout = 5 * time.Second + envoyMetricsAddr = "http://127.0.0.1:19000/stats/prometheus" + // prometheusServiceMetricsSuccessKey is the key of the prometheus metric used to + // indicate if service metrics were scraped successfully. + prometheusServiceMetricsSuccessKey = "consul_merged_service_metrics_success" +) type Command struct { UI cli.Ui @@ -240,27 +245,36 @@ func (c *Command) createMergedMetricsServer() *http.Server { mergedMetricsServerAddr := fmt.Sprintf("127.0.0.1:%s", c.flagMergedMetricsPort) server := &http.Server{Addr: mergedMetricsServerAddr, Handler: mux} + // http.Client satisfies the metricsGetter interface. // The default http.Client timeout is indefinite, so adding a timeout makes // sure that requests don't hang. client := &http.Client{ Timeout: time.Second * 10, } - // http.Client satisfies the metricsGetter interface. - c.envoyMetricsGetter = client - c.serviceMetricsGetter = client + + // During tests these may already be set to mocks. + if c.envoyMetricsGetter == nil { + c.envoyMetricsGetter = client + } + if c.serviceMetricsGetter == nil { + c.serviceMetricsGetter = client + } return server } // mergedMetricsHandler has the logic to append both Envoy and service metrics // together, logging if it's unsuccessful at either. +// If the Envoy scrape fails, we respond with a 500 code which follows the Prometheus +// exporter guidelines. If the service scrape fails, we respond with a 200 so +// that the Envoy metrics are still scraped. +// We also include a metric line in each response indicating the success or +// failure of the service metric scraping. func (c *Command) mergedMetricsHandler(rw http.ResponseWriter, _ *http.Request) { - envoyMetrics, err := c.envoyMetricsGetter.Get(envoyMetricsAddr) if err != nil { - // If there is an error scraping Envoy, we want the handler to return - // without writing anything to the response, and log the error. - c.logger.Error(fmt.Sprintf("Error scraping Envoy proxy metrics: %s", err.Error())) + c.logger.Error("Error scraping Envoy proxy metrics", "err", err) + http.Error(rw, fmt.Sprintf("Error scraping Envoy proxy metrics: %s", err), http.StatusInternalServerError) return } @@ -273,18 +287,22 @@ func (c *Command) mergedMetricsHandler(rw http.ResponseWriter, _ *http.Request) }() envoyMetricsBody, err := ioutil.ReadAll(envoyMetrics.Body) if err != nil { - c.logger.Error(fmt.Sprintf("Couldn't read Envoy proxy metrics: %s", err.Error())) + c.logger.Error("Could not read Envoy proxy metrics", "err", err) + http.Error(rw, fmt.Sprintf("Could not read Envoy proxy metrics: %s", err), http.StatusInternalServerError) return } - _, err = rw.Write(envoyMetricsBody) - if err != nil { - c.logger.Error(fmt.Sprintf("Error writing envoy metrics body: %s", err.Error())) + if non2xxCode(envoyMetrics.StatusCode) { + c.logger.Error("Received non-2xx status code scraping Envoy proxy metrics", "code", envoyMetrics.StatusCode, "response", string(envoyMetricsBody)) + http.Error(rw, fmt.Sprintf("Received non-2xx status code scraping Envoy proxy metrics: %d: %s", envoyMetrics.StatusCode, string(envoyMetricsBody)), http.StatusInternalServerError) + return } + writeResponse(rw, envoyMetricsBody, "envoy metrics", c.logger) serviceMetricsAddr := fmt.Sprintf("http://127.0.0.1:%s%s", c.flagServiceMetricsPort, c.flagServiceMetricsPath) serviceMetrics, err := c.serviceMetricsGetter.Get(serviceMetricsAddr) if err != nil { - c.logger.Warn(fmt.Sprintf("Error scraping service metrics: %s", err.Error())) + c.logger.Warn("Error scraping service metrics", "err", err) + writeResponse(rw, serviceMetricSuccess(false), "service metrics success", c.logger) // Since we've already written the Envoy metrics to the response, we can // return at this point if we were unable to get service metrics. return @@ -300,12 +318,25 @@ func (c *Command) mergedMetricsHandler(rw http.ResponseWriter, _ *http.Request) }() serviceMetricsBody, err := ioutil.ReadAll(serviceMetrics.Body) if err != nil { - c.logger.Error(fmt.Sprintf("Couldn't read service metrics: %s", err.Error())) + c.logger.Error("Could not read service metrics", "err", err) + writeResponse(rw, serviceMetricSuccess(false), "service metrics success", c.logger) return } - _, err = rw.Write(serviceMetricsBody) + if non2xxCode(serviceMetrics.StatusCode) { + c.logger.Error("Received non-2xx status code scraping service metrics", "code", serviceMetrics.StatusCode, "response", string(serviceMetricsBody)) + writeResponse(rw, serviceMetricSuccess(false), "service metrics success", c.logger) + return + } + writeResponse(rw, serviceMetricsBody, "service metrics", c.logger) + writeResponse(rw, serviceMetricSuccess(true), "service metrics success", c.logger) +} + +// writeResponse is a helper method to write resp to rw and log if there is an error writing. +// respName is the name of this response that will be used in the error log. +func writeResponse(rw http.ResponseWriter, resp []byte, respName string, logger hclog.Logger) { + _, err := rw.Write(resp) if err != nil { - c.logger.Error(fmt.Sprintf("Error writing service metrics body: %s", err.Error())) + logger.Error(fmt.Sprintf("Error writing %s: %s", respName, err.Error())) } } @@ -339,6 +370,21 @@ func (c *Command) validateFlags() error { return nil } +// non2xxCode returns true if code is not in the range of 200-299 inclusive. +func non2xxCode(code int) bool { + return code < 200 || code >= 300 +} + +// serviceMetricSuccess returns a prometheus metric line indicating +// the success of the metrics merging. +func serviceMetricSuccess(success bool) []byte { + boolAsInt := 0 + if success { + boolAsInt = 1 + } + return []byte(fmt.Sprintf("%s %d\n", prometheusServiceMetricsSuccessKey, boolAsInt)) +} + // parseConsulFlags creates Consul client command flags // from command's HTTP flags and returns them as an array of strings. func (c *Command) parseConsulFlags() []string { @@ -352,7 +398,7 @@ func (c *Command) parseConsulFlags() []string { } // interrupt sends os.Interrupt signal to the command -// so it can exit gracefully. This function is needed for tests +// so it can exit gracefully. This function is needed for tests. func (c *Command) interrupt() { c.sendSignal(syscall.SIGINT) } diff --git a/control-plane/subcommand/consul-sidecar/command_ent_test.go b/control-plane/subcommand/consul-sidecar/command_ent_test.go index ed20e1eb76..c41e73842b 100644 --- a/control-plane/subcommand/consul-sidecar/command_ent_test.go +++ b/control-plane/subcommand/consul-sidecar/command_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package consulsidecar diff --git a/control-plane/subcommand/consul-sidecar/command_test.go b/control-plane/subcommand/consul-sidecar/command_test.go index 6ee55f8ea0..f9b7c77625 100644 --- a/control-plane/subcommand/consul-sidecar/command_test.go +++ b/control-plane/subcommand/consul-sidecar/command_test.go @@ -91,7 +91,7 @@ func TestRunSignalHandlingMetricsOnly(t *testing.T) { UI: ui, } - randomPorts := freeport.MustTake(1) + randomPorts := freeport.GetN(t, 1) // Run async because we need to kill it when the test is over. exitChan := runCommandAsynchronously(&cmd, []string{ "-enable-service-registration=false", @@ -163,7 +163,7 @@ func TestRunSignalHandlingAllProcessesEnabled(t *testing.T) { require.NoError(t, err) - randomPorts := freeport.MustTake(1) + randomPorts := freeport.GetN(t, 1) // Run async because we need to kill it when the test is over. exitChan := runCommandAsynchronously(&cmd, []string{ "-service-config", configFile, @@ -214,56 +214,94 @@ func TestRunSignalHandlingAllProcessesEnabled(t *testing.T) { } } -type envoyMetrics struct { +type mockEnvoyMetricsGetter struct { + respStatusCode int } -func (em *envoyMetrics) Get(url string) (resp *http.Response, err error) { +func (em *mockEnvoyMetricsGetter) Get(_ string) (resp *http.Response, err error) { response := &http.Response{} + response.StatusCode = em.respStatusCode response.Body = ioutil.NopCloser(bytes.NewReader([]byte("envoy metrics\n"))) return response, nil } -type serviceMetrics struct { - url string +// mockServiceMetricsGetter. +type mockServiceMetricsGetter struct { + // reqURL is the last URL that was passed to Get(url) + reqURL string + + // respStatusCode is the status code to use for the response. + respStatusCode int } -func (sm *serviceMetrics) Get(url string) (resp *http.Response, err error) { +func (sm *mockServiceMetricsGetter) Get(url string) (resp *http.Response, err error) { + // Record the URL that we were called with. + sm.reqURL = url + response := &http.Response{} response.Body = ioutil.NopCloser(bytes.NewReader([]byte("service metrics\n"))) - sm.url = url + response.StatusCode = sm.respStatusCode + return response, nil } func TestMergedMetricsServer(t *testing.T) { cases := []struct { - name string - runEnvoyMetricsServer bool - runServiceMetricsServer bool - expectedOutput string + name string + envoyMetricsGetter *mockEnvoyMetricsGetter + serviceMetricsGetter *mockServiceMetricsGetter + expectedStatusCode int + expectedOutput string }{ { - name: "happy path: envoy and service metrics are merged", - runEnvoyMetricsServer: true, - runServiceMetricsServer: true, - expectedOutput: "envoy metrics\nservice metrics\n", + name: "happy path: envoy and service metrics are merged", + envoyMetricsGetter: &mockEnvoyMetricsGetter{ + respStatusCode: 200, + }, + serviceMetricsGetter: &mockServiceMetricsGetter{ + respStatusCode: 200, + }, + expectedStatusCode: 200, + expectedOutput: "envoy metrics\nservice metrics\nconsul_merged_service_metrics_success 1\n", }, { - name: "no service metrics", - runEnvoyMetricsServer: true, - runServiceMetricsServer: false, - expectedOutput: "envoy metrics\n", + name: "service metrics non-200", + envoyMetricsGetter: &mockEnvoyMetricsGetter{ + respStatusCode: 200, + }, + serviceMetricsGetter: &mockServiceMetricsGetter{ + respStatusCode: 404, + }, + expectedStatusCode: 200, + expectedOutput: "envoy metrics\nconsul_merged_service_metrics_success 0\n", }, { - name: "no envoy metrics", - runEnvoyMetricsServer: false, - runServiceMetricsServer: true, - expectedOutput: "", + name: "envoy metrics non-200", + envoyMetricsGetter: &mockEnvoyMetricsGetter{ + respStatusCode: 404, + }, + serviceMetricsGetter: &mockServiceMetricsGetter{ + respStatusCode: 200, + }, + expectedStatusCode: 500, + expectedOutput: "Received non-2xx status code scraping Envoy proxy metrics: 404: envoy metrics\n\n", + }, + { + name: "envoy and service metrics non-200", + envoyMetricsGetter: &mockEnvoyMetricsGetter{ + respStatusCode: 500, + }, + serviceMetricsGetter: &mockServiceMetricsGetter{ + respStatusCode: 500, + }, + expectedStatusCode: 500, + expectedOutput: "Received non-2xx status code scraping Envoy proxy metrics: 500: envoy metrics\n\n", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - randomPorts := freeport.MustTake(2) + randomPorts := freeport.GetN(t, 2) ui := cli.NewMockUi() cmd := Command{ UI: ui, @@ -272,21 +310,11 @@ func TestMergedMetricsServer(t *testing.T) { flagServiceMetricsPort: fmt.Sprint(randomPorts[1]), flagServiceMetricsPath: "/metrics", logger: hclog.Default(), + envoyMetricsGetter: c.envoyMetricsGetter, + serviceMetricsGetter: c.serviceMetricsGetter, } server := cmd.createMergedMetricsServer() - - // Override the cmd's envoyMetricsGetter and serviceMetricsGetter - // with stubs. - em := &envoyMetrics{} - sm := &serviceMetrics{} - if c.runEnvoyMetricsServer { - cmd.envoyMetricsGetter = em - } - if c.runServiceMetricsServer { - cmd.serviceMetricsGetter = sm - } - go func() { _ = server.ListenAndServe() }() @@ -304,8 +332,8 @@ func TestMergedMetricsServer(t *testing.T) { // Verify the correct service metrics url was used. The service // metrics endpoint is only called if the Envoy metrics endpoint // call succeeds. - if c.runServiceMetricsServer && c.runEnvoyMetricsServer { - require.Equal(r, fmt.Sprintf("http://127.0.0.1:%d%s", randomPorts[1], "/metrics"), sm.url) + if c.envoyMetricsGetter.respStatusCode == 200 { + require.Equal(r, fmt.Sprintf("http://127.0.0.1:%d%s", randomPorts[1], "/metrics"), c.serviceMetricsGetter.reqURL) } }) }) @@ -457,7 +485,7 @@ func TestRun_ServicesRegistration_ConsulDown(t *testing.T) { // we need to reserve all 6 ports to avoid potential // port collisions with other tests - randomPorts := freeport.MustTake(6) + randomPorts := freeport.GetN(t, 6) // Run async because we need to kill it when the test is over. exitChan := runCommandAsynchronously(&cmd, []string{ diff --git a/control-plane/subcommand/controller/command.go b/control-plane/subcommand/controller/command.go index 939ef86775..389421e830 100644 --- a/control-plane/subcommand/controller/command.go +++ b/control-plane/subcommand/controller/command.go @@ -7,9 +7,11 @@ import ( "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" + "github.com/hashicorp/consul-k8s/control-plane/consul" "github.com/hashicorp/consul-k8s/control-plane/controller" cmdCommon "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" + "github.com/hashicorp/consul/api" "github.com/mitchellh/cli" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" @@ -128,12 +130,24 @@ func (c *Command) Run(args []string) int { return 1 } - consulClient, err := c.httpFlags.APIClient() + cfg := api.DefaultConfig() + c.httpFlags.MergeOntoConfig(cfg) + consulClient, err := consul.NewClient(cfg) if err != nil { setupLog.Error(err, "connecting to Consul agent") return 1 } + partitionsEnabled := c.httpFlags.Partition() != "" + consulMeta := common.ConsulMeta{ + PartitionsEnabled: partitionsEnabled, + Partition: c.httpFlags.Partition(), + NamespacesEnabled: c.flagEnableNamespaces, + DestinationNamespace: c.flagConsulDestinationNamespace, + Mirroring: c.flagEnableNSMirroring, + Prefix: c.flagNSMirroringPrefix, + } + configEntryReconciler := &controller.ConfigEntryController{ ConsulClient: consulClient, DatacenterName: c.flagDatacenter, @@ -179,6 +193,15 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", common.Mesh) return 1 } + if err = (&controller.ExportedServicesController{ + ConfigEntryController: configEntryReconciler, + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controller").WithName(common.ExportedServices), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", common.ExportedServices) + return 1 + } if err = (&controller.ServiceRouterController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), @@ -234,89 +257,72 @@ func (c *Command) Run(args []string) int { // annotation in each webhook file. mgr.GetWebhookServer().Register("/mutate-v1alpha1-servicedefaults", &webhook.Admission{Handler: &v1alpha1.ServiceDefaultsWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceDefaults), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - NSMirroringPrefix: c.flagNSMirroringPrefix, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceDefaults), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-serviceresolver", &webhook.Admission{Handler: &v1alpha1.ServiceResolverWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceResolver), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - NSMirroringPrefix: c.flagNSMirroringPrefix, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceResolver), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-proxydefaults", &webhook.Admission{Handler: &v1alpha1.ProxyDefaultsWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.ProxyDefaults), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.ProxyDefaults), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-mesh", &webhook.Admission{Handler: &v1alpha1.MeshWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.Mesh), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.Mesh), + }}) + mgr.GetWebhookServer().Register("/mutate-v1alpha1-exportedservices", + &webhook.Admission{Handler: &v1alpha1.ExportedServicesWebhook{ + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.ExportedServices), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-servicerouter", &webhook.Admission{Handler: &v1alpha1.ServiceRouterWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceRouter), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - NSMirroringPrefix: c.flagNSMirroringPrefix, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceRouter), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-servicesplitter", &webhook.Admission{Handler: &v1alpha1.ServiceSplitterWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceSplitter), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - NSMirroringPrefix: c.flagNSMirroringPrefix, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceSplitter), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-serviceintentions", &webhook.Admission{Handler: &v1alpha1.ServiceIntentionsWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceIntentions), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - NSMirroringPrefix: c.flagNSMirroringPrefix, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.ServiceIntentions), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-ingressgateway", &webhook.Admission{Handler: &v1alpha1.IngressGatewayWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.IngressGateway), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - NSMirroringPrefix: c.flagNSMirroringPrefix, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.IngressGateway), + ConsulMeta: consulMeta, }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-terminatinggateway", &webhook.Admission{Handler: &v1alpha1.TerminatingGatewayWebhook{ - Client: mgr.GetClient(), - ConsulClient: consulClient, - Logger: ctrl.Log.WithName("webhooks").WithName(common.TerminatingGateway), - EnableConsulNamespaces: c.flagEnableNamespaces, - EnableNSMirroring: c.flagEnableNSMirroring, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - NSMirroringPrefix: c.flagNSMirroringPrefix, + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.TerminatingGateway), + ConsulMeta: consulMeta, }}) } // +kubebuilder:scaffold:builder diff --git a/control-plane/subcommand/create-federation-secret/command.go b/control-plane/subcommand/create-federation-secret/command.go index 794dbb2804..c62d3a0b32 100644 --- a/control-plane/subcommand/create-federation-secret/command.go +++ b/control-plane/subcommand/create-federation-secret/command.go @@ -67,6 +67,7 @@ type Command struct { once sync.Once help string + ctx context.Context } func (c *Command) init() { @@ -117,12 +118,17 @@ func (c *Command) Run(args []string) int { return 1 } + if c.ctx == nil { + c.ctx = context.Background() + } + // The initial secret struct. We will be filling in its data map // as we continue. federationSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-federation", c.flagResourcePrefix), Namespace: c.flagK8sNamespace, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Type: "Opaque", Data: make(map[string][]byte), @@ -239,10 +245,10 @@ func (c *Command) Run(args []string) int { // Now create the Kubernetes secret. logger.Info("Creating/updating Kubernetes secret", "name", federationSecret.ObjectMeta.Name, "ns", c.flagK8sNamespace) - _, err = c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Create(context.TODO(), federationSecret, metav1.CreateOptions{}) + _, err = c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Create(c.ctx, federationSecret, metav1.CreateOptions{}) if k8serrors.IsAlreadyExists(err) { logger.Info("Secret already exists, updating instead") - _, err = c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Update(context.TODO(), federationSecret, metav1.UpdateOptions{}) + _, err = c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Update(c.ctx, federationSecret, metav1.UpdateOptions{}) } if err != nil { @@ -296,7 +302,7 @@ func (c *Command) replicationToken(logger hclog.Logger) ([]byte, error) { // This will run forever but it's running as a Helm hook so Helm will timeout // after a configurable time period. err := backoff.Retry(func() error { - secret, err := c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Get(context.TODO(), secretName, metav1.GetOptions{}) + secret, err := c.k8sClient.CoreV1().Secrets(c.flagK8sNamespace).Get(c.ctx, secretName, metav1.GetOptions{}) if k8serrors.IsNotFound(err) { logger.Warn("secret not yet created, retrying", "secret", secretName, "ns", c.flagK8sNamespace) return errors.New("") diff --git a/control-plane/subcommand/create-federation-secret/command_test.go b/control-plane/subcommand/create-federation-secret/command_test.go index b3adcc6afd..3a28594f18 100644 --- a/control-plane/subcommand/create-federation-secret/command_test.go +++ b/control-plane/subcommand/create-federation-secret/command_test.go @@ -213,7 +213,8 @@ func TestRun_ReplicationTokenMissingExpectedKey(t *testing.T) { context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "prefix-" + common.ACLReplicationTokenName + "-acl-token", + Name: "prefix-" + common.ACLReplicationTokenName + "-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, }, metav1.CreateOptions{}) @@ -391,7 +392,8 @@ func TestRun_ACLs_K8SNamespaces_ResourcePrefixes(tt *testing.T) { context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: c.resourcePrefix + "-acl-replication-acl-token", + Name: c.resourcePrefix + "-acl-replication-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ common.ACLTokenSecretKey: []byte(replicationToken), @@ -786,7 +788,8 @@ func TestRun_ReplicationSecretDelay(t *testing.T) { context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "prefix-" + common.ACLReplicationTokenName + "-acl-token", + Name: "prefix-" + common.ACLReplicationTokenName + "-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ common.ACLTokenSecretKey: []byte(replicationToken), @@ -941,7 +944,7 @@ func TestRun_ConsulClientDelay(t *testing.T) { // We need to reserve all 6 ports to avoid potential // port collisions with other tests. - randomPorts := freeport.MustTake(6) + randomPorts := freeport.GetN(t, 6) caFile, certFile, keyFile := test.GenerateServerCerts(t) // Create fake k8s. diff --git a/control-plane/subcommand/delete-completed-job/command.go b/control-plane/subcommand/delete-completed-job/command.go index 23f8c22776..273e621a2a 100644 --- a/control-plane/subcommand/delete-completed-job/command.go +++ b/control-plane/subcommand/delete-completed-job/command.go @@ -35,6 +35,8 @@ type Command struct { // retryDuration is how often we'll retry deletion. retryDuration time.Duration + + ctx context.Context } func (c *Command) init() { @@ -82,9 +84,13 @@ func (c *Command) Run(args []string) int { c.UI.Error(fmt.Sprintf("%q is not a valid timeout: %s", c.flagTimeout, err)) return 1 } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - // The context will only ever be intentionally ended by the timeout. - defer cancel() + + if c.ctx == nil { + var cancel context.CancelFunc + c.ctx, cancel = context.WithTimeout(context.Background(), timeout) + // The context will only ever be intentionally ended by the timeout. + defer cancel() + } // c.k8sclient might already be set in a test. if c.k8sClient == nil { @@ -110,7 +116,7 @@ func (c *Command) Run(args []string) int { // Wait for job to complete. logger.Info(fmt.Sprintf("waiting for job %q to complete successfully", jobName)) for { - job, err := c.k8sClient.BatchV1().Jobs(c.flagNamespace).Get(context.TODO(), jobName, metav1.GetOptions{}) + job, err := c.k8sClient.BatchV1().Jobs(c.flagNamespace).Get(c.ctx, jobName, metav1.GetOptions{}) if k8serrors.IsNotFound(err) { logger.Info(fmt.Sprintf("job %q does not exist, no need to delete", jobName)) return 0 @@ -139,7 +145,7 @@ func (c *Command) Run(args []string) int { select { case <-time.After(c.retryDuration): continue - case <-ctx.Done(): + case <-c.ctx.Done(): logger.Warn(fmt.Sprintf("timeout %q has been reached, exiting without deleting job", timeout)) return 1 } @@ -149,7 +155,7 @@ func (c *Command) Run(args []string) int { // ourselves. logger.Info(fmt.Sprintf("job %q has succeeded, deleting", jobName)) propagationPolicy := metav1.DeletePropagationForeground - err = c.k8sClient.BatchV1().Jobs(c.flagNamespace).Delete(context.TODO(), jobName, metav1.DeleteOptions{ + err = c.k8sClient.BatchV1().Jobs(c.flagNamespace).Delete(c.ctx, jobName, metav1.DeleteOptions{ // Needed so that the underlying pods are also deleted. PropagationPolicy: &propagationPolicy, }) diff --git a/control-plane/subcommand/flags/flag_map_value.go b/control-plane/subcommand/flags/flag_map_value.go index cb10c8ecc4..9ddb4dad08 100644 --- a/control-plane/subcommand/flags/flag_map_value.go +++ b/control-plane/subcommand/flags/flag_map_value.go @@ -9,7 +9,7 @@ import ( // Taken from https://github.com/hashicorp/consul/blob/35daee45bc3bf9fdce5845f2219576e861b23f40/command/flags/flag_map_value.go // This was done so we don't depend on internal Consul implementation. -// Ensure implements +// Ensure implements. var _ flag.Value = (*FlagMapValue)(nil) // FlagMapValue is a flag implementation used to provide key=value semantics diff --git a/control-plane/subcommand/flags/http.go b/control-plane/subcommand/flags/http.go index 8830ad5e00..090ef3bad1 100644 --- a/control-plane/subcommand/flags/http.go +++ b/control-plane/subcommand/flags/http.go @@ -23,6 +23,7 @@ type HTTPFlags struct { certFile StringValue keyFile StringValue tlsServerName StringValue + partition StringValue } func (f *HTTPFlags) Flags() *flag.FlagSet { @@ -56,6 +57,8 @@ func (f *HTTPFlags) Flags() *flag.FlagSet { fs.Var(&f.tlsServerName, "tls-server-name", "The server name to use as the SNI host when connecting via TLS. This "+ "can also be specified via the CONSUL_TLS_SERVER_NAME environment variable.") + fs.Var(&f.partition, "partition", + "[Enterprise Only] Name of the Consul Admin Partition to query. Default to \"default\" if Admin Partitions are enabled.") return fs } @@ -93,6 +96,10 @@ func (f *HTTPFlags) ReadTokenFile() (string, error) { return strings.TrimSpace(string(data)), nil } +func (f *HTTPFlags) Partition() string { + return f.partition.String() +} + func (f *HTTPFlags) APIClient() (*api.Client, error) { c := api.DefaultConfig() @@ -110,6 +117,7 @@ func (f *HTTPFlags) MergeOntoConfig(c *api.Config) { f.certFile.Merge(&c.TLSConfig.CertFile) f.keyFile.Merge(&c.TLSConfig.KeyFile) f.tlsServerName.Merge(&c.TLSConfig.Address) + f.partition.Merge(&c.Partition) } func Merge(dst, src *flag.FlagSet) { diff --git a/control-plane/subcommand/flags/usage_test.go b/control-plane/subcommand/flags/usage_test.go index 0b9c4793c9..2cf18ed166 100644 --- a/control-plane/subcommand/flags/usage_test.go +++ b/control-plane/subcommand/flags/usage_test.go @@ -47,6 +47,10 @@ HTTP API Options can also be set to HTTPS by setting the environment variable CONSUL_HTTP_SSL=true. + -partition= + [Enterprise Only] Name of the Consul Admin Partition to query. + Default to "default" if Admin Partitions are enabled. + -tls-server-name= The server name to use as the SNI host when connecting via TLS. This can also be specified via the CONSUL_TLS_SERVER_NAME diff --git a/control-plane/subcommand/get-consul-client-ca/command_test.go b/control-plane/subcommand/get-consul-client-ca/command_test.go index 2f9fb4de43..4250e6d81f 100644 --- a/control-plane/subcommand/get-consul-client-ca/command_test.go +++ b/control-plane/subcommand/get-consul-client-ca/command_test.go @@ -64,7 +64,7 @@ func TestRun_FlagsValidation(t *testing.T) { // Test that in the happy case scenario // we retrieve the CA from Consul and -// write it to a file +// write it to a file. func TestRun(t *testing.T) { t.Parallel() outputFile, err := ioutil.TempFile("", "ca") @@ -139,7 +139,7 @@ func TestRun_ConsulServerAvailableLater(t *testing.T) { UI: ui, } - randomPorts := freeport.MustTake(6) + randomPorts := freeport.GetN(t, 6) // Start the consul agent asynchronously var a *testutil.TestServer @@ -293,7 +293,7 @@ func TestRun_GetsOnlyActiveRoot(t *testing.T) { } // Test that when using cloud auto-join -// it uses the provider to get the address of the server +// it uses the provider to get the address of the server. func TestRun_WithProvider(t *testing.T) { t.Parallel() outputFile, err := ioutil.TempFile("", "ca") diff --git a/control-plane/subcommand/gossip-encryption-autogenerate/command.go b/control-plane/subcommand/gossip-encryption-autogenerate/command.go new file mode 100644 index 0000000000..3668a20c35 --- /dev/null +++ b/control-plane/subcommand/gossip-encryption-autogenerate/command.go @@ -0,0 +1,212 @@ +package gossipencryptionautogenerate + +import ( + "context" + "crypto/rand" + "encoding/base64" + "flag" + "fmt" + "sync" + + "github.com/hashicorp/consul-k8s/control-plane/subcommand" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" + "github.com/hashicorp/go-hclog" + "github.com/mitchellh/cli" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type Command struct { + UI cli.Ui + + flags *flag.FlagSet + k8s *flags.K8SFlags + + // These flags determine where the Kubernetes secret will be stored. + flagNamespace string + flagSecretName string + flagSecretKey string + + flagLogLevel string + flagLogJSON bool + + k8sClient kubernetes.Interface + + log hclog.Logger + once sync.Once + ctx context.Context + help string +} + +// init is run once to set up usage documentation for flags. +func (c *Command) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.flagLogLevel, "log-level", "info", + "Log verbosity level. Supported values (in order of detail) are \"trace\", "+ + "\"debug\", \"info\", \"warn\", and \"error\".") + c.flags.BoolVar(&c.flagLogJSON, "log-json", false, "Enable or disable JSON output format for logging.") + c.flags.StringVar(&c.flagNamespace, "namespace", "", "Name of Kubernetes namespace where Consul and consul-k8s components are deployed.") + c.flags.StringVar(&c.flagSecretName, "secret-name", "", "Name of the secret to create.") + c.flags.StringVar(&c.flagSecretKey, "secret-key", "key", "Name of the secret key to create.") + + c.k8s = &flags.K8SFlags{} + flags.Merge(c.flags, c.k8s.Flags()) + + c.help = flags.Usage(help, c.flags) +} + +// Run parses input and creates a gossip secret in Kubernetes if none exists at the given namespace and secret name. +func (c *Command) Run(args []string) int { + c.once.Do(c.init) + + if err := c.flags.Parse(args); err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) + return 1 + } + + if err := c.validateFlags(); err != nil { + c.UI.Error(fmt.Sprintf("Failed to validate flags: %v", err)) + return 1 + } + + var err error + c.log, err = common.Logger(c.flagLogLevel, c.flagLogJSON) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if c.ctx == nil { + c.ctx = context.Background() + } + + if c.k8sClient == nil { + if err = c.createKubernetesClient(); err != nil { + c.UI.Error(fmt.Sprintf("Failed to create Kubernetes client: %v", err)) + return 1 + } + } + + if exists, err := c.doesKubernetesSecretExist(); err != nil { + c.UI.Error(fmt.Sprintf("Failed to check if Kubernetes secret exists: %v", err)) + return 1 + } else if exists { + // Safe exit if secret already exists. + c.UI.Info(fmt.Sprintf("A Kubernetes secret with the name `%s` already exists.", c.flagSecretName)) + return 0 + } + + gossipSecret, err := generateGossipSecret() + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to generate gossip secret: %v", err)) + return 1 + } + + // Create the Kubernetes secret object. + kubernetesSecret := v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.flagSecretName, + Namespace: c.flagNamespace, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + Data: map[string][]byte{ + c.flagSecretKey: []byte(gossipSecret), + }, + } + + // Write the secret to Kubernetes. + _, err = c.k8sClient.CoreV1().Secrets(c.flagNamespace).Create(c.ctx, &kubernetesSecret, metav1.CreateOptions{}) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to create Kubernetes secret: %v", err)) + return 1 + } + + c.UI.Info(fmt.Sprintf("Successfully created Kubernetes secret `%s` in namespace `%s`.", c.flagSecretName, c.flagNamespace)) + return 0 +} + +// Help returns the command's help text. +func (c *Command) Help() string { + c.once.Do(c.init) + return c.help +} + +// Synopsis returns a one-line synopsis of the command. +func (c *Command) Synopsis() string { + return synopsis +} + +// validateFlags ensures that all required flags are set. +func (c *Command) validateFlags() error { + if c.flagNamespace == "" { + return fmt.Errorf("-namespace must be set") + } + + if c.flagSecretName == "" { + return fmt.Errorf("-secret-name must be set") + } + + return nil +} + +// createKubernetesClient creates a Kubernetes client on the command object. +func (c *Command) createKubernetesClient() error { + config, err := subcommand.K8SConfig(c.k8s.KubeConfig()) + if err != nil { + return fmt.Errorf("failed to create Kubernetes config: %v", err) + } + + c.k8sClient, err = kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("error initializing Kubernetes client: %s", err) + } + + return nil +} + +// doesKubernetesSecretExist checks if a secret with the given name exists in the given namespace. +func (c *Command) doesKubernetesSecretExist() (bool, error) { + _, err := c.k8sClient.CoreV1().Secrets(c.flagNamespace).Get(c.ctx, c.flagSecretName, metav1.GetOptions{}) + + // If the secret does not exist, the error will be a NotFound error. + if err != nil && apierrors.IsNotFound(err) { + return false, nil + } + + // If the error is not a NotFound error, return the error. + if err != nil && !apierrors.IsNotFound(err) { + return false, fmt.Errorf("failed to get Kubernetes secret: %v", err) + } + + // The secret exists. + return true, nil +} + +// generateGossipSecret generates a random 32 byte secret returned as a base64 encoded string. +func generateGossipSecret() (string, error) { + // This code was copied from Consul's Keygen command: + // https://github.com/hashicorp/consul/blob/d652cc86e3d0322102c2b5e9026c6a60f36c17a5/command/keygen/keygen.go + + key := make([]byte, 32) + n, err := rand.Reader.Read(key) + + if err != nil { + return "", fmt.Errorf("error reading random data: %s", err) + } + if n != 32 { + return "", fmt.Errorf("couldn't read enough entropy") + } + + return base64.StdEncoding.EncodeToString(key), nil +} + +const synopsis = "Generate and store a secret for gossip encryption." +const help = ` +Usage: consul-k8s-control-plane gossip-encryption-autogenerate [options] + + Bootstraps the installation with a secret for gossip encryption. +` diff --git a/control-plane/subcommand/gossip-encryption-autogenerate/command_test.go b/control-plane/subcommand/gossip-encryption-autogenerate/command_test.go new file mode 100644 index 0000000000..91d7101232 --- /dev/null +++ b/control-plane/subcommand/gossip-encryption-autogenerate/command_test.go @@ -0,0 +1,103 @@ +package gossipencryptionautogenerate + +import ( + "context" + "encoding/base64" + "fmt" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestRun_FlagValidation(t *testing.T) { + t.Parallel() + cases := []struct { + flags []string + expErr string + }{ + { + flags: []string{}, + expErr: "-namespace must be set", + }, + { + flags: []string{"-namespace", "default"}, + expErr: "-secret-name must be set", + }, + { + flags: []string{"-namespace", "default", "-secret-name", "my-secret", "-log-level", "oak"}, + expErr: "unknown log level", + }, + } + + for _, c := range cases { + t.Run(c.expErr, func(t *testing.T) { + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + code := cmd.Run(c.flags) + require.Equal(t, 1, code) + require.Contains(t, ui.ErrorWriter.String(), c.expErr) + }) + } +} + +func TestRun_EarlyTerminationWithSuccessCodeIfSecretExists(t *testing.T) { + namespace := "default" + secretName := "my-secret" + secretKey := "my-secret-key" + + ui := cli.NewMockUi() + k8s := fake.NewSimpleClientset() + + cmd := Command{UI: ui, k8sClient: k8s} + + // Create a secret. + secret := v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + secretKey: []byte(secretKey), + }, + } + _, err := k8s.CoreV1().Secrets(namespace).Create(context.Background(), &secret, metav1.CreateOptions{}) + require.NoError(t, err) + + // Run the command. + flags := []string{"-namespace", namespace, "-secret-name", secretName, "-secret-key", secretKey} + code := cmd.Run(flags) + + require.Equal(t, 0, code) + require.Contains(t, ui.OutputWriter.String(), fmt.Sprintf("A Kubernetes secret with the name `%s` already exists.", secretName)) +} + +func TestRun_SecretIsGeneratedIfNoneExists(t *testing.T) { + namespace := "default" + secretName := "my-secret" + secretKey := "my-secret-key" + + ui := cli.NewMockUi() + k8s := fake.NewSimpleClientset() + + cmd := Command{UI: ui, k8sClient: k8s} + + // Run the command. + flags := []string{"-namespace", namespace, "-secret-name", secretName, "-secret-key", secretKey} + code := cmd.Run(flags) + + require.Equal(t, 0, code) + require.Contains(t, ui.OutputWriter.String(), fmt.Sprintf("Successfully created Kubernetes secret `%s` in namespace `%s`.", secretName, namespace)) + + // Check the secret was created. + secret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{}) + require.NoError(t, err) + gossipSecret, err := base64.StdEncoding.DecodeString(string(secret.Data[secretKey])) + require.NoError(t, err) + require.Len(t, gossipSecret, 32) +} diff --git a/control-plane/subcommand/inject-connect/command.go b/control-plane/subcommand/inject-connect/command.go index 530bbdae98..1d4c88a069 100644 --- a/control-plane/subcommand/inject-connect/command.go +++ b/control-plane/subcommand/inject-connect/command.go @@ -50,6 +50,8 @@ type Command struct { flagAllowK8sNamespacesList []string // K8s namespaces to explicitly inject flagDenyK8sNamespacesList []string // K8s namespaces to deny injection (has precedence) + flagEnablePartitions bool // Use Admin Partitions on all components + // Flags to support Consul namespaces flagEnableNamespaces bool // Use namespacing on all components flagConsulDestinationNamespace string // Consul namespace to register everything if not mirroring @@ -75,10 +77,10 @@ type Command struct { flagDefaultPrometheusScrapePath string // Consul sidecar resource settings. - flagConsulSidecarCPULimit string - flagConsulSidecarCPURequest string - flagConsulSidecarMemoryLimit string - flagConsulSidecarMemoryRequest string + flagDefaultConsulSidecarCPULimit string + flagDefaultConsulSidecarCPURequest string + flagDefaultConsulSidecarMemoryLimit string + flagDefaultConsulSidecarMemoryRequest string // Init container resource settings. flagInitContainerCPULimit string @@ -90,6 +92,10 @@ type Command struct { flagDefaultEnableTransparentProxy bool flagTransparentProxyDefaultOverwriteProbes bool + // Consul DNS flags. + flagEnableConsulDNS bool + flagResourcePrefix string + flagEnableOpenShift bool flagSet *flag.FlagSet @@ -141,6 +147,8 @@ func (c *Command) init() { "K8s namespaces to explicitly deny. Takes precedence over allow. May be specified multiple times.") c.flagSet.StringVar(&c.flagReleaseName, "release-name", "consul", "The Consul Helm installation release name, e.g 'helm install '") c.flagSet.StringVar(&c.flagReleaseNamespace, "release-namespace", "default", "The Consul Helm installation namespace, e.g 'helm install --namespace '") + c.flagSet.BoolVar(&c.flagEnablePartitions, "enable-partitions", false, + "[Enterprise Only] Enables Admin Partitions.") c.flagSet.BoolVar(&c.flagEnableNamespaces, "enable-namespaces", false, "[Enterprise Only] Enables namespaces, in either a single Consul namespace or mirrored.") c.flagSet.StringVar(&c.flagConsulDestinationNamespace, "consul-destination-namespace", "default", @@ -157,6 +165,10 @@ func (c *Command) init() { "Enable transparent proxy mode for all Consul service mesh applications by default.") c.flagSet.BoolVar(&c.flagTransparentProxyDefaultOverwriteProbes, "transparent-proxy-default-overwrite-probes", true, "Overwrite Kubernetes probes to point to Envoy by default when in Transparent Proxy mode.") + c.flagSet.BoolVar(&c.flagEnableConsulDNS, "enable-consul-dns", false, + "Enables Consul DNS lookup for services in the mesh.") + c.flagSet.StringVar(&c.flagResourcePrefix, "resource-prefix", "", + "Release prefix of the Consul installation used to determine Consul DNS Service name.") c.flagSet.BoolVar(&c.flagEnableOpenShift, "enable-openshift", false, "Indicates that the command runs in an OpenShift cluster.") c.flagSet.StringVar(&c.flagLogLevel, "log-level", zapcore.InfoLevel.String(), @@ -185,10 +197,10 @@ func (c *Command) init() { c.flagSet.StringVar(&c.flagInitContainerMemoryLimit, "init-container-memory-limit", "150Mi", "Init container memory limit.") // Consul sidecar resource setting flags. - c.flagSet.StringVar(&c.flagConsulSidecarCPURequest, "consul-sidecar-cpu-request", "20m", "Consul sidecar CPU request.") - c.flagSet.StringVar(&c.flagConsulSidecarCPULimit, "consul-sidecar-cpu-limit", "20m", "Consul sidecar CPU limit.") - c.flagSet.StringVar(&c.flagConsulSidecarMemoryRequest, "consul-sidecar-memory-request", "25Mi", "Consul sidecar memory request.") - c.flagSet.StringVar(&c.flagConsulSidecarMemoryLimit, "consul-sidecar-memory-limit", "50Mi", "Consul sidecar memory limit.") + c.flagSet.StringVar(&c.flagDefaultConsulSidecarCPURequest, "default-consul-sidecar-cpu-request", "20m", "Default consul sidecar CPU request.") + c.flagSet.StringVar(&c.flagDefaultConsulSidecarCPULimit, "default-consul-sidecar-cpu-limit", "20m", "Default consul sidecar CPU limit.") + c.flagSet.StringVar(&c.flagDefaultConsulSidecarMemoryRequest, "default-consul-sidecar-memory-request", "25Mi", "Default consul sidecar memory request.") + c.flagSet.StringVar(&c.flagDefaultConsulSidecarMemoryLimit, "default-consul-sidecar-memory-limit", "50Mi", "Default consul sidecar memory limit.") c.http = &flags.HTTPFlags{} @@ -228,6 +240,16 @@ func (c *Command) Run(args []string) int { return 1 } + if c.flagEnablePartitions && c.http.Partition() == "" { + c.UI.Error("-partition-name must set if -enable-partitions is set to 'true'") + return 1 + } + + if c.http.Partition() != "" && !c.flagEnablePartitions { + c.UI.Error("-enable-partitions must be set to 'true' if -partition-name is set") + return 1 + } + // Proxy resources. var sidecarProxyCPULimit, sidecarProxyCPURequest, sidecarProxyMemoryLimit, sidecarProxyMemoryRequest resource.Quantity var err error @@ -403,6 +425,7 @@ func (c *Command) Run(args []string) int { DenyK8sNamespacesSet: denyK8sNamespaces, MetricsConfig: metricsConfig, ConsulClientCfg: cfg, + EnableConsulPartitions: c.flagEnablePartitions, EnableConsulNamespaces: c.flagEnableNamespaces, ConsulDestinationNamespace: c.flagConsulDestinationNamespace, EnableNSMirroring: c.flagEnableK8SNSMirroring, @@ -430,35 +453,38 @@ func (c *Command) Run(args []string) int { mgr.GetWebhookServer().Register("/mutate", &webhook.Admission{Handler: &connectinject.Handler{ - Clientset: c.clientset, - ConsulClient: c.consulClient, - ImageConsul: c.flagConsulImage, - ImageEnvoy: c.flagEnvoyImage, - EnvoyExtraArgs: c.flagEnvoyExtraArgs, - ImageConsulK8S: c.flagConsulK8sImage, - RequireAnnotation: !c.flagDefaultInject, - AuthMethod: c.flagACLAuthMethod, - ConsulCACert: string(consulCACert), - DefaultProxyCPURequest: sidecarProxyCPURequest, - DefaultProxyCPULimit: sidecarProxyCPULimit, - DefaultProxyMemoryRequest: sidecarProxyMemoryRequest, - DefaultProxyMemoryLimit: sidecarProxyMemoryLimit, - MetricsConfig: metricsConfig, - InitContainerResources: initResources, - ConsulSidecarResources: consulSidecarResources, - AllowK8sNamespacesSet: allowK8sNamespaces, - DenyK8sNamespacesSet: denyK8sNamespaces, - EnableNamespaces: c.flagEnableNamespaces, - ConsulDestinationNamespace: c.flagConsulDestinationNamespace, - EnableK8SNSMirroring: c.flagEnableK8SNSMirroring, - K8SNSMirroringPrefix: c.flagK8SNSMirroringPrefix, - CrossNamespaceACLPolicy: c.flagCrossNamespaceACLPolicy, - EnableTransparentProxy: c.flagDefaultEnableTransparentProxy, - TProxyOverwriteProbes: c.flagTransparentProxyDefaultOverwriteProbes, - EnableOpenShift: c.flagEnableOpenShift, - Log: ctrl.Log.WithName("handler").WithName("connect"), - LogLevel: c.flagLogLevel, - LogJSON: c.flagLogJSON, + Clientset: c.clientset, + ConsulClient: c.consulClient, + ImageConsul: c.flagConsulImage, + ImageEnvoy: c.flagEnvoyImage, + EnvoyExtraArgs: c.flagEnvoyExtraArgs, + ImageConsulK8S: c.flagConsulK8sImage, + RequireAnnotation: !c.flagDefaultInject, + AuthMethod: c.flagACLAuthMethod, + ConsulCACert: string(consulCACert), + DefaultProxyCPURequest: sidecarProxyCPURequest, + DefaultProxyCPULimit: sidecarProxyCPULimit, + DefaultProxyMemoryRequest: sidecarProxyMemoryRequest, + DefaultProxyMemoryLimit: sidecarProxyMemoryLimit, + MetricsConfig: metricsConfig, + InitContainerResources: initResources, + DefaultConsulSidecarResources: consulSidecarResources, + ConsulPartition: c.http.Partition(), + AllowK8sNamespacesSet: allowK8sNamespaces, + DenyK8sNamespacesSet: denyK8sNamespaces, + EnableNamespaces: c.flagEnableNamespaces, + ConsulDestinationNamespace: c.flagConsulDestinationNamespace, + EnableK8SNSMirroring: c.flagEnableK8SNSMirroring, + K8SNSMirroringPrefix: c.flagK8SNSMirroringPrefix, + CrossNamespaceACLPolicy: c.flagCrossNamespaceACLPolicy, + EnableTransparentProxy: c.flagDefaultEnableTransparentProxy, + TProxyOverwriteProbes: c.flagTransparentProxyDefaultOverwriteProbes, + EnableConsulDNS: c.flagEnableConsulDNS, + ResourcePrefix: c.flagResourcePrefix, + EnableOpenShift: c.flagEnableOpenShift, + Log: ctrl.Log.WithName("handler").WithName("connect"), + LogLevel: c.flagLogLevel, + LogJSON: c.flagLogJSON, }}) if err := mgr.Start(ctx); err != nil { @@ -522,36 +548,36 @@ func (c *Command) parseAndValidateResourceFlags() (corev1.ResourceRequirements, var consulSidecarCPULimit, consulSidecarCPURequest, consulSidecarMemoryLimit, consulSidecarMemoryRequest resource.Quantity // Parse and validate the Consul sidecar resources - consulSidecarCPURequest, err = resource.ParseQuantity(c.flagConsulSidecarCPURequest) + consulSidecarCPURequest, err = resource.ParseQuantity(c.flagDefaultConsulSidecarCPURequest) if err != nil { return corev1.ResourceRequirements{}, corev1.ResourceRequirements{}, - fmt.Errorf("-consul-sidecar-cpu-request '%s' is invalid: %s", c.flagConsulSidecarCPURequest, err) + fmt.Errorf("-default-consul-sidecar-cpu-request '%s' is invalid: %s", c.flagDefaultConsulSidecarCPURequest, err) } - consulSidecarCPULimit, err = resource.ParseQuantity(c.flagConsulSidecarCPULimit) + consulSidecarCPULimit, err = resource.ParseQuantity(c.flagDefaultConsulSidecarCPULimit) if err != nil { return corev1.ResourceRequirements{}, corev1.ResourceRequirements{}, - fmt.Errorf("-consul-sidecar-cpu-limit '%s' is invalid: %s", c.flagConsulSidecarCPULimit, err) + fmt.Errorf("-default-consul-sidecar-cpu-limit '%s' is invalid: %s", c.flagDefaultConsulSidecarCPULimit, err) } if consulSidecarCPULimit.Value() != 0 && consulSidecarCPURequest.Cmp(consulSidecarCPULimit) > 0 { return corev1.ResourceRequirements{}, corev1.ResourceRequirements{}, fmt.Errorf( - "request must be <= limit: -consul-sidecar-cpu-request value of %q is greater than the -consul-sidecar-cpu-limit value of %q", - c.flagConsulSidecarCPURequest, c.flagConsulSidecarCPULimit) + "request must be <= limit: -default-consul-sidecar-cpu-request value of %q is greater than the -default-consul-sidecar-cpu-limit value of %q", + c.flagDefaultConsulSidecarCPURequest, c.flagDefaultConsulSidecarCPULimit) } - consulSidecarMemoryRequest, err = resource.ParseQuantity(c.flagConsulSidecarMemoryRequest) + consulSidecarMemoryRequest, err = resource.ParseQuantity(c.flagDefaultConsulSidecarMemoryRequest) if err != nil { return corev1.ResourceRequirements{}, corev1.ResourceRequirements{}, - fmt.Errorf("-consul-sidecar-memory-request '%s' is invalid: %s", c.flagConsulSidecarMemoryRequest, err) + fmt.Errorf("-default-consul-sidecar-memory-request '%s' is invalid: %s", c.flagDefaultConsulSidecarMemoryRequest, err) } - consulSidecarMemoryLimit, err = resource.ParseQuantity(c.flagConsulSidecarMemoryLimit) + consulSidecarMemoryLimit, err = resource.ParseQuantity(c.flagDefaultConsulSidecarMemoryLimit) if err != nil { return corev1.ResourceRequirements{}, corev1.ResourceRequirements{}, - fmt.Errorf("-consul-sidecar-memory-limit '%s' is invalid: %s", c.flagConsulSidecarMemoryLimit, err) + fmt.Errorf("-default-consul-sidecar-memory-limit '%s' is invalid: %s", c.flagDefaultConsulSidecarMemoryLimit, err) } if consulSidecarMemoryLimit.Value() != 0 && consulSidecarMemoryRequest.Cmp(consulSidecarMemoryLimit) > 0 { return corev1.ResourceRequirements{}, corev1.ResourceRequirements{}, fmt.Errorf( - "request must be <= limit: -consul-sidecar-memory-request value of %q is greater than the -consul-sidecar-memory-limit value of %q", - c.flagConsulSidecarMemoryRequest, c.flagConsulSidecarMemoryLimit) + "request must be <= limit: -default-consul-sidecar-memory-request value of %q is greater than the -default-consul-sidecar-memory-limit value of %q", + c.flagDefaultConsulSidecarMemoryRequest, c.flagDefaultConsulSidecarMemoryLimit) } // Put into corev1.ResourceRequirements form diff --git a/control-plane/subcommand/inject-connect/command_test.go b/control-plane/subcommand/inject-connect/command_test.go index 5273d10313..3a2ef4642b 100644 --- a/control-plane/subcommand/inject-connect/command_test.go +++ b/control-plane/subcommand/inject-connect/command_test.go @@ -47,6 +47,16 @@ func TestRun_FlagValidation(t *testing.T) { "-ca-file", "bar"}, expErr: "error reading Consul's CA cert file \"bar\"", }, + { + flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", + "-enable-partitions", "true"}, + expErr: "-partition-name must set if -enable-partitions is set to 'true'", + }, + { + flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", + "-partition", "default"}, + expErr: "-enable-partitions must be set to 'true' if -partition-name is set", + }, { flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", "-default-sidecar-proxy-cpu-limit=unparseable"}, @@ -117,37 +127,37 @@ func TestRun_FlagValidation(t *testing.T) { }, { flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", - "-consul-sidecar-cpu-limit=unparseable"}, - expErr: "-consul-sidecar-cpu-limit 'unparseable' is invalid", + "-default-consul-sidecar-cpu-limit=unparseable"}, + expErr: "-default-consul-sidecar-cpu-limit 'unparseable' is invalid", }, { flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", - "-consul-sidecar-cpu-request=unparseable"}, - expErr: "-consul-sidecar-cpu-request 'unparseable' is invalid", + "-default-consul-sidecar-cpu-request=unparseable"}, + expErr: "-default-consul-sidecar-cpu-request 'unparseable' is invalid", }, { flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", - "-consul-sidecar-memory-limit=unparseable"}, - expErr: "-consul-sidecar-memory-limit 'unparseable' is invalid", + "-default-consul-sidecar-memory-limit=unparseable"}, + expErr: "-default-consul-sidecar-memory-limit 'unparseable' is invalid", }, { flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", - "-consul-sidecar-memory-request=unparseable"}, - expErr: "-consul-sidecar-memory-request 'unparseable' is invalid", + "-default-consul-sidecar-memory-request=unparseable"}, + expErr: "-default-consul-sidecar-memory-request 'unparseable' is invalid", }, { flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", - "-consul-sidecar-memory-request=50Mi", - "-consul-sidecar-memory-limit=25Mi", + "-default-consul-sidecar-memory-request=50Mi", + "-default-consul-sidecar-memory-limit=25Mi", }, - expErr: "request must be <= limit: -consul-sidecar-memory-request value of \"50Mi\" is greater than the -consul-sidecar-memory-limit value of \"25Mi\"", + expErr: "request must be <= limit: -default-consul-sidecar-memory-request value of \"50Mi\" is greater than the -default-consul-sidecar-memory-limit value of \"25Mi\"", }, { flags: []string{"-consul-k8s-image", "foo", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", - "-consul-sidecar-cpu-request=50m", - "-consul-sidecar-cpu-limit=25m", + "-default-consul-sidecar-cpu-request=50m", + "-default-consul-sidecar-cpu-limit=25m", }, - expErr: "request must be <= limit: -consul-sidecar-cpu-request value of \"50m\" is greater than the -consul-sidecar-cpu-limit value of \"25m\"", + expErr: "request must be <= limit: -default-consul-sidecar-cpu-request value of \"50m\" is greater than the -default-consul-sidecar-cpu-limit value of \"25m\"", }, { flags: []string{"-consul-k8s-image", "hashicorp/consul-k8s", "-consul-image", "foo", "-envoy-image", "envoy:1.16.0", @@ -189,10 +199,10 @@ func TestRun_ResourceLimitDefaults(t *testing.T) { require.Equal(t, cmd.flagInitContainerMemoryLimit, "150Mi") // Consul sidecar container defaults - require.Equal(t, cmd.flagConsulSidecarCPURequest, "20m") - require.Equal(t, cmd.flagConsulSidecarCPULimit, "20m") - require.Equal(t, cmd.flagConsulSidecarMemoryRequest, "25Mi") - require.Equal(t, cmd.flagConsulSidecarMemoryLimit, "50Mi") + require.Equal(t, cmd.flagDefaultConsulSidecarCPURequest, "20m") + require.Equal(t, cmd.flagDefaultConsulSidecarCPULimit, "20m") + require.Equal(t, cmd.flagDefaultConsulSidecarMemoryRequest, "25Mi") + require.Equal(t, cmd.flagDefaultConsulSidecarMemoryLimit, "50Mi") } func TestRun_ValidationConsulHTTPAddr(t *testing.T) { diff --git a/control-plane/subcommand/partition-init/command.go b/control-plane/subcommand/partition-init/command.go new file mode 100644 index 0000000000..8846d42efc --- /dev/null +++ b/control-plane/subcommand/partition-init/command.go @@ -0,0 +1,201 @@ +package partition_init + +import ( + "context" + "errors" + "flag" + "fmt" + "sync" + "time" + + "github.com/hashicorp/consul-k8s/control-plane/consul" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" + k8sflags "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/go-discover" + "github.com/hashicorp/go-hclog" + "github.com/mitchellh/cli" +) + +type Command struct { + UI cli.Ui + + flags *flag.FlagSet + k8s *k8sflags.K8SFlags + http *flags.HTTPFlags + + flagPartitionName string + + // Flags to configure Consul connection + flagServerAddresses []string + flagServerPort uint + flagConsulCACert string + flagConsulTLSServerName string + flagUseHTTPS bool + + flagLogLevel string + flagLogJSON bool + flagTimeout time.Duration + + // ctx is cancelled when the command timeout is reached. + ctx context.Context + retryDuration time.Duration + + // log + log hclog.Logger + + once sync.Once + help string + + providers map[string]discover.Provider +} + +func (c *Command) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.flagPartitionName, "partition-name", "", "The name of the partition being created.") + + c.flags.Var((*flags.AppendSliceValue)(&c.flagServerAddresses), "server-address", + "The IP, DNS name or the cloud auto-join string of the Consul server(s). If providing IPs or DNS names, may be specified multiple times. "+ + "At least one value is required.") + c.flags.UintVar(&c.flagServerPort, "server-port", 8500, "The HTTP or HTTPS port of the Consul server. Defaults to 8500.") + c.flags.StringVar(&c.flagConsulCACert, "consul-ca-cert", "", + "Path to the PEM-encoded CA certificate of the Consul cluster.") + c.flags.StringVar(&c.flagConsulTLSServerName, "consul-tls-server-name", "", + "The server name to set as the SNI header when sending HTTPS requests to Consul.") + c.flags.BoolVar(&c.flagUseHTTPS, "use-https", false, + "Toggle for using HTTPS for all API calls to Consul.") + c.flags.DurationVar(&c.flagTimeout, "timeout", 10*time.Minute, + "How long we'll try to bootstrap Partitions for before timing out, e.g. 1ms, 2s, 3m") + c.flags.StringVar(&c.flagLogLevel, "log-level", "info", + "Log verbosity level. Supported values (in order of detail) are \"trace\", "+ + "\"debug\", \"info\", \"warn\", and \"error\".") + c.flags.BoolVar(&c.flagLogJSON, "log-json", false, + "Enable or disable JSON output format for logging.") + + c.k8s = &k8sflags.K8SFlags{} + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.k8s.Flags()) + flags.Merge(c.flags, c.http.Flags()) + c.help = flags.Usage(help, c.flags) + + // Default retry to 1s. This is exposed for setting in tests. + if c.retryDuration == 0 { + c.retryDuration = 1 * time.Second + } +} + +func (c *Command) Synopsis() string { return synopsis } + +func (c *Command) Help() string { + c.once.Do(c.init) + return c.help +} + +// Run bootstraps Admin Partitions on Consul servers. +// The function will retry its tasks until success, or it exceeds its timeout. +func (c *Command) Run(args []string) int { + c.once.Do(c.init) + if err := c.flags.Parse(args); err != nil { + return 1 + } + if len(c.flags.Args()) > 0 { + c.UI.Error("Should have no non-flag arguments.") + return 1 + } + + // Validate flags + if err := c.validateFlags(); err != nil { + c.UI.Error(err.Error()) + return 1 + } + var cancel context.CancelFunc + c.ctx, cancel = context.WithTimeout(context.Background(), c.flagTimeout) + // The context will only ever be intentionally ended by the timeout. + defer cancel() + + var err error + c.log, err = common.Logger(c.flagLogLevel, c.flagLogJSON) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + serverAddresses, err := common.GetResolvedServerAddresses(c.flagServerAddresses, c.providers, c.log) + if err != nil { + c.UI.Error(fmt.Sprintf("Unable to discover any Consul addresses from %q: %s", c.flagServerAddresses[0], err)) + return 1 + } + + scheme := "http" + if c.flagUseHTTPS { + scheme = "https" + } + // For all of the next operations we'll need a Consul client. + serverAddr := fmt.Sprintf("%s:%d", serverAddresses[0], c.flagServerPort) + consulClient, err := consul.NewClient(&api.Config{ + Address: serverAddr, + Scheme: scheme, + TLSConfig: api.TLSConfig{ + Address: c.flagConsulTLSServerName, + CAFile: c.flagConsulCACert, + }, + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error creating Consul client for addr %q: %s", serverAddr, err)) + return 1 + } + for { + partition, _, err := consulClient.Partitions().Read(c.ctx, c.flagPartitionName, nil) + // The API does not return an error if the Partition does not exist. It returns a nil Partition. + if err != nil { + c.log.Error("Error reading Partition from Consul", "name", c.flagPartitionName, "error", err.Error()) + } else if partition == nil { + // Retry Admin Partition creation until it succeeds, or we reach the command timeout. + _, _, err = consulClient.Partitions().Create(c.ctx, &api.Partition{ + Name: c.flagPartitionName, + Description: "Created by Helm installation", + }, nil) + if err == nil { + c.log.Info("Successfully created Admin Partition", "name", c.flagPartitionName) + return 0 + } + c.log.Error("Error creating partition", "name", c.flagPartitionName, "error", err.Error()) + } else { + c.log.Info("Admin Partition already exists", "name", c.flagPartitionName) + return 0 + } + // Wait on either the retry duration (in which case we continue) or the + // overall command timeout. + c.log.Info("Retrying in " + c.retryDuration.String()) + select { + case <-time.After(c.retryDuration): + continue + case <-c.ctx.Done(): + c.log.Error("Timed out attempting to create partition", "name", c.flagPartitionName) + return 1 + } + } +} + +func (c *Command) validateFlags() error { + if len(c.flagServerAddresses) == 0 { + return errors.New("-server-address must be set at least once") + } + + if c.flagPartitionName == "" { + return errors.New("-partition-name must be set") + } + return nil +} + +const synopsis = "Initialize an Admin Partition on Consul." +const help = ` +Usage: consul-k8s-control-plane partition-init [options] + + Bootstraps Consul with non-default Admin Partitions. + It will run until the partition has been created or the operation times out. It is idempotent + and safe to run multiple times. + +` diff --git a/control-plane/subcommand/partition-init/command_ent_test.go b/control-plane/subcommand/partition-init/command_ent_test.go new file mode 100644 index 0000000000..22bb9b8651 --- /dev/null +++ b/control-plane/subcommand/partition-init/command_ent_test.go @@ -0,0 +1,150 @@ +//go:build enterprise + +package partition_init + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" +) + +func TestRun_FlagValidation(t *testing.T) { + t.Parallel() + + cases := []struct { + flags []string + expErr string + }{ + { + flags: nil, + expErr: "-server-address must be set at least once", + }, + { + flags: []string{"-server-address", "foo"}, + expErr: "-partition-name must be set", + }, + { + flags: []string{"-server-address", "foo", "-partition-name", "bar", "-log-level", "invalid"}, + expErr: "unknown log level: invalid", + }, + } + + for _, c := range cases { + t.Run(c.expErr, func(tt *testing.T) { + ui := cli.NewMockUi() + cmd := Command{UI: ui} + exitCode := cmd.Run(c.flags) + require.Equal(tt, 1, exitCode, ui.ErrorWriter.String()) + require.Contains(tt, ui.ErrorWriter.String(), c.expErr) + }) + } +} + +func TestRun_PartitionCreate(t *testing.T) { + partitionName := "test-partition" + + server, err := testutil.NewTestServerConfigT(t, nil) + require.NoError(t, err) + server.WaitForLeader(t) + defer server.Stop() + + consul, err := api.NewClient(&api.Config{ + Address: server.HTTPAddr, + }) + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + cmd.init() + args := []string{ + "-server-address=" + strings.Split(server.HTTPAddr, ":")[0], + "-server-port=" + strings.Split(server.HTTPAddr, ":")[1], + "-partition-name", partitionName, + } + + responseCode := cmd.Run(args) + + require.Equal(t, 0, responseCode) + + partition, _, err := consul.Partitions().Read(context.Background(), partitionName, nil) + require.NoError(t, err) + require.NotNil(t, partition) + require.Equal(t, partitionName, partition.Name) +} + +func TestRun_PartitionExists(t *testing.T) { + partitionName := "test-partition" + + server, err := testutil.NewTestServerConfigT(t, nil) + require.NoError(t, err) + server.WaitForLeader(t) + defer server.Stop() + + consul, err := api.NewClient(&api.Config{ + Address: server.HTTPAddr, + }) + require.NoError(t, err) + + // Create the Admin Partition before the test runs. + _, _, err = consul.Partitions().Create(context.Background(), &api.Partition{Name: partitionName, Description: "Created before test"}, nil) + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + cmd.init() + args := []string{ + "-server-address=" + strings.Split(server.HTTPAddr, ":")[0], + "-server-port=" + strings.Split(server.HTTPAddr, ":")[1], + "-partition-name", partitionName, + } + + responseCode := cmd.Run(args) + + require.Equal(t, 0, responseCode) + + partition, _, err := consul.Partitions().Read(context.Background(), partitionName, nil) + require.NoError(t, err) + require.NotNil(t, partition) + require.Equal(t, partitionName, partition.Name) + require.Equal(t, "Created before test", partition.Description) +} + +func TestRun_ExitsAfterTimeout(t *testing.T) { + partitionName := "test-partition" + + server, err := testutil.NewTestServerConfigT(t, nil) + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + } + cmd.init() + args := []string{ + "-server-address=" + strings.Split(server.HTTPAddr, ":")[0], + "-server-port=" + strings.Split(server.HTTPAddr, ":")[1], + "-partition-name", partitionName, + "-timeout", "500ms", + } + server.Stop() + startTime := time.Now() + responseCode := cmd.Run(args) + completeTime := time.Now() + + require.Equal(t, 1, responseCode) + // While the timeout is 500ms, adding a buffer of 500ms ensures we account for + // some buffer time required for the task to run and assignments to occur. + require.WithinDuration(t, completeTime, startTime, 1*time.Second) +} + +// TODO: Write tests with ACLs enabled diff --git a/control-plane/subcommand/server-acl-init/command.go b/control-plane/subcommand/server-acl-init/command.go index 0625b056b9..64906d44ad 100644 --- a/control-plane/subcommand/server-acl-init/command.go +++ b/control-plane/subcommand/server-acl-init/command.go @@ -11,12 +11,6 @@ import ( "sync" "time" - "github.com/hashicorp/consul-k8s/control-plane/consul" - godiscover "github.com/hashicorp/consul-k8s/control-plane/helper/go-discover" - "github.com/hashicorp/consul-k8s/control-plane/subcommand" - "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" - "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" - k8sflags "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" "github.com/hashicorp/consul/api" "github.com/hashicorp/go-discover" "github.com/hashicorp/go-hclog" @@ -25,6 +19,12 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + + "github.com/hashicorp/consul-k8s/control-plane/consul" + "github.com/hashicorp/consul-k8s/control-plane/subcommand" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" + k8sflags "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" ) type Command struct { @@ -57,18 +57,24 @@ type Command struct { flagIngressGatewayNames []string flagTerminatingGatewayNames []string - // Flags to configure Consul connection + flagCreateAPIGatewayToken bool + + // Flags to configure Consul connection. flagServerAddresses []string flagServerPort uint flagConsulCACert string flagConsulTLSServerName string flagUseHTTPS bool - // Flags for ACL replication + // Flags for ACL replication. flagCreateACLReplicationToken bool flagACLReplicationTokenFile string - // Flags to support namespaces + // Flags to support partitions. + flagEnablePartitions bool // true if Admin Partitions are enabled + flagPartitionName string // name of the Admin Partition + + // Flags to support namespaces. flagEnableNamespaces bool // Use namespacing on all components flagConsulSyncDestinationNamespace string // Consul namespace to register all catalog sync services into if not mirroring flagEnableSyncK8SNSMirroring bool // Enables mirroring of k8s namespaces into Consul for catalog sync @@ -77,7 +83,7 @@ type Command struct { flagEnableInjectK8SNSMirroring bool // Enables mirroring of k8s namespaces into Consul for Connect inject flagInjectK8SNSMirroringPrefix string // Prefix added to Consul namespaces created when mirroring injected services - // Flag to support a custom bootstrap token + // Flag to support a custom bootstrap token. flagBootstrapTokenFile string flagLogLevel string @@ -91,8 +97,8 @@ type Command struct { clientset kubernetes.Interface - // cmdTimeout is cancelled when the command timeout is reached. - cmdTimeout context.Context + // ctx is cancelled when the command timeout is reached. + ctx context.Context retryDuration time.Duration // log @@ -158,6 +164,8 @@ func (c *Command) init() { "Name of a terminating gateway that needs an acl token. May be specified multiple times. "+ "[Enterprise Only] If using Consul namespaces and registering the gateway outside of the "+ "default namespace, specify the value in the form ..") + c.flags.BoolVar(&c.flagCreateAPIGatewayToken, "create-api-gateway-token", false, + "Toggle for creating a token for the API Gateway controller integration.") c.flags.Var((*flags.AppendSliceValue)(&c.flagServerAddresses), "server-address", "The IP, DNS name or the cloud auto-join string of the Consul server(s). If providing IPs or DNS names, may be specified multiple times. "+ @@ -170,6 +178,11 @@ func (c *Command) init() { c.flags.BoolVar(&c.flagUseHTTPS, "use-https", false, "Toggle for using HTTPS for all API calls to Consul.") + c.flags.BoolVar(&c.flagEnablePartitions, "enable-partitions", false, + "[Enterprise Only] Enables Admin Partitions") + c.flags.StringVar(&c.flagPartitionName, "partition", "", + "[Enterprise Only] Name of the Admin Partition") + c.flags.BoolVar(&c.flagEnableNamespaces, "enable-namespaces", false, "[Enterprise Only] Enables namespaces, in either a single Consul namespace or mirrored [Enterprise only feature]") c.flags.StringVar(&c.flagConsulSyncDestinationNamespace, "consul-sync-destination-namespace", consulDefaultNamespace, @@ -276,7 +289,7 @@ func (c *Command) Run(args []string) int { } var cancel context.CancelFunc - c.cmdTimeout, cancel = context.WithTimeout(context.Background(), c.flagTimeout) + c.ctx, cancel = context.WithTimeout(context.Background(), c.flagTimeout) // The context will only ever be intentionally ended by the timeout. defer cancel() @@ -287,18 +300,6 @@ func (c *Command) Run(args []string) int { return 1 } - serverAddresses := c.flagServerAddresses - // Check if the provided addresses contain a cloud-auto join string. - // If yes, call godiscover to discover addresses of the Consul servers. - if len(c.flagServerAddresses) == 1 && strings.Contains(c.flagServerAddresses[0], "provider=") { - var err error - serverAddresses, err = godiscover.ConsulServerAddresses(c.flagServerAddresses[0], c.providers, c.log) - if err != nil { - c.UI.Error(fmt.Sprintf("Unable to discover any Consul addresses from %q: %s", c.flagServerAddresses[0], err)) - return 1 - } - } - // The ClientSet might already be set if we're in a test. if c.clientset == nil { if err := c.configureKubeClient(); err != nil { @@ -307,12 +308,16 @@ func (c *Command) Run(args []string) int { } } + serverAddresses, err := common.GetResolvedServerAddresses(c.flagServerAddresses, c.providers, c.log) + if err != nil { + c.UI.Error(fmt.Sprintf("Unable to discover any Consul addresses from %q: %s", c.flagServerAddresses[0], err)) + return 1 + } scheme := "http" if c.flagUseHTTPS { scheme = "https" } - var updateServerPolicy bool var bootstrapToken string if c.flagBootstrapTokenFile != "" { @@ -320,7 +325,7 @@ func (c *Command) Run(args []string) int { // the provided token to create policies and tokens for the rest of the components. c.log.Info("Bootstrap token is provided so skipping Consul server ACL bootstrapping") bootstrapToken = providedBootstrapToken - } else if c.flagACLReplicationTokenFile != "" { + } else if c.flagACLReplicationTokenFile != "" && !c.flagCreateACLReplicationToken { // If ACL replication is enabled, we don't need to ACL bootstrap the servers // since they will be performing replication. // We can use the replication token as our bootstrap token because it @@ -339,25 +344,19 @@ func (c *Command) Run(args []string) int { if bootstrapToken != "" { c.log.Info(fmt.Sprintf("ACLs already bootstrapped - retrieved bootstrap token from Secret %q", bootTokenSecretName)) - - // Mark that we should update the server ACL policy in case - // there are namespace related config changes. Because of the - // organization of the server token creation code, the policy - // otherwise won't be updated. - updateServerPolicy = true } else { c.log.Info("No bootstrap token from previous installation found, continuing on to bootstrapping") - bootstrapToken, err = c.bootstrapServers(serverAddresses, bootTokenSecretName, scheme) - if err != nil { - c.log.Error(err.Error()) - return 1 - } + } + bootstrapToken, err = c.bootstrapServers(serverAddresses, bootstrapToken, bootTokenSecretName, scheme) + if err != nil { + c.log.Error(err.Error()) + return 1 } } // For all of the next operations we'll need a Consul client. serverAddr := fmt.Sprintf("%s:%d", serverAddresses[0], c.flagServerPort) - consulClient, err := consul.NewClient(&api.Config{ + clientConfig := &api.Config{ Address: serverAddr, Scheme: scheme, Token: bootstrapToken, @@ -365,7 +364,12 @@ func (c *Command) Run(args []string) int { Address: c.flagConsulTLSServerName, CAFile: c.flagConsulCACert, }, - }) + } + if c.flagEnablePartitions { + clientConfig.Partition = c.flagPartitionName + } + + consulClient, err := consul.NewClient(clientConfig) if err != nil { c.log.Error(fmt.Sprintf("Error creating Consul client for addr %q: %s", serverAddr, err)) return 1 @@ -378,14 +382,11 @@ func (c *Command) Run(args []string) int { c.log.Info("Current datacenter", "datacenter", consulDC, "primaryDC", primaryDC) isPrimary := consulDC == primaryDC - // With the addition of namespaces, the ACL policies associated - // with the server tokens may need to be updated if Enterprise Consul - // users upgrade to 1.7+. This updates the policy if the bootstrap - // token had previously existed, which signals a potential config change. - if updateServerPolicy { - _, err = c.setServerPolicy(consulClient) + if c.flagEnablePartitions && c.flagPartitionName == consulDefaultPartition && isPrimary { + // Partition token is local because only the Primary datacenter can have Admin Partitions. + err := c.createLocalACL("partitions", partitionRules, consulDC, isPrimary, consulClient) if err != nil { - c.log.Error("Error updating the server ACL policy", "err", err) + c.log.Error(err.Error()) return 1 } } @@ -397,12 +398,17 @@ func (c *Command) Run(args []string) int { // connect inject) needs to reference this policy on namespace creation // to finish the cross namespace permission setup. if c.flagEnableNamespaces { + crossNamespaceRule, err := c.crossNamespaceRules() + if err != nil { + c.log.Error("Error templating cross namespace rules", "err", err) + return 1 + } policyTmpl := api.ACLPolicy{ Name: "cross-namespace-policy", Description: "Policy to allow permissions to cross Consul namespaces for k8s services", - Rules: crossNamespaceRules, + Rules: crossNamespaceRule, } - err := c.untilSucceeds(fmt.Sprintf("creating %s policy", policyTmpl.Name), + err = c.untilSucceeds(fmt.Sprintf("creating %s policy", policyTmpl.Name), func() error { return c.createOrUpdateACLPolicy(policyTmpl, consulClient) }) @@ -449,7 +455,21 @@ func (c *Command) Run(args []string) int { } if c.createAnonymousPolicy(isPrimary) { - err := c.configureAnonymousPolicy(consulClient) + // When the default partition is in a VM, the anonymous policy does not allow cross-partition + // DNS lookups. The anonymous policy in the default partition needs to be updated in order to + // support this use-case. Creating a separate anonymous token client that updates the anonymous + // policy and token in the default partition ensures this works. + anonTokenConfig := clientConfig + if c.flagEnablePartitions { + anonTokenConfig.Partition = consulDefaultPartition + } + anonTokenClient, err := consul.NewClient(anonTokenConfig) + if err != nil { + c.log.Error(err.Error()) + return 1 + } + + err = c.configureAnonymousPolicy(anonTokenClient) if err != nil { c.log.Error(err.Error()) return 1 @@ -505,7 +525,12 @@ func (c *Command) Run(args []string) int { } if c.flagCreateEntLicenseToken { - err := c.createLocalACL("enterprise-license", entLicenseRules, consulDC, isPrimary, consulClient) + var err error + if c.flagEnablePartitions { + err = c.createLocalACL("enterprise-license", entPartitionLicenseRules, consulDC, isPrimary, consulClient) + } else { + err = c.createLocalACL("enterprise-license", entLicenseRules, consulDC, isPrimary, consulClient) + } if err != nil { c.log.Error(err.Error()) return 1 @@ -520,6 +545,19 @@ func (c *Command) Run(args []string) int { } } + if c.flagCreateAPIGatewayToken { + apigwRules, err := c.apiGatewayControllerRules() + if err != nil { + c.log.Error("Error templating api gateway rules", "err", err) + return 1 + } + err = c.createLocalACL("api-gateway-controller", apigwRules, consulDC, isPrimary, consulClient) + if err != nil { + c.log.Error(err.Error()) + return 1 + } + } + if c.flagCreateMeshGatewayToken { meshGatewayRules, err := c.meshGatewayRules() if err != nil { @@ -656,7 +694,11 @@ func (c *Command) Run(args []string) int { } // Policy must be global because it replicates from the primary DC // and so the primary DC needs to be able to accept the token. - err = c.createGlobalACL(common.ACLReplicationTokenName, rules, consulDC, isPrimary, consulClient) + if aclReplicationToken != "" { + err = c.createGlobalACLWithSecretID(common.ACLReplicationTokenName, rules, consulDC, isPrimary, consulClient, aclReplicationToken) + } else { + err = c.createGlobalACL(common.ACLReplicationTokenName, rules, consulDC, isPrimary, consulClient) + } if err != nil { c.log.Error(err.Error()) return 1 @@ -687,7 +729,7 @@ func (c *Command) Run(args []string) int { // reading the Kubernetes Secret with name secretName. // If there is no bootstrap token yet, then it returns an empty string (not an error). func (c *Command) getBootstrapToken(secretName string) (string, error) { - secret, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(context.TODO(), secretName, metav1.GetOptions{}) + secret, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(c.ctx, secretName, metav1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return "", nil @@ -729,7 +771,7 @@ func (c *Command) untilSucceeds(opName string, op func() error) error { select { case <-time.After(c.retryDuration): continue - case <-c.cmdTimeout.Done(): + case <-c.ctx.Done(): return errors.New("reached command timeout") } } @@ -835,10 +877,17 @@ func (c *Command) validateFlags() error { ) } + if c.flagEnablePartitions && c.flagPartitionName == "" { + return errors.New("-partition must be set if -enable-partitions is true") + } + if !c.flagEnablePartitions && c.flagPartitionName != "" { + return errors.New("-enable-partitions must be 'true' if -partition is set") + } return nil } const consulDefaultNamespace = "default" +const consulDefaultPartition = "default" const synopsis = "Initialize ACLs on Consul servers and other components." const help = ` Usage: consul-k8s-control-plane server-acl-init [options] diff --git a/control-plane/subcommand/server-acl-init/command_ent_test.go b/control-plane/subcommand/server-acl-init/command_ent_test.go index 57270644b6..5824d4af9b 100644 --- a/control-plane/subcommand/server-acl-init/command_ent_test.go +++ b/control-plane/subcommand/server-acl-init/command_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package serveraclinit @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/hashicorp/consul-k8s/control-plane/consul" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil" "github.com/mitchellh/cli" @@ -19,7 +21,6 @@ import ( // and there's a single consul destination namespace. func TestRun_ConnectInject_SingleDestinationNamespace(t *testing.T) { t.Parallel() - consulDestNamespaces := []string{"default", "destination"} for _, consulDestNamespace := range consulDestNamespaces { t.Run(consulDestNamespace, func(tt *testing.T) { @@ -40,6 +41,8 @@ func TestRun_ConnectInject_SingleDestinationNamespace(t *testing.T) { "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-create-inject-token", + "-enable-partitions", + "-partition=default", "-enable-namespaces", "-consul-inject-destination-namespace", consulDestNamespace, "-acl-binding-rule-selector=serviceaccount.name!=default", @@ -160,6 +163,8 @@ func TestRun_ConnectInject_NamespaceMirroring(t *testing.T) { "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-create-inject-token", + "-enable-partitions", + "-partition=default", "-enable-namespaces", "-enable-inject-k8s-namespace-mirroring", "-inject-k8s-namespace-mirroring-prefix", c.MirroringPrefix, @@ -203,7 +208,55 @@ func TestRun_ConnectInject_NamespaceMirroring(t *testing.T) { } } -// Test that ACL policies get updated if namespaces config changes. +// Test that the anonymous token policy is created in the default partition from +// a non-default partition. +func TestRun_AnonymousToken_CreatedFromNonDefaultPartition(t *testing.T) { + bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + tokenFile := common.WriteTempFile(t, bootToken) + server, stopFn := partitionedSetup(t, bootToken, "test") + defer stopFn() + k8s := fake.NewSimpleClientset() + setUpK8sServiceAccount(t, k8s, ns) + + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + clientset: k8s, + } + cmd.init() + args := []string{ + "-server-address=" + strings.Split(server.HTTPAddr, ":")[0], + "-server-port=" + strings.Split(server.HTTPAddr, ":")[1], + "-resource-prefix=" + resourcePrefix, + "-k8s-namespace=" + ns, + "-bootstrap-token-file", tokenFile, + "-enable-partitions", + "-allow-dns", + "-partition=test", + "-enable-namespaces", + } + responseCode := cmd.Run(args) + require.Equal(t, 0, responseCode, ui.ErrorWriter.String()) + + consul, err := api.NewClient(&api.Config{ + Address: server.HTTPAddr, + Token: bootToken, + }) + require.NoError(t, err) + + anonPolicyName := "anonymous-token-policy" + // Check that the anonymous token policy was created. + policy := policyExists(t, anonPolicyName, consul) + // Should be a global policy. + require.Len(t, policy.Datacenters, 0) + + // Check that the anonymous token has the policy. + tokenData, _, err := consul.ACL().TokenReadSelf(&api.QueryOptions{Token: "anonymous"}) + require.NoError(t, err) + require.Equal(t, anonPolicyName, tokenData.Policies[0].Name) +} + +// Test that ACL policies get updated if namespaces/partition config changes. func TestRun_ACLPolicyUpdates(t *testing.T) { t.Parallel() @@ -234,9 +287,11 @@ func TestRun_ACLPolicyUpdates(t *testing.T) { "-terminating-gateway-name=anothergw", "-create-controller-token", } - // Our second run, we're going to update from namespaces disabled to - // namespaces enabled with a single destination ns. + // Our second run, we're going to update from partitions and namespaces disabled to + // namespaces enabled with a single destination ns and partitions enabled. secondRunArgs := append(firstRunArgs, + "-enable-partitions", + "-partition=default", "-enable-namespaces", "-consul-sync-destination-namespace=sync", "-consul-inject-destination-namespace=dest") @@ -322,6 +377,7 @@ func TestRun_ACLPolicyUpdates(t *testing.T) { "gw-terminating-gateway-token", "anothergw-terminating-gateway-token", "controller-token", + "partitions-token", } policies, _, err = consul.ACL().PolicyList(nil) require.NoError(err) @@ -348,10 +404,13 @@ func TestRun_ACLPolicyUpdates(t *testing.T) { case "connect-inject-token": // The connect inject token doesn't have namespace config, // but does change to operator:write from an empty string. - require.Contains(actRules, "operator = \"write\"") + require.Contains(actRules, "policy = \"write\"") case "client-snapshot-agent-token", "enterprise-license-token": // The snapshot agent and enterprise license tokens shouldn't change. require.NotContains(actRules, "namespace") + require.Contains(actRules, "acl = \"write\"") + case "partitions-token": + require.Contains(actRules, "operator = \"write\"") default: // Assert that the policies have the word namespace in them. This // tests that they were updated. The actual contents are tested @@ -528,6 +587,8 @@ func TestRun_ConnectInject_Updates(t *testing.T) { "-server-port=" + strings.Split(testAgent.HTTPAddr, ":")[1], "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, + "-enable-partitions", + "-partition=default", "-create-inject-token", } @@ -693,6 +754,13 @@ func TestRun_TokensWithNamespacesEnabled(t *testing.T) { SecretNames: []string{resourcePrefix + "-controller-acl-token"}, LocalToken: false, }, + "partitions token": { + TokenFlags: []string{"-enable-partitions", "-partition=default"}, + PolicyNames: []string{"partitions-token"}, + PolicyDCs: []string{"dc1"}, + SecretNames: []string{resourcePrefix + "-partitions-acl-token"}, + LocalToken: true, + }, } for testName, c := range cases { t.Run(testName, func(t *testing.T) { @@ -713,6 +781,8 @@ func TestRun_TokensWithNamespacesEnabled(t *testing.T) { "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, + "-enable-partitions", + "-partition=default", "-enable-namespaces", }, c.TokenFlags...) @@ -779,37 +849,43 @@ func TestRun_GatewayNamespaceParsing(t *testing.T) { "gateway-ingress-gateway-token", "another-gateway-ingress-gateway-token"}, ExpectedPolicies: []string{` -namespace "default" { - service "ingress" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "ingress" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`, ` -namespace "default" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`, ` -namespace "default" { - service "another-gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "another-gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`}, }, @@ -822,37 +898,43 @@ namespace "default" { "gateway-ingress-gateway-token", "another-gateway-ingress-gateway-token"}, ExpectedPolicies: []string{` -namespace "default" { - service "ingress" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "ingress" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`, ` -namespace "namespace1" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" +partition "default" { + namespace "namespace1" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`, ` -namespace "namespace2" { - service "another-gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" +partition "default" { + namespace "namespace2" { + service "another-gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`}, }, @@ -865,28 +947,34 @@ namespace "namespace2" { "gateway-terminating-gateway-token", "another-gateway-terminating-gateway-token"}, ExpectedPolicies: []string{` -namespace "default" { - service "terminating" { - policy = "write" - } - node_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "terminating" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`, ` -namespace "default" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`, ` -namespace "default" { - service "another-gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "another-gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`}, }, @@ -899,28 +987,34 @@ namespace "default" { "gateway-terminating-gateway-token", "another-gateway-terminating-gateway-token"}, ExpectedPolicies: []string{` -namespace "default" { - service "terminating" { - policy = "write" - } - node_prefix "" { - policy = "read" +partition "default" { + namespace "default" { + service "terminating" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`, ` -namespace "namespace1" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" +partition "default" { + namespace "namespace1" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`, ` -namespace "namespace2" { - service "another-gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" +partition "default" { + namespace "namespace2" { + service "another-gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`}, }, @@ -944,6 +1038,8 @@ namespace "namespace2" { "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], "-resource-prefix=" + resourcePrefix, "-enable-namespaces=true", + "-enable-partitions", + "-partition=default", }, c.TokenFlags...) responseCode := cmd.Run(cmdArgs) @@ -991,3 +1087,40 @@ func completeEnterpriseSetup(t *testing.T) (*fake.Clientset, *testutil.TestServe return k8s, svr } + +// partitionedSetup is a helper function which creates a server and a consul agent that runs as +// a client in the provided partitionName. The bootToken is the token used as the bootstrap token +// for both the client and the server. The helper creates a server, then creates a partition with +// the provided partitionName and then creates a client in said partition. +func partitionedSetup(t *testing.T, bootToken string, partitionName string) (*testutil.TestServer, func()) { + server, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.ACL.Enabled = true + c.ACL.Tokens.InitialManagement = bootToken + }) + require.NoError(t, err) + server.WaitForLeader(t) + + serverAPIClient, err := consul.NewClient(&api.Config{ + Address: server.HTTPAddr, + Token: bootToken, + }) + require.NoError(t, err) + + _, _, err = serverAPIClient.Partitions().Create(context.Background(), &api.Partition{Name: partitionName}, &api.WriteOptions{}) + require.NoError(t, err) + + partitionedClient, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.Server = false + c.Bootstrap = false + c.Partition = partitionName + c.RetryJoin = []string{server.LANAddr} + c.ACL.Enabled = true + c.ACL.Tokens.Agent = bootToken + }) + require.NoError(t, err) + + return server, func() { + server.Stop() + partitionedClient.Stop() + } +} diff --git a/control-plane/subcommand/server-acl-init/command_test.go b/control-plane/subcommand/server-acl-init/command_test.go index 037731de62..bbccfcc20d 100644 --- a/control-plane/subcommand/server-acl-init/command_test.go +++ b/control-plane/subcommand/server-acl-init/command_test.go @@ -10,27 +10,31 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "strconv" "strings" "testing" "time" - "github.com/hashicorp/consul-k8s/control-plane/helper/cert" - "github.com/hashicorp/consul-k8s/control-plane/helper/go-discover/mocks" - "github.com/hashicorp/consul-k8s/control-plane/helper/test" - "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/sdk/freeport" - "github.com/hashicorp/consul/sdk/testutil" - "github.com/hashicorp/consul/sdk/testutil/retry" - "github.com/hashicorp/go-discover" - "github.com/hashicorp/go-hclog" "github.com/mitchellh/cli" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/freeport" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/hashicorp/go-discover" + "github.com/hashicorp/go-hclog" + + "github.com/hashicorp/consul-k8s/control-plane/helper/cert" + "github.com/hashicorp/consul-k8s/control-plane/helper/go-discover/mocks" + "github.com/hashicorp/consul-k8s/control-plane/helper/test" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" ) var ns = "default" @@ -188,6 +192,14 @@ func TestRun_TokensPrimaryDC(t *testing.T) { SecretNames: []string{resourcePrefix + "-client-snapshot-agent-acl-token"}, LocalToken: true, }, + { + TestName: "API gateway token", + TokenFlags: []string{"-create-api-gateway-token"}, + PolicyNames: []string{"api-gateway-controller-token"}, + PolicyDCs: []string{"dc1"}, + SecretNames: []string{resourcePrefix + "-api-gateway-controller-acl-token"}, + LocalToken: true, + }, { TestName: "Mesh gateway token", TokenFlags: []string{"-create-mesh-gateway-token"}, @@ -315,6 +327,70 @@ func TestRun_TokensPrimaryDC(t *testing.T) { } } +func TestRun_ReplicationTokenPrimaryDC_WithProvidedSecretID(t *testing.T) { + t.Parallel() + + k8s, testSvr := completeSetup(t) + defer testSvr.Stop() + require := require.New(t) + + replicationToken := "123e4567-e89b-12d3-a456-426614174000" + replicationTokenFile, err := ioutil.TempFile("", "replicationtoken") + require.NoError(err) + defer os.Remove(replicationTokenFile.Name()) + + replicationTokenFile.WriteString(replicationToken) + // Run the command. + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + clientset: k8s, + } + cmd.init() + cmdArgs := []string{ + "-timeout=1m", + "-k8s-namespace=" + ns, + "-server-address", strings.Split(testSvr.HTTPAddr, ":")[0], + "-server-port", strings.Split(testSvr.HTTPAddr, ":")[1], + "-resource-prefix=" + resourcePrefix, + "-create-acl-replication-token", + "-acl-replication-token-file", replicationTokenFile.Name(), + } + + responseCode := cmd.Run(cmdArgs) + require.Equal(0, responseCode, ui.ErrorWriter.String()) + + // Check that this token is created. + consul, err := api.NewClient(&api.Config{ + Address: testSvr.HTTPAddr, + Token: replicationToken, + }) + require.NoError(err) + token, _, err := consul.ACL().TokenReadSelf(nil) + require.NoError(err) + + for _, policyLink := range token.Policies { + policy := policyExists(t, policyLink.Name, consul) + require.Nil(policy.Datacenters) + + // Test that the token was not created as a Kubernetes Secret. + _, err := k8s.CoreV1().Secrets(ns).Get(context.Background(), resourcePrefix+"-acl-replication-acl-token", metav1.GetOptions{}) + require.True(k8serrors.IsNotFound(err)) + } + + // Test that if the same command is run again, it doesn't error. + t.Run(t.Name()+"-retried", func(t *testing.T) { + ui = cli.NewMockUi() + cmd = Command{ + UI: ui, + clientset: k8s, + } + cmd.init() + responseCode = cmd.Run(cmdArgs) + require.Equal(0, responseCode, ui.ErrorWriter.String()) + }) +} + // Test creating each token type when replication is enabled. func TestRun_TokensReplicatedDC(t *testing.T) { t.Parallel() @@ -359,6 +435,14 @@ func TestRun_TokensReplicatedDC(t *testing.T) { SecretNames: []string{resourcePrefix + "-client-snapshot-agent-acl-token"}, LocalToken: true, }, + { + TestName: "API Gateway token", + TokenFlags: []string{"-create-api-gateway-token"}, + PolicyNames: []string{"api-gateway-controller-token-dc2"}, + PolicyDCs: []string{"dc2"}, + SecretNames: []string{resourcePrefix + "-api-gateway-controller-acl-token"}, + LocalToken: true, + }, { TestName: "Mesh gateway token", TokenFlags: []string{"-create-mesh-gateway-token"}, @@ -505,6 +589,12 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { PolicyNames: []string{"client-snapshot-agent-token"}, SecretNames: []string{resourcePrefix + "-client-snapshot-agent-acl-token"}, }, + { + TestName: "API Gateway token", + TokenFlags: []string{"-create-api-gateway-token"}, + PolicyNames: []string{"api-gateway-controller-token"}, + SecretNames: []string{resourcePrefix + "-api-gateway-controller-acl-token"}, + }, { TestName: "Mesh gateway token", TokenFlags: []string{"-create-mesh-gateway-token"}, @@ -1168,7 +1258,7 @@ func TestRun_DelayedServers(t *testing.T) { require := require.New(t) k8s := fake.NewSimpleClientset() - randomPorts := freeport.MustTake(6) + randomPorts := freeport.GetN(t, 6) ui := cli.NewMockUi() cmd := Command{ @@ -1284,6 +1374,8 @@ func TestRun_NoLeader(t *testing.T) { numACLBootCalls++ case "/v1/agent/self": fmt.Fprintln(w, `{"Config": {"Datacenter": "dc1", "PrimaryDatacenter": "dc1"}}`) + case "/v1/acl/tokens": + fmt.Fprintln(w, `[]`) default: fmt.Fprintln(w, "{}") } @@ -1342,6 +1434,10 @@ func TestRun_NoLeader(t *testing.T) { "PUT", "/v1/acl/policy", }, + { + "GET", + "/v1/acl/tokens", + }, { "PUT", "/v1/acl/token", @@ -1448,8 +1544,8 @@ func TestConsulDatacenterList(t *testing.T) { require.NoError(t, err) command := Command{ - log: hclog.New(hclog.DefaultOptions), - cmdTimeout: context.Background(), + log: hclog.New(hclog.DefaultOptions), + ctx: context.Background(), } actDC, actPrimaryDC, err := command.consulDatacenterList(consulClient) if c.expErr != "" { @@ -1496,6 +1592,8 @@ func TestRun_ClientTokensRetry(t *testing.T) { numPolicyCalls++ case "/v1/agent/self": fmt.Fprintln(w, `{"Config": {"Datacenter": "dc1", "PrimaryDatacenter": "dc1"}}`) + case "/v1/acl/tokens": + fmt.Fprintln(w, `[]`) default: fmt.Fprintln(w, "{}") } @@ -1530,6 +1628,10 @@ func TestRun_ClientTokensRetry(t *testing.T) { "PUT", "/v1/acl/policy", }, + { + "GET", + "/v1/acl/tokens", + }, { "PUT", "/v1/acl/token", @@ -1558,8 +1660,8 @@ func TestRun_ClientTokensRetry(t *testing.T) { }, consulAPICalls) } -// Test if there is an old bootstrap Secret we assume the servers were -// bootstrapped already and continue on to the next step. +// Test if there is an old bootstrap Secret we still try to create and set +// server tokens. func TestRun_AlreadyBootstrapped(t *testing.T) { t.Parallel() require := require.New(t) @@ -1581,6 +1683,8 @@ func TestRun_AlreadyBootstrapped(t *testing.T) { switch r.URL.Path { case "/v1/agent/self": fmt.Fprintln(w, `{"Config": {"Datacenter": "dc1", "PrimaryDatacenter": "dc1"}}`) + case "/v1/acl/tokens": + fmt.Fprintln(w, `[]`) default: // Send an empty JSON response with code 200 to all calls. fmt.Fprintln(w, "{}") @@ -1596,7 +1700,8 @@ func TestRun_AlreadyBootstrapped(t *testing.T) { context.Background(), &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: resourcePrefix + "-bootstrap-acl-token", + Name: resourcePrefix + "-bootstrap-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ "token": []byte("old-token"), @@ -1629,15 +1734,27 @@ func TestRun_AlreadyBootstrapped(t *testing.T) { // Test that the expected API calls were made. require.Equal([]APICall{ - // We only expect the calls for creating client tokens - // and updating the server policy. + // We expect calls for updating the server policy, setting server tokens, + // and updating client policy. + { + "PUT", + "/v1/acl/policy", + }, { "GET", - "/v1/agent/self", + "/v1/acl/tokens", }, { "PUT", - "/v1/acl/policy", + "/v1/acl/token", + }, + { + "PUT", + "/v1/agent/token/agent", + }, + { + "GET", + "/v1/agent/self", }, { "PUT", @@ -1650,6 +1767,81 @@ func TestRun_AlreadyBootstrapped(t *testing.T) { }, consulAPICalls) } +// Test if there is an old bootstrap Secret and the server token exists +// that we don't try and recreate the token. +func TestRun_AlreadyBootstrapped_ServerTokenExists(t *testing.T) { + t.Parallel() + require := require.New(t) + + // First set everything up with ACLs bootstrapped. + bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + k8s, testAgent := completeBootstrappedSetup(t, bootToken) + setUpK8sServiceAccount(t, k8s, ns) + defer testAgent.Stop() + k8s.CoreV1().Secrets(ns).Create(context.Background(), &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourcePrefix + "-bootstrap-acl-token", + }, + Data: map[string][]byte{ + "token": []byte(bootToken), + }, + }, metav1.CreateOptions{}) + + consulClient, err := api.NewClient(&api.Config{ + Address: testAgent.HTTPAddr, + Token: bootToken, + }) + require.NoError(err) + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + clientset: k8s, + } + + // Create the server policy and token _before_ we run the command. + agentPolicyRules, err := cmd.agentRules() + require.NoError(err) + policy, _, err := consulClient.ACL().PolicyCreate(&api.ACLPolicy{ + Name: "agent-token", + Description: "Agent Token Policy", + Rules: agentPolicyRules, + }, nil) + require.NoError(err) + _, _, err = consulClient.ACL().TokenCreate(&api.ACLToken{ + Description: fmt.Sprintf("Server Token for %s", strings.Split(testAgent.HTTPAddr, ":")[0]), + Policies: []*api.ACLTokenPolicyLink{ + { + Name: policy.Name, + }, + }, + }, nil) + require.NoError(err) + + // Run the command. + cmdArgs := []string{ + "-timeout=1m", + "-k8s-namespace", ns, + "-server-address", strings.Split(testAgent.HTTPAddr, ":")[0], + "-server-port", strings.Split(testAgent.HTTPAddr, ":")[1], + "-resource-prefix", resourcePrefix, + } + + responseCode := cmd.Run(cmdArgs) + require.Equal(0, responseCode, ui.ErrorWriter.String()) + + // Check that only one server token exists, i.e. it didn't create an + // extra token. + tokens, _, err := consulClient.ACL().TokenList(nil) + require.NoError(err) + count := 0 + for _, token := range tokens { + if len(token.Policies) == 1 && token.Policies[0].Name == policy.Name { + count++ + } + } + require.Equal(1, count) +} + // Test if there is a provided bootstrap we skip bootstrapping of the servers // and continue on to the next step. func TestRun_SkipBootstrapping_WhenBootstrapTokenIsProvided(t *testing.T) { @@ -2005,7 +2197,7 @@ func completeBootstrappedSetup(t *testing.T, masterToken string) (*fake.Clientse svr, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { c.ACL.Enabled = true - c.ACL.Tokens.Master = masterToken + c.ACL.Tokens.InitialManagement = masterToken }) require.NoError(t, err) svr.WaitForActiveCARoot(t) @@ -2050,7 +2242,7 @@ func replicatedSetup(t *testing.T, bootToken string) (*fake.Clientset, *api.Clie primarySvr, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { c.ACL.Enabled = true if bootToken != "" { - c.ACL.Tokens.Master = bootToken + c.ACL.Tokens.InitialManagement = bootToken } }) require.NoError(t, err) @@ -2103,7 +2295,6 @@ func replicatedSetup(t *testing.T, bootToken string) (*fake.Clientset, *api.Clie } }) require.NoError(t, err) - secondarySvr.WaitForLeader(t) // Our consul client will use the secondary dc. clientToken := bootToken @@ -2127,6 +2318,8 @@ func replicatedSetup(t *testing.T, bootToken string) (*fake.Clientset, *api.Clie err = consul.Agent().Join(secondarySvr.WANAddr, true) require.NoError(t, err) + secondarySvr.WaitForLeader(t) + // Overwrite consul client, pointing it to the secondary DC consul, err = api.NewClient(&api.Config{ Address: secondarySvr.HTTPAddr, @@ -2161,7 +2354,7 @@ func getBootToken(t *testing.T, k8s *fake.Clientset, prefix string, k8sNamespace func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset, namespace string) (string, string) { // Create ServiceAccount for the kubernetes auth method if it doesn't exist, // otherwise, do nothing. - serviceAccountName := resourcePrefix + "-connect-injector-authmethod-svc-account" + serviceAccountName := resourcePrefix + "-connect-injector" sa, _ := k8s.CoreV1().ServiceAccounts(namespace).Get(context.Background(), serviceAccountName, metav1.GetOptions{}) if sa == nil { // Create a service account that references two secrets. @@ -2178,7 +2371,7 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset, namespace string) Name: resourcePrefix + "-some-other-secret", }, { - Name: resourcePrefix + "-connect-injector-authmethod-svc-account", + Name: resourcePrefix + "-connect-injector", }, }, }, @@ -2193,10 +2386,11 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset, namespace string) require.NoError(t, err) // Create a Kubernetes secret if it doesn't exist, otherwise update it - secretName := resourcePrefix + "-connect-injector-authmethod-svc-account" + secretName := resourcePrefix + "-connect-injector" secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + Name: secretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ "ca.crt": caCertBytes, @@ -2209,7 +2403,8 @@ func setUpK8sServiceAccount(t *testing.T, k8s *fake.Clientset, namespace string) // Create the second secret of a different type otherSecret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: resourcePrefix + "-some-other-secret", + Name: resourcePrefix + "-some-other-secret", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{}, Type: v1.SecretTypeDockercfg, diff --git a/control-plane/subcommand/server-acl-init/connect_inject.go b/control-plane/subcommand/server-acl-init/connect_inject.go index 6389b6d3c8..abd10f9f7f 100644 --- a/control-plane/subcommand/server-acl-init/connect_inject.go +++ b/control-plane/subcommand/server-acl-init/connect_inject.go @@ -1,7 +1,6 @@ package serveraclinit import ( - "context" "errors" "fmt" @@ -140,11 +139,11 @@ func (c *Command) configureConnectInjectAuthMethod(consulClient *api.Client) err func (c *Command) createAuthMethodTmpl(authMethodName string) (api.ACLAuthMethod, error) { // Get the Secret name for the auth method ServiceAccount. var authMethodServiceAccount *apiv1.ServiceAccount - saName := c.withPrefix("connect-injector-authmethod-svc-account") + saName := c.withPrefix("connect-injector") err := c.untilSucceeds(fmt.Sprintf("getting %s ServiceAccount", saName), func() error { var err error - authMethodServiceAccount, err = c.clientset.CoreV1().ServiceAccounts(c.flagK8sNamespace).Get(context.TODO(), saName, metav1.GetOptions{}) + authMethodServiceAccount, err = c.clientset.CoreV1().ServiceAccounts(c.flagK8sNamespace).Get(c.ctx, saName, metav1.GetOptions{}) return err }) if err != nil { @@ -161,7 +160,7 @@ func (c *Command) createAuthMethodTmpl(authMethodName string) (api.ACLAuthMethod err = c.untilSucceeds(fmt.Sprintf("getting %s Secret", secretRef.Name), func() error { var err error - secret, err = c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(context.TODO(), secretRef.Name, metav1.GetOptions{}) + secret, err = c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(c.ctx, secretRef.Name, metav1.GetOptions{}) return err }) if secret != nil && secret.Type == apiv1.SecretTypeServiceAccountToken { diff --git a/control-plane/subcommand/server-acl-init/connect_inject_test.go b/control-plane/subcommand/server-acl-init/connect_inject_test.go index cad81d8b3c..a17d635bc1 100644 --- a/control-plane/subcommand/server-acl-init/connect_inject_test.go +++ b/control-plane/subcommand/server-acl-init/connect_inject_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" @@ -19,23 +20,24 @@ import ( // Also note that the remainder of this function is tested in the command_test.go. func TestCommand_createAuthMethodTmpl_SecretNotFound(t *testing.T) { k8s := fake.NewSimpleClientset() + ctx := context.Background() cmd := &Command{ flagK8sNamespace: ns, flagResourcePrefix: resourcePrefix, clientset: k8s, log: hclog.New(nil), - cmdTimeout: context.TODO(), + ctx: ctx, } - serviceAccountName := resourcePrefix + "-connect-injector-authmethod-svc-account" - secretName := resourcePrefix + "-connect-injector-authmethod-svc-account" + serviceAccountName := resourcePrefix + "-connect-injector" + secretName := resourcePrefix + "-connect-injector" // Create a service account referencing secretName - sa, _ := k8s.CoreV1().ServiceAccounts(ns).Get(context.Background(), serviceAccountName, metav1.GetOptions{}) + sa, _ := k8s.CoreV1().ServiceAccounts(ns).Get(ctx, serviceAccountName, metav1.GetOptions{}) if sa == nil { _, err := k8s.CoreV1().ServiceAccounts(ns).Create( - context.Background(), + ctx, &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: serviceAccountName, @@ -53,14 +55,15 @@ func TestCommand_createAuthMethodTmpl_SecretNotFound(t *testing.T) { // Create a secret of non service-account-token type (we're using the opaque type). secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + Name: secretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{}, Type: v1.SecretTypeOpaque, } - _, err := k8s.CoreV1().Secrets(ns).Create(context.TODO(), secret, metav1.CreateOptions{}) + _, err := k8s.CoreV1().Secrets(ns).Create(ctx, secret, metav1.CreateOptions{}) require.NoError(t, err) _, err = cmd.createAuthMethodTmpl("test") - require.EqualError(t, err, "found no secret of type 'kubernetes.io/service-account-token' associated with the release-name-consul-connect-injector-authmethod-svc-account service account") + require.EqualError(t, err, "found no secret of type 'kubernetes.io/service-account-token' associated with the release-name-consul-connect-injector service account") } diff --git a/control-plane/subcommand/server-acl-init/create_or_update.go b/control-plane/subcommand/server-acl-init/create_or_update.go index 4710258c84..80dca054bf 100644 --- a/control-plane/subcommand/server-acl-init/create_or_update.go +++ b/control-plane/subcommand/server-acl-init/create_or_update.go @@ -1,7 +1,6 @@ package serveraclinit import ( - "context" "fmt" "strings" @@ -14,26 +13,34 @@ import ( // createLocalACL creates a policy and acl token for this dc (datacenter), i.e. // the policy is only valid for this datacenter and the token is a local token. func (c *Command) createLocalACL(name, rules, dc string, isPrimary bool, consulClient *api.Client) error { - return c.createACL(name, rules, true, dc, isPrimary, consulClient) + return c.createACL(name, rules, true, dc, isPrimary, consulClient, "") } // createGlobalACL creates a global policy and acl token. The policy is valid // for all datacenters and the token is global. dc must be passed because the // policy name may have the datacenter name appended. func (c *Command) createGlobalACL(name, rules, dc string, isPrimary bool, consulClient *api.Client) error { - return c.createACL(name, rules, false, dc, isPrimary, consulClient) + return c.createACL(name, rules, false, dc, isPrimary, consulClient, "") +} + +// createGlobalACLWithSecretID creates a global policy and acl token with provided secret ID. +func (c *Command) createGlobalACLWithSecretID(name, rules, dc string, isPrimary bool, consulClient *api.Client, secretID string) error { + return c.createACL(name, rules, false, dc, isPrimary, consulClient, secretID) } // createACL creates a policy with rules and name. If localToken is true then // the token will be a local token and the policy will be scoped to only dc. // If localToken is false, the policy will be global. // The token will be written to a Kubernetes secret. -func (c *Command) createACL(name, rules string, localToken bool, dc string, isPrimary bool, consulClient *api.Client) error { +// When secretID is provided, we will use that value for the created token and +// will skip writing it to a Kubernetes secret (because in this case we assume that +// this value already exists in some secrets storage). +func (c *Command) createACL(name, rules string, localToken bool, dc string, isPrimary bool, consulClient *api.Client, secretID string) error { // Create policy with the given rules. policyName := fmt.Sprintf("%s-token", name) if c.flagFederation && !isPrimary { // If performing ACL replication, we must ensure policy names are - // globally unique so we append the datacenter name but only in secondary datacenters.. + // globally unique so we append the datacenter name but only in secondary datacenters. policyName += fmt.Sprintf("-%s", dc) } var datacenters []string @@ -54,21 +61,37 @@ func (c *Command) createACL(name, rules string, localToken bool, dc string, isPr return err } - // Check if the secret already exists, if so, we assume the ACL has already been - // created and return. - secretName := c.withPrefix(name + "-acl-token") - _, err = c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(context.TODO(), secretName, metav1.GetOptions{}) - if err == nil { - c.log.Info(fmt.Sprintf("Secret %q already exists", secretName)) - return nil - } - // Create token for the policy if the secret did not exist previously. tokenTmpl := api.ACLToken{ Description: fmt.Sprintf("%s Token", policyTmpl.Name), Policies: []*api.ACLTokenPolicyLink{{Name: policyTmpl.Name}}, Local: localToken, } + + // Check if the replication token already exists in some form. + secretName := c.withPrefix(name + "-acl-token") + // When secretID is not provided, we assume that replication token should exist + // as a Kubernetes secret. + if secretID == "" { + // Check if the secret already exists, if so, we assume the ACL has already been + // created and return. + _, err = c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(c.ctx, secretName, metav1.GetOptions{}) + if err == nil { + c.log.Info(fmt.Sprintf("Secret %q already exists", secretName)) + return nil + } + } else { + // If secretID is provided, we check if the token with secretID already exists in Consul + // and exit if it does. Otherwise, set the secretID to the provided value. + _, _, err = consulClient.ACL().TokenReadSelf(&api.QueryOptions{Token: secretID}) + if err == nil { + c.log.Info("ACL replication token already exists; skipping creation") + return nil + } else { + tokenTmpl.SecretID = secretID + } + } + var token string err = c.untilSucceeds(fmt.Sprintf("creating token for policy %s", policyTmpl.Name), func() error { @@ -82,24 +105,28 @@ func (c *Command) createACL(name, rules string, localToken bool, dc string, isPr return err } - // Write token to a Kubernetes secret. - return c.untilSucceeds(fmt.Sprintf("writing Secret for token %s", policyTmpl.Name), - func() error { - secret := &apiv1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - }, - Data: map[string][]byte{ - common.ACLTokenSecretKey: []byte(token), - }, - } - _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) - return err - }) + if secretID == "" { + // Write token to a Kubernetes secret. + return c.untilSucceeds(fmt.Sprintf("writing Secret for token %s", policyTmpl.Name), + func() error { + secret := &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + Data: map[string][]byte{ + common.ACLTokenSecretKey: []byte(token), + }, + } + _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(c.ctx, secret, metav1.CreateOptions{}) + return err + }) + } + return nil } func (c *Command) createOrUpdateACLPolicy(policy api.ACLPolicy, consulClient *api.Client) error { - // Attempt to create the ACL policy + // Attempt to create the ACL policy. _, _, err := consulClient.ACL().PolicyCreate(&policy, &api.WriteOptions{}) // With the introduction of Consul namespaces, if someone upgrades into a diff --git a/control-plane/subcommand/server-acl-init/create_or_update_test.go b/control-plane/subcommand/server-acl-init/create_or_update_test.go index 62775e786c..57cdffa2a1 100644 --- a/control-plane/subcommand/server-acl-init/create_or_update_test.go +++ b/control-plane/subcommand/server-acl-init/create_or_update_test.go @@ -30,7 +30,7 @@ func TestCreateOrUpdateACLPolicy_ErrorsIfDescriptionDoesNotMatch(t *testing.T) { bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" svr, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { c.ACL.Enabled = true - c.ACL.Tokens.Master = bootToken + c.ACL.Tokens.InitialManagement = bootToken }) require.NoError(err) svr.WaitForLeader(t) diff --git a/control-plane/subcommand/server-acl-init/rules.go b/control-plane/subcommand/server-acl-init/rules.go index 1b8a64b704..a634e9473d 100644 --- a/control-plane/subcommand/server-acl-init/rules.go +++ b/control-plane/subcommand/server-acl-init/rules.go @@ -7,6 +7,8 @@ import ( ) type rulesData struct { + EnablePartitions bool + PartitionName string EnableNamespaces bool SyncConsulDestNS string SyncEnableNSMirroring bool @@ -34,29 +36,62 @@ service "consul-snapshot" { policy = "write" }` +// The enterprise license rules are acl="write" inside partitions as operator="write" +// is unsupported in partitions. const entLicenseRules = `operator = "write"` +const entPartitionLicenseRules = `acl = "write"` -const crossNamespaceRules = `namespace_prefix "" { - service_prefix "" { - policy = "read" +// The partition token is utilized by the partition-init job and server-acl-init in +// non-default partitions. This token requires permissions to create partitions, read the +// agent endpoint during startup and have the ability to create an auth-method within a namespace +// for any partition. +const partitionRules = `operator = "write" +agent_prefix "" { + policy = "read" +} +partition_prefix "" { + namespace_prefix "" { + acl = "write" } - node_prefix "" { - policy = "read" +}` + +func (c *Command) crossNamespaceRules() (string, error) { + crossNamespaceRulesTpl := `{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { +{{- end }} + namespace_prefix "" { + service_prefix "" { + policy = "read" + } + node_prefix "" { + policy = "read" + } } -} ` +{{- if .EnablePartitions }} +} +{{- end }}` + + return c.renderRules(crossNamespaceRulesTpl) +} func (c *Command) agentRules() (string, error) { agentRulesTpl := ` +{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { +{{- end }} node_prefix "" { policy = "write" } {{- if .EnableNamespaces }} -namespace_prefix "" { + namespace_prefix "" { {{- end }} - service_prefix "" { - policy = "read" - } + service_prefix "" { + policy = "read" + } {{- if .EnableNamespaces }} + } +{{- end }} +{{- if .EnablePartitions }} } {{- end }} ` @@ -80,21 +115,57 @@ func (c *Command) anonymousTokenRules() (string, error) { // ACL token. Thus the anonymous policy must // allow reading all services. anonTokenRulesTpl := ` +{{- if .EnablePartitions }} +partition_prefix "" { +{{- end }} {{- if .EnableNamespaces }} -namespace_prefix "" { + namespace_prefix "" { {{- end }} - node_prefix "" { - policy = "read" + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } +{{- if .EnableNamespaces }} } +{{- end }} +{{- if .EnablePartitions }} +} +{{- end }} +` + + return c.renderRules(anonTokenRulesTpl) +} + +func (c *Command) apiGatewayControllerRules() (string, error) { + apiGatewayRulesTpl := `{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { + mesh = "write" + acl = "write" +{{- else }} +operator = "write" +acl = "write" +{{- end }} +{{- if .EnableNamespaces }} +namespace_prefix "" { +{{- end }} service_prefix "" { - policy = "read" + policy = "write" + intentions = "write" + } + node_prefix "" { + policy = "read" } {{- if .EnableNamespaces }} } {{- end }} -` +{{- if .EnablePartitions }} +} +{{- end }} + ` - return c.renderRules(anonTokenRulesTpl) + return c.renderRules(apiGatewayRulesTpl) } // This assumes users are using the default name for the service, i.e. @@ -133,19 +204,25 @@ namespace_prefix "" { func (c *Command) ingressGatewayRules(name, namespace string) (string, error) { ingressGatewayRulesTpl := ` +{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { +{{- end }} {{- if .EnableNamespaces }} -namespace "{{ .GatewayNamespace }}" { + namespace "{{ .GatewayNamespace }}" { {{- end }} - service "{{ .GatewayName }}" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" - } + service "{{ .GatewayName }}" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } {{- if .EnableNamespaces }} + } +{{- end }} +{{- if .EnablePartitions }} } {{- end }} ` @@ -156,19 +233,25 @@ namespace "{{ .GatewayNamespace }}" { // Creating a separate terminating gateway rule function because // eventually this may need to be created with permissions for // all of the services it represents, though that is not part -// of the initial implementation +// of the initial implementation. func (c *Command) terminatingGatewayRules(name, namespace string) (string, error) { terminatingGatewayRulesTpl := ` +{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { +{{- end }} {{- if .EnableNamespaces }} -namespace "{{ .GatewayNamespace }}" { + namespace "{{ .GatewayNamespace }}" { {{- end }} - service "{{ .GatewayName }}" { - policy = "write" - } - node_prefix "" { - policy = "read" - } + service "{{ .GatewayName }}" { + policy = "write" + } + node_prefix "" { + policy = "read" + } {{- if .EnableNamespaces }} + } +{{- end }} +{{- if .EnablePartitions }} } {{- end }} ` @@ -176,6 +259,9 @@ namespace "{{ .GatewayNamespace }}" { return c.renderGatewayRules(terminatingGatewayRulesTpl, name, namespace) } +// acl = "write" is required when creating namespace with a default policy. +// Attaching a default ACL policy to a namespace requires acl = "write" in the +// namespace that the policy is defined in, which in our case is "default". func (c *Command) syncRules() (string, error) { syncRulesTpl := ` node "{{ .SyncConsulNodeName }}" { @@ -183,6 +269,7 @@ func (c *Command) syncRules() (string, error) { } {{- if .EnableNamespaces }} operator = "write" +acl = "write" {{- if .SyncEnableNSMirroring }} namespace_prefix "{{ .SyncNSMirroringPrefix }}" { {{- else }} @@ -207,22 +294,33 @@ func (c *Command) injectRules() (string, error) { // The Connect injector needs permissions to create namespaces when namespaces are enabled. // It must also create/update service health checks via the endpoints controller. // When ACLs are enabled, the endpoints controller needs "acl:write" permissions - // to delete ACL tokens created via "consul login". + // to delete ACL tokens created via "consul login". policy = "write" is required when + // creating namespaces within a partition. injectRulesTpl := ` +{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { +{{- else }} {{- if .EnableNamespaces }} -operator = "write" + operator = "write" {{- end }} -node_prefix "" { - policy = "write" -} -{{- if .EnableNamespaces }} -namespace_prefix "" { {{- end }} - acl = "write" - service_prefix "" { + node_prefix "" { policy = "write" } {{- if .EnableNamespaces }} + namespace_prefix "" { +{{- end }} +{{- if .EnablePartitions }} + policy = "write" +{{- end }} + acl = "write" + service_prefix "" { + policy = "write" + } +{{- if .EnableNamespaces }} + } +{{- end }} +{{- if .EnablePartitions }} } {{- end }}` return c.renderRules(injectRulesTpl) @@ -237,43 +335,66 @@ func (c *Command) aclReplicationRules() (string, error) { // datacenters during federation since in order to start ACL replication, // we need a token with both replication and agent permissions. aclReplicationRulesTpl := ` -operator = "write" -agent_prefix "" { - policy = "read" -} -node_prefix "" { - policy = "write" -} -{{- if .EnableNamespaces }} -namespace_prefix "" { +{{- if .EnablePartitions }} +partition "default" { {{- end }} - acl = "write" - service_prefix "" { + operator = "write" + agent_prefix "" { policy = "read" - intentions = "read" } + node_prefix "" { + policy = "write" + } +{{- if .EnableNamespaces }} + namespace_prefix "" { +{{- end }} + acl = "write" + service_prefix "" { + policy = "read" + intentions = "read" + } {{- if .EnableNamespaces }} + } +{{- end }} +{{- if .EnablePartitions }} } {{- end }} ` return c.renderRules(aclReplicationRulesTpl) } +// policy = "write" is required when creating namespaces within a partition. +// acl = "write" is required when creating namespace with a default policy. +// Attaching a default ACL policy to a namespace requires acl = "write" in the +// namespace that the policy is defined in, which in our case is "default". func (c *Command) controllerRules() (string, error) { controllerRules := ` -operator = "write" +{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { + mesh = "write" + acl = "write" +{{- else }} + operator = "write" + acl = "write" +{{- end }} {{- if .EnableNamespaces }} {{- if .InjectEnableNSMirroring }} -namespace_prefix "{{ .InjectNSMirroringPrefix }}" { + namespace_prefix "{{ .InjectNSMirroringPrefix }}" { {{- else }} -namespace "{{ .InjectConsulDestNS }}" { + namespace "{{ .InjectConsulDestNS }}" { {{- end }} {{- end }} - service_prefix "" { +{{- if .EnablePartitions }} policy = "write" - intentions = "write" - } +{{- end }} + service_prefix "" { + policy = "write" + intentions = "write" + } {{- if .EnableNamespaces }} + } +{{- end }} +{{- if .EnablePartitions }} } {{- end }} ` @@ -282,6 +403,8 @@ namespace "{{ .InjectConsulDestNS }}" { func (c *Command) rulesData() rulesData { return rulesData{ + EnablePartitions: c.flagEnablePartitions, + PartitionName: c.flagPartitionName, EnableNamespaces: c.flagEnableNamespaces, SyncConsulDestNS: c.flagConsulSyncDestinationNamespace, SyncEnableNSMirroring: c.flagEnableSyncK8SNSMirroring, diff --git a/control-plane/subcommand/server-acl-init/rules_test.go b/control-plane/subcommand/server-acl-init/rules_test.go index 1160236eef..ba4eb88c1a 100644 --- a/control-plane/subcommand/server-acl-init/rules_test.go +++ b/control-plane/subcommand/server-acl-init/rules_test.go @@ -2,6 +2,7 @@ package serveraclinit import ( "fmt" + "strings" "testing" "github.com/stretchr/testify/require" @@ -10,28 +11,48 @@ import ( func TestAgentRules(t *testing.T) { cases := []struct { Name string + EnablePartitions bool + PartitionName string EnableNamespaces bool Expected string }{ { - "Namespaces are disabled", - false, - `node_prefix "" { + Name: "Namespaces and Partitions are disabled", + Expected: ` + node_prefix "" { policy = "write" } - service_prefix "" { - policy = "read" + service_prefix "" { + policy = "read" + }`, + }, + { + Name: "Namespaces are enabled, Partitions are disabled", + EnableNamespaces: true, + Expected: ` + node_prefix "" { + policy = "write" + } + namespace_prefix "" { + service_prefix "" { + policy = "read" + } }`, }, { - "Namespaces are enabled", - true, - `node_prefix "" { + Name: "Namespaces and Partitions are enabled", + EnablePartitions: true, + PartitionName: "part-1", + EnableNamespaces: true, + Expected: ` +partition "part-1" { + node_prefix "" { policy = "write" } -namespace_prefix "" { - service_prefix "" { - policy = "read" + namespace_prefix "" { + service_prefix "" { + policy = "read" + } } }`, }, @@ -39,16 +60,16 @@ namespace_prefix "" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - cmd := Command{ + flagEnablePartitions: tt.EnablePartitions, + flagPartitionName: tt.PartitionName, flagEnableNamespaces: tt.EnableNamespaces, } agentRules, err := cmd.agentRules() - require.NoError(err) - require.Equal(tt.Expected, agentRules) + require.NoError(t, err) + require.Equal(t, tt.Expected, agentRules) }) } } @@ -56,30 +77,101 @@ namespace_prefix "" { func TestAnonymousTokenRules(t *testing.T) { cases := []struct { Name string + EnablePartitions bool + PartitionName string EnableNamespaces bool Expected string }{ { - "Namespaces are disabled", - false, - ` - node_prefix "" { - policy = "read" + Name: "Namespaces and Partitions are disabled", + Expected: ` + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + }`, + }, + { + Name: "Namespaces are enabled, Partitions are disabled", + EnableNamespaces: true, + Expected: ` + namespace_prefix "" { + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } + }`, + }, + { + Name: "Namespaces and Partitions are enabled", + EnablePartitions: true, + PartitionName: "part-2", + EnableNamespaces: true, + Expected: ` +partition_prefix "" { + namespace_prefix "" { + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } +}`, + }, + } + + for _, tt := range cases { + t.Run(tt.Name, func(t *testing.T) { + cmd := Command{ + flagEnablePartitions: tt.EnablePartitions, + flagPartitionName: tt.PartitionName, + flagEnableNamespaces: tt.EnableNamespaces, + } + + rules, err := cmd.anonymousTokenRules() + + require.NoError(t, err) + require.Equal(t, tt.Expected, rules) + }) + } +} + +func TestAPIGatewayControllerRules(t *testing.T) { + cases := []struct { + Name string + EnableNamespaces bool + Expected string + }{ + { + Name: "Namespaces are disabled", + Expected: ` +operator = "write" +acl = "write" service_prefix "" { - policy = "read" + policy = "write" + intentions = "write" + } + node_prefix "" { + policy = "read" }`, }, { - "Namespaces are enabled", - true, - ` + Name: "Namespaces are enabled", + EnableNamespaces: true, + Expected: ` +operator = "write" +acl = "write" namespace_prefix "" { - node_prefix "" { - policy = "read" - } service_prefix "" { - policy = "read" + policy = "write" + intentions = "write" + } + node_prefix "" { + policy = "read" } }`, }, @@ -87,16 +179,14 @@ namespace_prefix "" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - cmd := Command{ flagEnableNamespaces: tt.EnableNamespaces, } - rules, err := cmd.anonymousTokenRules() + meshGatewayRules, err := cmd.apiGatewayControllerRules() - require.NoError(err) - require.Equal(tt.Expected, rules) + require.NoError(t, err) + require.Equal(t, tt.Expected, strings.Trim(meshGatewayRules, " ")) }) } } @@ -108,9 +198,8 @@ func TestMeshGatewayRules(t *testing.T) { Expected string }{ { - "Namespaces are disabled", - false, - `agent_prefix "" { + Name: "Namespaces are disabled", + Expected: `agent_prefix "" { policy = "read" } service "mesh-gateway" { @@ -124,9 +213,9 @@ func TestMeshGatewayRules(t *testing.T) { }`, }, { - "Namespaces are enabled", - true, - `agent_prefix "" { + Name: "Namespaces are enabled", + EnableNamespaces: true, + Expected: `agent_prefix "" { policy = "read" } namespace "default" { @@ -147,16 +236,14 @@ namespace_prefix "" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - cmd := Command{ flagEnableNamespaces: tt.EnableNamespaces, } meshGatewayRules, err := cmd.meshGatewayRules() - require.NoError(err) - require.Equal(tt.Expected, meshGatewayRules) + require.NoError(t, err) + require.Equal(t, tt.Expected, meshGatewayRules) }) } } @@ -166,58 +253,102 @@ func TestIngressGatewayRules(t *testing.T) { Name string GatewayName string GatewayNamespace string + EnablePartitions bool + PartitionName string EnableNamespaces bool Expected string }{ { - "Namespaces are disabled", - "ingress-gateway", - "", - false, - ` - service "ingress-gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" + Name: "Namespaces and Partitions are disabled", + GatewayName: "ingress-gateway", + Expected: ` + service "ingress-gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + }`, + }, + { + Name: "Namespaces are enabled, Partitions are disabled", + GatewayName: "gateway", + GatewayNamespace: "default", + EnableNamespaces: true, + Expected: ` + namespace "default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } }`, }, { - "Namespaces are enabled", - "gateway", - "default", - true, - ` -namespace "default" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" + Name: "Namespaces are enabled, non-default namespace, Partitions are disabled", + GatewayName: "gateway", + GatewayNamespace: "non-default", + EnableNamespaces: true, + Expected: ` + namespace "non-default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } + }`, + }, + { + Name: "Namespaces and Partitions are enabled", + GatewayName: "gateway", + GatewayNamespace: "default", + EnableNamespaces: true, + EnablePartitions: true, + PartitionName: "part-1", + Expected: ` +partition "part-1" { + namespace "default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`, }, { - "Namespaces are enabled, non-default namespace", - "gateway", - "non-default", - true, - ` -namespace "non-default" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" - } - service_prefix "" { - policy = "read" + Name: "Namespaces and Partitions are enabled, non-default namespace", + GatewayName: "gateway", + GatewayNamespace: "non-default", + EnableNamespaces: true, + EnablePartitions: true, + PartitionName: "default", + Expected: ` +partition "default" { + namespace "non-default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + service_prefix "" { + policy = "read" + } } }`, }, @@ -225,16 +356,16 @@ namespace "non-default" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - cmd := Command{ + flagEnablePartitions: tt.EnablePartitions, + flagPartitionName: tt.PartitionName, flagEnableNamespaces: tt.EnableNamespaces, } ingressGatewayRules, err := cmd.ingressGatewayRules(tt.GatewayName, tt.GatewayNamespace) - require.NoError(err) - require.Equal(tt.Expected, ingressGatewayRules) + require.NoError(t, err) + require.Equal(t, tt.Expected, ingressGatewayRules) }) } } @@ -245,48 +376,86 @@ func TestTerminatingGatewayRules(t *testing.T) { GatewayName string GatewayNamespace string EnableNamespaces bool + EnablePartitions bool + PartitionName string Expected string }{ { - "Namespaces are disabled", - "terminating-gateway", - "", - false, - ` - service "terminating-gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" + Name: "Namespaces and Partitions are disabled", + GatewayName: "terminating-gateway", + Expected: ` + service "terminating-gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + }`, + }, + { + Name: "Namespaces are enabled, Partitions are disabled", + GatewayName: "gateway", + GatewayNamespace: "default", + EnableNamespaces: true, + Expected: ` + namespace "default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } }`, }, { - "Namespaces are enabled", - "gateway", - "default", - true, - ` -namespace "default" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" + Name: "Namespaces are enabled, non-default namespace, Partitions are disabled", + GatewayName: "gateway", + GatewayNamespace: "non-default", + EnableNamespaces: true, + Expected: ` + namespace "non-default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } + }`, + }, + { + Name: "Namespaces and Partitions are enabled", + GatewayName: "gateway", + GatewayNamespace: "default", + EnableNamespaces: true, + EnablePartitions: true, + PartitionName: "part-1", + Expected: ` +partition "part-1" { + namespace "default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`, }, { - "Namespaces are enabled, non-default namespace", - "gateway", - "non-default", - true, - ` -namespace "non-default" { - service "gateway" { - policy = "write" - } - node_prefix "" { - policy = "read" + Name: "Namespaces and Partitions are enabled, non-default namespace", + GatewayName: "gateway", + GatewayNamespace: "non-default", + EnableNamespaces: true, + EnablePartitions: true, + PartitionName: "default", + Expected: ` +partition "default" { + namespace "non-default" { + service "gateway" { + policy = "write" + } + node_prefix "" { + policy = "read" + } } }`, }, @@ -294,16 +463,16 @@ namespace "non-default" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - cmd := Command{ + flagEnablePartitions: tt.EnablePartitions, + flagPartitionName: tt.PartitionName, flagEnableNamespaces: tt.EnableNamespaces, } terminatingGatewayRules, err := cmd.terminatingGatewayRules(tt.GatewayName, tt.GatewayNamespace) - require.NoError(err) - require.Equal(tt.Expected, terminatingGatewayRules) + require.NoError(t, err) + require.Equal(t, tt.Expected, terminatingGatewayRules) }) } } @@ -319,13 +488,12 @@ func TestSyncRules(t *testing.T) { Expected string }{ { - "Namespaces are disabled", - false, - "sync-namespace", - true, - "prefix-", - "k8s-sync", - `node "k8s-sync" { + Name: "Namespaces are disabled", + ConsulSyncDestinationNamespace: "sync-namespace", + EnableSyncK8SNSMirroring: true, + SyncK8SNSMirroringPrefix: "prefix-", + SyncConsulNodeName: "k8s-sync", + Expected: `node "k8s-sync" { policy = "write" } node_prefix "" { @@ -336,13 +504,12 @@ func TestSyncRules(t *testing.T) { }`, }, { - "Namespaces are disabled, non-default node name", - false, - "sync-namespace", - true, - "prefix-", - "new-node-name", - `node "new-node-name" { + Name: "Namespaces are disabled, non-default node name", + ConsulSyncDestinationNamespace: "sync-namespace", + EnableSyncK8SNSMirroring: true, + SyncK8SNSMirroringPrefix: "prefix-", + SyncConsulNodeName: "new-node-name", + Expected: `node "new-node-name" { policy = "write" } node_prefix "" { @@ -353,16 +520,16 @@ func TestSyncRules(t *testing.T) { }`, }, { - "Namespaces are enabled, mirroring disabled", - true, - "sync-namespace", - false, - "prefix-", - "k8s-sync", - `node "k8s-sync" { + Name: "Namespaces are enabled, mirroring disabled", + EnableNamespaces: true, + ConsulSyncDestinationNamespace: "sync-namespace", + SyncK8SNSMirroringPrefix: "prefix-", + SyncConsulNodeName: "k8s-sync", + Expected: `node "k8s-sync" { policy = "write" } operator = "write" +acl = "write" namespace "sync-namespace" { node_prefix "" { policy = "read" @@ -373,16 +540,16 @@ namespace "sync-namespace" { }`, }, { - "Namespaces are enabled, mirroring disabled, non-default node name", - true, - "sync-namespace", - false, - "prefix-", - "new-node-name", - `node "new-node-name" { + Name: "Namespaces are enabled, mirroring disabled, non-default node name", + EnableNamespaces: true, + ConsulSyncDestinationNamespace: "sync-namespace", + SyncK8SNSMirroringPrefix: "prefix-", + SyncConsulNodeName: "new-node-name", + Expected: `node "new-node-name" { policy = "write" } operator = "write" +acl = "write" namespace "sync-namespace" { node_prefix "" { policy = "read" @@ -393,16 +560,16 @@ namespace "sync-namespace" { }`, }, { - "Namespaces are enabled, mirroring enabled, prefix empty", - true, - "sync-namespace", - true, - "", - "k8s-sync", - `node "k8s-sync" { + Name: "Namespaces are enabled, mirroring enabled, prefix empty", + EnableNamespaces: true, + ConsulSyncDestinationNamespace: "sync-namespace", + EnableSyncK8SNSMirroring: true, + SyncConsulNodeName: "k8s-sync", + Expected: `node "k8s-sync" { policy = "write" } operator = "write" +acl = "write" namespace_prefix "" { node_prefix "" { policy = "read" @@ -413,16 +580,16 @@ namespace_prefix "" { }`, }, { - "Namespaces are enabled, mirroring enabled, prefix empty, non-default node name", - true, - "sync-namespace", - true, - "", - "new-node-name", - `node "new-node-name" { + Name: "Namespaces are enabled, mirroring enabled, prefix empty, non-default node name", + EnableNamespaces: true, + ConsulSyncDestinationNamespace: "sync-namespace", + EnableSyncK8SNSMirroring: true, + SyncConsulNodeName: "new-node-name", + Expected: `node "new-node-name" { policy = "write" } operator = "write" +acl = "write" namespace_prefix "" { node_prefix "" { policy = "read" @@ -433,16 +600,17 @@ namespace_prefix "" { }`, }, { - "Namespaces are enabled, mirroring enabled, prefix defined", - true, - "sync-namespace", - true, - "prefix-", - "k8s-sync", - `node "k8s-sync" { + Name: "Namespaces are enabled, mirroring enabled, prefix defined", + EnableNamespaces: true, + ConsulSyncDestinationNamespace: "sync-namespace", + EnableSyncK8SNSMirroring: true, + SyncK8SNSMirroringPrefix: "prefix-", + SyncConsulNodeName: "k8s-sync", + Expected: `node "k8s-sync" { policy = "write" } operator = "write" +acl = "write" namespace_prefix "prefix-" { node_prefix "" { policy = "read" @@ -453,16 +621,17 @@ namespace_prefix "prefix-" { }`, }, { - "Namespaces are enabled, mirroring enabled, prefix defined, non-default node name", - true, - "sync-namespace", - true, - "prefix-", - "new-node-name", - `node "new-node-name" { + Name: "Namespaces are enabled, mirroring enabled, prefix defined, non-default node name", + EnableNamespaces: true, + ConsulSyncDestinationNamespace: "sync-namespace", + EnableSyncK8SNSMirroring: true, + SyncK8SNSMirroringPrefix: "prefix-", + SyncConsulNodeName: "new-node-name", + Expected: `node "new-node-name" { policy = "write" } operator = "write" +acl = "write" namespace_prefix "prefix-" { node_prefix "" { policy = "read" @@ -476,8 +645,6 @@ namespace_prefix "prefix-" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - cmd := Command{ flagEnableNamespaces: tt.EnableNamespaces, flagConsulSyncDestinationNamespace: tt.ConsulSyncDestinationNamespace, @@ -488,8 +655,8 @@ namespace_prefix "prefix-" { syncRules, err := cmd.syncRules() - require.NoError(err) - require.Equal(tt.Expected, syncRules) + require.NoError(t, err) + require.Equal(t, tt.Expected, syncRules) }) } } @@ -498,48 +665,71 @@ namespace_prefix "prefix-" { func TestInjectRules(t *testing.T) { cases := []struct { EnableNamespaces bool + EnablePartitions bool + PartitionName string Expected string }{ { EnableNamespaces: false, + EnablePartitions: false, Expected: ` -node_prefix "" { - policy = "write" -} - acl = "write" - service_prefix "" { + node_prefix "" { policy = "write" + } + acl = "write" + service_prefix "" { + policy = "write" + }`, + }, + { + EnableNamespaces: true, + EnablePartitions: false, + Expected: ` + operator = "write" + node_prefix "" { + policy = "write" + } + namespace_prefix "" { + acl = "write" + service_prefix "" { + policy = "write" + } }`, }, { EnableNamespaces: true, + EnablePartitions: true, + PartitionName: "part-1", Expected: ` -operator = "write" -node_prefix "" { - policy = "write" -} -namespace_prefix "" { - acl = "write" - service_prefix "" { +partition "part-1" { + node_prefix "" { policy = "write" } + namespace_prefix "" { + policy = "write" + acl = "write" + service_prefix "" { + policy = "write" + } + } }`, }, } for _, tt := range cases { - caseName := fmt.Sprintf("ns=%t", tt.EnableNamespaces) + caseName := fmt.Sprintf("ns=%t, partition=%t", tt.EnableNamespaces, tt.EnablePartitions) t.Run(caseName, func(t *testing.T) { - require := require.New(t) cmd := Command{ + flagEnablePartitions: tt.EnablePartitions, + flagPartitionName: tt.PartitionName, flagEnableNamespaces: tt.EnableNamespaces, } injectorRules, err := cmd.injectRules() - require.NoError(err) - require.Equal(tt.Expected, injectorRules) + require.NoError(t, err) + require.Equal(t, tt.Expected, injectorRules) }) } } @@ -548,39 +738,65 @@ func TestReplicationTokenRules(t *testing.T) { cases := []struct { Name string EnableNamespaces bool + EnablePartitions bool + PartitionName string Expected string }{ { - "Namespaces are disabled", - false, - `operator = "write" -agent_prefix "" { - policy = "read" -} -node_prefix "" { - policy = "write" -} - acl = "write" - service_prefix "" { + Name: "Namespaces and Partitions are disabled", + Expected: ` + operator = "write" + agent_prefix "" { + policy = "read" + } + node_prefix "" { + policy = "write" + } + acl = "write" + service_prefix "" { + policy = "read" + intentions = "read" + }`, + }, + { + Name: "Namespaces are enabled, Partitions are disabled", + EnableNamespaces: true, + Expected: ` + operator = "write" + agent_prefix "" { policy = "read" - intentions = "read" + } + node_prefix "" { + policy = "write" + } + namespace_prefix "" { + acl = "write" + service_prefix "" { + policy = "read" + intentions = "read" + } }`, }, { - "Namespaces are enabled", - true, - `operator = "write" -agent_prefix "" { - policy = "read" -} -node_prefix "" { - policy = "write" -} -namespace_prefix "" { - acl = "write" - service_prefix "" { + Name: "Namespaces and Partitions are enabled, default partition", + EnableNamespaces: true, + EnablePartitions: true, + PartitionName: "default", + Expected: ` +partition "default" { + operator = "write" + agent_prefix "" { policy = "read" - intentions = "read" + } + node_prefix "" { + policy = "write" + } + namespace_prefix "" { + acl = "write" + service_prefix "" { + policy = "read" + intentions = "read" + } } }`, }, @@ -588,13 +804,14 @@ namespace_prefix "" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) cmd := Command{ + flagEnablePartitions: tt.EnablePartitions, + flagPartitionName: tt.PartitionName, flagEnableNamespaces: tt.EnableNamespaces, } replicationTokenRules, err := cmd.aclReplicationRules() - require.NoError(err) - require.Equal(tt.Expected, replicationTokenRules) + require.NoError(t, err) + require.Equal(t, tt.Expected, replicationTokenRules) }) } } @@ -602,6 +819,8 @@ namespace_prefix "" { func TestControllerRules(t *testing.T) { cases := []struct { Name string + EnablePartitions bool + PartitionName string EnableNamespaces bool DestConsulNS string Mirroring bool @@ -609,48 +828,113 @@ func TestControllerRules(t *testing.T) { Expected string }{ { - Name: "namespaces=disabled", - EnableNamespaces: false, - Expected: `operator = "write" - service_prefix "" { - policy = "write" - intentions = "write" + Name: "namespaces=disabled, partitions=disabled", + Expected: ` + operator = "write" + acl = "write" + service_prefix "" { + policy = "write" + intentions = "write" + }`, + }, + { + Name: "namespaces=enabled, consulDestNS=consul, partitions=disabled", + EnableNamespaces: true, + DestConsulNS: "consul", + Expected: ` + operator = "write" + acl = "write" + namespace "consul" { + service_prefix "" { + policy = "write" + intentions = "write" + } + }`, + }, + { + Name: "namespaces=enabled, mirroring=true, partitions=disabled", + EnableNamespaces: true, + Mirroring: true, + Expected: ` + operator = "write" + acl = "write" + namespace_prefix "" { + service_prefix "" { + policy = "write" + intentions = "write" + } }`, }, { - Name: "namespaces=enabled, consulDestNS=consul", + Name: "namespaces=enabled, mirroring=true, mirroringPrefix=prefix-, partitions=disabled", + EnableNamespaces: true, + Mirroring: true, + MirroringPrefix: "prefix-", + Expected: ` + operator = "write" + acl = "write" + namespace_prefix "prefix-" { + service_prefix "" { + policy = "write" + intentions = "write" + } + }`, + }, + { + Name: "namespaces=enabled, consulDestNS=consul, partitions=enabled", + EnablePartitions: true, + PartitionName: "part-1", EnableNamespaces: true, DestConsulNS: "consul", - Expected: `operator = "write" -namespace "consul" { - service_prefix "" { + Expected: ` +partition "part-1" { + mesh = "write" + acl = "write" + namespace "consul" { policy = "write" - intentions = "write" + service_prefix "" { + policy = "write" + intentions = "write" + } } }`, }, { - Name: "namespaces=enabled, mirroring=true", + Name: "namespaces=enabled, mirroring=true, partitions=enabled", + EnablePartitions: true, + PartitionName: "part-1", EnableNamespaces: true, Mirroring: true, - Expected: `operator = "write" -namespace_prefix "" { - service_prefix "" { + Expected: ` +partition "part-1" { + mesh = "write" + acl = "write" + namespace_prefix "" { policy = "write" - intentions = "write" + service_prefix "" { + policy = "write" + intentions = "write" + } } }`, }, { - Name: "namespaces=enabled, mirroring=true, mirroringPrefix=prefix-", + Name: "namespaces=enabled, mirroring=true, mirroringPrefix=prefix-, partitions=enabled", + EnablePartitions: true, + PartitionName: "part-1", EnableNamespaces: true, Mirroring: true, MirroringPrefix: "prefix-", - Expected: `operator = "write" -namespace_prefix "prefix-" { - service_prefix "" { + Expected: ` +partition "part-1" { + mesh = "write" + acl = "write" + namespace_prefix "prefix-" { policy = "write" - intentions = "write" + service_prefix "" { + policy = "write" + intentions = "write" + } } }`, }, @@ -658,19 +942,19 @@ namespace_prefix "prefix-" { for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { - require := require.New(t) - cmd := Command{ flagEnableNamespaces: tt.EnableNamespaces, flagConsulInjectDestinationNamespace: tt.DestConsulNS, flagEnableInjectK8SNSMirroring: tt.Mirroring, flagInjectK8SNSMirroringPrefix: tt.MirroringPrefix, + flagEnablePartitions: tt.EnablePartitions, + flagPartitionName: tt.PartitionName, } rules, err := cmd.controllerRules() - require.NoError(err) - require.Equal(tt.Expected, rules) + require.NoError(t, err) + require.Equal(t, tt.Expected, rules) }) } } diff --git a/control-plane/subcommand/server-acl-init/servers.go b/control-plane/subcommand/server-acl-init/servers.go index 8e4b782481..0f0ab8a0d1 100644 --- a/control-plane/subcommand/server-acl-init/servers.go +++ b/control-plane/subcommand/server-acl-init/servers.go @@ -1,22 +1,57 @@ package serveraclinit import ( - "context" "errors" "fmt" "strings" - "github.com/hashicorp/consul-k8s/control-plane/consul" - "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" "github.com/hashicorp/consul/api" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/hashicorp/consul-k8s/control-plane/consul" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" ) // bootstrapServers bootstraps ACLs and ensures each server has an ACL token. -func (c *Command) bootstrapServers(serverAddresses []string, bootTokenSecretName, scheme string) (string, error) { +// If bootstrapToken is not empty then ACLs are already bootstrapped. +func (c *Command) bootstrapServers(serverAddresses []string, bootstrapToken, bootTokenSecretName, scheme string) (string, error) { // Pick the first server address to connect to for bootstrapping and set up connection. firstServerAddr := fmt.Sprintf("%s:%d", serverAddresses[0], c.flagServerPort) + + if bootstrapToken == "" { + var err error + bootstrapToken, err = c.bootstrapACLs(firstServerAddr, scheme, bootTokenSecretName) + if err != nil { + return "", err + } + } + + // Override our original client with a new one that has the bootstrap token + // set. + consulClient, err := consul.NewClient(&api.Config{ + Address: firstServerAddr, + Scheme: scheme, + Token: bootstrapToken, + TLSConfig: api.TLSConfig{ + Address: c.flagConsulTLSServerName, + CAFile: c.flagConsulCACert, + }, + }) + if err != nil { + return "", fmt.Errorf("creating Consul client for address %s: %s", firstServerAddr, err) + } + + // Create new tokens for each server and apply them. + if err := c.setServerTokens(consulClient, serverAddresses, bootstrapToken, scheme); err != nil { + return "", err + } + return bootstrapToken, nil +} + +// bootstrapACLs makes the ACL bootstrap API call and writes the bootstrap token +// to a kube secret. +func (c *Command) bootstrapACLs(firstServerAddr string, scheme string, bootTokenSecretName string) (string, error) { consulClient, err := consul.NewClient(&api.Config{ Address: firstServerAddr, Scheme: scheme, @@ -30,13 +65,13 @@ func (c *Command) bootstrapServers(serverAddresses []string, bootTokenSecretName } // Call bootstrap ACLs API. - var bootstrapToken []byte + var bootstrapToken string var unrecoverableErr error err = c.untilSucceeds("bootstrapping ACLs - PUT /v1/acl/bootstrap", func() error { bootstrapResp, _, err := consulClient.ACL().Bootstrap() if err == nil { - bootstrapToken = []byte(bootstrapResp.SecretID) + bootstrapToken = bootstrapResp.SecretID return nil } @@ -67,39 +102,17 @@ func (c *Command) bootstrapServers(serverAddresses []string, bootTokenSecretName func() error { secret := &apiv1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: bootTokenSecretName, + Name: bootTokenSecretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ - common.ACLTokenSecretKey: bootstrapToken, + common.ACLTokenSecretKey: []byte(bootstrapToken), }, } - _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) + _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(c.ctx, secret, metav1.CreateOptions{}) return err }) - if err != nil { - return "", err - } - - // Override our original client with a new one that has the bootstrap token - // set. - consulClient, err = consul.NewClient(&api.Config{ - Address: firstServerAddr, - Scheme: scheme, - Token: string(bootstrapToken), - TLSConfig: api.TLSConfig{ - Address: c.flagConsulTLSServerName, - CAFile: c.flagConsulCACert, - }, - }) - if err != nil { - return "", fmt.Errorf("creating Consul client for address %s: %s", firstServerAddr, err) - } - - // Create new tokens for each server and apply them. - if err := c.setServerTokens(consulClient, serverAddresses, string(bootstrapToken), scheme); err != nil { - return "", err - } - return string(bootstrapToken), nil + return bootstrapToken, err } // setServerTokens creates policies and associated ACL token for each server @@ -110,9 +123,14 @@ func (c *Command) setServerTokens(consulClient *api.Client, serverAddresses []st return err } + existingTokens, _, err := consulClient.ACL().TokenList(nil) + if err != nil { + return err + } + // Create agent token for each server agent. for _, host := range serverAddresses { - var token *api.ACLToken + var tokenSecretID string // We create a new client for each server because we need to call each // server specifically. @@ -129,26 +147,44 @@ func (c *Command) setServerTokens(consulClient *api.Client, serverAddresses []st return err } - // Create token for the server - err = c.untilSucceeds(fmt.Sprintf("creating server token for %s - PUT /v1/acl/token", host), - func() error { - tokenReq := api.ACLToken{ - Description: fmt.Sprintf("Server Token for %s", host), - Policies: []*api.ACLTokenPolicyLink{{Name: agentPolicy.Name}}, + tokenDescription := fmt.Sprintf("Server Token for %s", host) + + // Check if the token was already created. We're matching on the description + // since that's the only part that's unique. + for _, t := range existingTokens { + if len(t.Policies) == 1 && t.Policies[0].Name == agentPolicy.Name { + if t.Description == tokenDescription { + tokenSecretID = t.SecretID + break } - var err error - token, _, err = serverClient.ACL().TokenCreate(&tokenReq, nil) + } + } + + // Create token for the server if it doesn't already exist. + if tokenSecretID == "" { + err = c.untilSucceeds(fmt.Sprintf("creating server token for %s - PUT /v1/acl/token", host), + func() error { + tokenReq := api.ACLToken{ + Description: tokenDescription, + Policies: []*api.ACLTokenPolicyLink{{Name: agentPolicy.Name}}, + } + token, _, err := serverClient.ACL().TokenCreate(&tokenReq, nil) + if err != nil { + return err + } + tokenSecretID = token.SecretID + return nil + }) + if err != nil { return err - }) - if err != nil { - return err + } } - // Pass out agent tokens to servers. - // Update token. + // Pass out agent tokens to servers. It's okay to make this API call + // even if the server already has a token since the call is idempotent. err = c.untilSucceeds(fmt.Sprintf("updating server token for %s - PUT /v1/agent/token/agent", host), func() error { - _, err := serverClient.Agent().UpdateAgentACLToken(token.SecretID, nil) + _, err := serverClient.Agent().UpdateAgentACLToken(tokenSecretID, nil) return err }) if err != nil { diff --git a/control-plane/subcommand/service-address/command.go b/control-plane/subcommand/service-address/command.go index 80e41ecf3a..d775b65ba1 100644 --- a/control-plane/subcommand/service-address/command.go +++ b/control-plane/subcommand/service-address/command.go @@ -39,6 +39,8 @@ type Command struct { k8sClient kubernetes.Interface once sync.Once help string + + ctx context.Context } func (c *Command) init() { @@ -92,11 +94,15 @@ func (c *Command) Run(args []string) int { return 1 } + if c.ctx == nil { + c.ctx = context.Background() + } + // Run until we get an address from the service. var address string var unretryableErr error err = backoff.Retry(withErrLogger(logger, func() error { - svc, err := c.k8sClient.CoreV1().Services(c.flagNamespace).Get(context.TODO(), c.flagServiceName, metav1.GetOptions{}) + svc, err := c.k8sClient.CoreV1().Services(c.flagNamespace).Get(c.ctx, c.flagServiceName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("getting service %s: %s", c.flagServiceName, err) } diff --git a/control-plane/subcommand/sync-catalog/command.go b/control-plane/subcommand/sync-catalog/command.go index be893f0a54..105ce6619c 100644 --- a/control-plane/subcommand/sync-catalog/command.go +++ b/control-plane/subcommand/sync-catalog/command.go @@ -262,6 +262,7 @@ func (c *Command) Run(args []string) int { Log: c.logger.Named("to-consul/source"), Client: c.clientset, Syncer: syncer, + Ctx: ctx, AllowK8sNamespacesSet: allowSet, DenyK8sNamespacesSet: denySet, ExplicitEnable: !c.flagK8SDefault, @@ -293,6 +294,7 @@ func (c *Command) Run(args []string) int { Client: c.clientset, Namespace: c.flagK8SWriteNamespace, Log: c.logger.Named("to-k8s/sink"), + Ctx: ctx, } source := &catalogtok8s.Source{ @@ -380,7 +382,7 @@ func (c *Command) Help() string { } // interrupt sends os.Interrupt signal to the command -// so it can exit gracefully. This function is needed for tests +// so it can exit gracefully. This function is needed for tests. func (c *Command) interrupt() { c.sendSignal(syscall.SIGINT) } diff --git a/control-plane/subcommand/sync-catalog/command_ent_test.go b/control-plane/subcommand/sync-catalog/command_ent_test.go index 80af158ea4..4e5ba14e93 100644 --- a/control-plane/subcommand/sync-catalog/command_ent_test.go +++ b/control-plane/subcommand/sync-catalog/command_ent_test.go @@ -1,4 +1,4 @@ -// +build enterprise +//go:build enterprise package synccatalog diff --git a/control-plane/subcommand/sync-catalog/command_test.go b/control-plane/subcommand/sync-catalog/command_test.go index 3956b24f2d..6cc629336e 100644 --- a/control-plane/subcommand/sync-catalog/command_test.go +++ b/control-plane/subcommand/sync-catalog/command_test.go @@ -18,7 +18,7 @@ import ( "k8s.io/client-go/kubernetes/fake" ) -// Test flag validation +// Test flag validation. func TestRun_FlagValidation(t *testing.T) { t.Parallel() @@ -51,7 +51,7 @@ func TestRun_FlagValidation(t *testing.T) { } } -// Test that the default consul service is synced to k8s +// Test that the default consul service is synced to k8s. func TestRun_Defaults_SyncsConsulServiceToK8s(t *testing.T) { t.Parallel() @@ -83,7 +83,7 @@ func TestRun_Defaults_SyncsConsulServiceToK8s(t *testing.T) { }) } -// Test that the command exits cleanly on signals +// Test that the command exits cleanly on signals. func TestRun_ExitCleanlyOnSignals(t *testing.T) { t.Run("SIGINT", testSignalHandling(syscall.SIGINT)) t.Run("SIGTERM", testSignalHandling(syscall.SIGTERM)) @@ -126,7 +126,7 @@ func testSignalHandling(sig os.Signal) func(*testing.T) { } // Test that when -add-k8s-namespace-suffix flag is used -// k8s namespaces are appended to the service names synced to Consul +// k8s namespaces are appended to the service names synced to Consul. func TestRun_ToConsulWithAddK8SNamespaceSuffix(t *testing.T) { t.Parallel() @@ -171,7 +171,7 @@ func TestRun_ToConsulWithAddK8SNamespaceSuffix(t *testing.T) { } // Test that switching AddK8SNamespaceSuffix from false to true -// results in re-registering services in Consul with namespaced names +// results in re-registering services in Consul with namespaced names. func TestCommand_Run_ToConsulChangeAddK8SNamespaceSuffixToTrue(t *testing.T) { t.Parallel() @@ -231,7 +231,7 @@ func TestCommand_Run_ToConsulChangeAddK8SNamespaceSuffixToTrue(t *testing.T) { // Test that services with same name but in different namespaces // get registered as different services in consul -// when using -add-k8s-namespace-suffix +// when using -add-k8s-namespace-suffix. func TestCommand_Run_ToConsulTwoServicesSameNameDifferentNamespace(t *testing.T) { t.Parallel() @@ -574,7 +574,7 @@ func TestRun_ToConsulChangingFlags(t *testing.T) { } } -// Set up test consul agent and fake kubernetes cluster client +// Set up test consul agent and fake kubernetes cluster client. func completeSetup(t *testing.T) (*fake.Clientset, *testutil.TestServer) { k8s := fake.NewSimpleClientset() diff --git a/control-plane/subcommand/tls-init/command.go b/control-plane/subcommand/tls-init/command.go index f40a900226..7a038b79b2 100644 --- a/control-plane/subcommand/tls-init/command.go +++ b/control-plane/subcommand/tls-init/command.go @@ -136,6 +136,7 @@ func (c *Command) Run(args []string) int { ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-ca-cert", c.flagNamePrefix), Namespace: c.flagK8sNamespace, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ corev1.TLSCertKey: []byte(ca), @@ -152,6 +153,7 @@ func (c *Command) Run(args []string) int { ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-ca-key", c.flagNamePrefix), Namespace: c.flagK8sNamespace, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ corev1.TLSPrivateKeyKey: []byte(pk), @@ -237,6 +239,7 @@ func (c *Command) Run(args []string) int { ObjectMeta: metav1.ObjectMeta{ Namespace: c.flagK8sNamespace, Name: fmt.Sprintf("%s-server-cert", c.flagNamePrefix), + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ corev1.TLSCertKey: []byte(serverCert), @@ -253,6 +256,13 @@ func (c *Command) Run(args []string) int { corev1.TLSCertKey: []byte(serverCert), corev1.TLSPrivateKeyKey: []byte(serverKey), } + + if serverCertSecret.ObjectMeta.Labels == nil { + serverCertSecret.ObjectMeta.Labels = map[string]string{common.CLILabelKey: common.CLILabelValue} + } else { + serverCertSecret.ObjectMeta.Labels[common.CLILabelKey] = common.CLILabelValue + } + c.log.Info("updating server certificate and private key secret") _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Update(c.ctx, serverCertSecret, metav1.UpdateOptions{}) if err != nil { diff --git a/control-plane/subcommand/tls-init/command_test.go b/control-plane/subcommand/tls-init/command_test.go index 7405754e18..e22254008a 100644 --- a/control-plane/subcommand/tls-init/command_test.go +++ b/control-plane/subcommand/tls-init/command_test.go @@ -58,50 +58,79 @@ func TestRun_FlagValidation(t *testing.T) { } func TestRun_CreatesServerCertificatesWithExistingCAAsFiles(t *testing.T) { - ui := cli.NewMockUi() - cmd := Command{UI: ui} - k8s := fake.NewSimpleClientset() - cmd.clientset = k8s - ca, err := ioutil.TempFile("", "") - require.NoError(t, err) - defer os.RemoveAll(ca.Name()) - err = ioutil.WriteFile(ca.Name(), []byte(caCert), 0644) - require.NoError(t, err) + cases := []struct { + caCert string + caKey string + algorithm string + }{ + { + caCert: caCertEC, + caKey: caKeyEC, + algorithm: "ec", + }, + { + caCert: caCertRSA, + caKey: caKeyRSA, + algorithm: "rsa", + }, + { + // caCertRSA is used because the key is just caKeyRSA encrypted. + caCert: caCertRSA, + caKey: caKeyPKCS8, + algorithm: "pkcs8", + }, + } - key, err := ioutil.TempFile("", "") - require.NoError(t, err) - defer os.RemoveAll(key.Name()) - err = ioutil.WriteFile(key.Name(), []byte(caKey), 0644) - require.NoError(t, err) + for _, c := range cases { + t.Run(c.algorithm, func(t *testing.T) { + ui := cli.NewMockUi() + cmd := Command{UI: ui} + k8s := fake.NewSimpleClientset() + cmd.clientset = k8s - flags := []string{"-name-prefix", "consul", "-ca", ca.Name(), "-key", key.Name()} + ca, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.RemoveAll(ca.Name()) + err = ioutil.WriteFile(ca.Name(), []byte(c.caCert), 0644) + require.NoError(t, err) - exitCode := cmd.Run(flags) - require.Equal(t, 0, exitCode) + key, err := ioutil.TempFile("", "") + require.NoError(t, err) + defer os.RemoveAll(key.Name()) + err = ioutil.WriteFile(key.Name(), []byte(c.caKey), 0644) + require.NoError(t, err) - caCertBlock, _ := pem.Decode([]byte(caCert)) - caCertificate, err := x509.ParseCertificate(caCertBlock.Bytes) - require.NoError(t, err) + flags := []string{"-name-prefix", "consul", "-ca", ca.Name(), "-key", key.Name()} - serverCertSecret, err := k8s.CoreV1().Secrets("default").Get(context.Background(), "consul-server-cert", metav1.GetOptions{}) - require.NoError(t, err) - serverCert := serverCertSecret.Data[corev1.TLSCertKey] - serverKey := serverCertSecret.Data[corev1.TLSPrivateKeyKey] + exitCode := cmd.Run(flags) + require.Equal(t, 0, exitCode) - certBlock, _ := pem.Decode(serverCert) - certificate, err := x509.ParseCertificate(certBlock.Bytes) - require.NoError(t, err) - require.False(t, certificate.IsCA) - require.Equal(t, []string{"server.dc1.consul", "localhost"}, certificate.DNSNames) - require.Equal(t, []net.IP{net.ParseIP("127.0.0.1").To4()}, certificate.IPAddresses) + caCertBlock, _ := pem.Decode([]byte(c.caCert)) + caCertificate, err := x509.ParseCertificate(caCertBlock.Bytes) + require.NoError(t, err) - keyBlock, _ := pem.Decode(serverKey) - privateKey, err := x509.ParseECPrivateKey(keyBlock.Bytes) - require.NoError(t, err) - require.Equal(t, &privateKey.PublicKey, certificate.PublicKey) + serverCertSecret, err := k8s.CoreV1().Secrets("default").Get(context.Background(), "consul-server-cert", metav1.GetOptions{}) + require.NoError(t, err) + serverCert := serverCertSecret.Data[corev1.TLSCertKey] + serverKey := serverCertSecret.Data[corev1.TLSPrivateKeyKey] - require.NoError(t, certificate.CheckSignatureFrom(caCertificate)) + certBlock, _ := pem.Decode(serverCert) + certificate, err := x509.ParseCertificate(certBlock.Bytes) + require.NoError(t, err) + require.False(t, certificate.IsCA) + require.Equal(t, []string{"server.dc1.consul", "localhost"}, certificate.DNSNames) + require.Equal(t, []net.IP{net.ParseIP("127.0.0.1").To4()}, certificate.IPAddresses) + + keyBlock, _ := pem.Decode(serverKey) + privateKey, err := x509.ParseECPrivateKey(keyBlock.Bytes) + require.NoError(t, err) + require.Equal(t, &privateKey.PublicKey, certificate.PublicKey) + + require.NoError(t, certificate.CheckSignatureFrom(caCertificate)) + + }) + } } func TestRun_UpdatesServerCertificatesWithExistingCertsAsFiles(t *testing.T) { @@ -126,13 +155,13 @@ func TestRun_UpdatesServerCertificatesWithExistingCertsAsFiles(t *testing.T) { ca, err := ioutil.TempFile("", "") require.NoError(t, err) defer os.RemoveAll(ca.Name()) - err = ioutil.WriteFile(ca.Name(), []byte(caCert), 0644) + err = ioutil.WriteFile(ca.Name(), []byte(caCertEC), 0644) require.NoError(t, err) key, err := ioutil.TempFile("", "") require.NoError(t, err) defer os.RemoveAll(key.Name()) - err = ioutil.WriteFile(key.Name(), []byte(caKey), 0644) + err = ioutil.WriteFile(key.Name(), []byte(caKeyEC), 0644) require.NoError(t, err) flags := []string{"-name-prefix", "consul", "-ca", ca.Name(), "-key", key.Name(), "-additional-dnsname", "test.dns.name"} @@ -140,7 +169,7 @@ func TestRun_UpdatesServerCertificatesWithExistingCertsAsFiles(t *testing.T) { exitCode := cmd.Run(flags) require.Equal(t, 0, exitCode) - caCertBlock, _ := pem.Decode([]byte(caCert)) + caCertBlock, _ := pem.Decode([]byte(caCertEC)) caCertificate, err := x509.ParseCertificate(caCertBlock.Bytes) require.NoError(t, err) @@ -176,7 +205,7 @@ func TestRun_CreatesServerCertificatesWithExistingCertsAsSecrets(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSCertKey: []byte(caCert), + corev1.TLSCertKey: []byte(caCertEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -188,7 +217,7 @@ func TestRun_CreatesServerCertificatesWithExistingCertsAsSecrets(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSPrivateKeyKey: []byte(caKey), + corev1.TLSPrivateKeyKey: []byte(caKeyEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -201,7 +230,7 @@ func TestRun_CreatesServerCertificatesWithExistingCertsAsSecrets(t *testing.T) { exitCode := cmd.Run(flags) require.Equal(t, 0, exitCode) - caCertBlock, _ := pem.Decode([]byte(caCert)) + caCertBlock, _ := pem.Decode([]byte(caCertEC)) caCertificate, err := x509.ParseCertificate(caCertBlock.Bytes) require.NoError(t, err) @@ -265,7 +294,7 @@ func TestRun_UpdatesServerCertificatesWithExistingCertsAsSecrets(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSCertKey: []byte(caCert), + corev1.TLSCertKey: []byte(caCertEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -277,7 +306,7 @@ func TestRun_UpdatesServerCertificatesWithExistingCertsAsSecrets(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSPrivateKeyKey: []byte(caKey), + corev1.TLSPrivateKeyKey: []byte(caKeyEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -300,7 +329,7 @@ func TestRun_UpdatesServerCertificatesWithExistingCertsAsSecrets(t *testing.T) { exitCode := cmd.Run(flags) require.Equal(t, 0, exitCode) - caCertBlock, _ := pem.Decode([]byte(caCert)) + caCertBlock, _ := pem.Decode([]byte(caCertEC)) caCertificate, err := x509.ParseCertificate(caCertBlock.Bytes) require.NoError(t, err) @@ -337,7 +366,7 @@ func TestRun_CreatesServerCertificatesWithExpiryWithinSpecifiedDays(t *testing.T Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSCertKey: []byte(caCert), + corev1.TLSCertKey: []byte(caCertEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -349,7 +378,7 @@ func TestRun_CreatesServerCertificatesWithExpiryWithinSpecifiedDays(t *testing.T Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSPrivateKeyKey: []byte(caKey), + corev1.TLSPrivateKeyKey: []byte(caKeyEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -382,7 +411,7 @@ func TestRun_CreatesServerCertificatesWithProvidedHosts(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSCertKey: []byte(caCert), + corev1.TLSCertKey: []byte(caCertEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -394,7 +423,7 @@ func TestRun_CreatesServerCertificatesWithProvidedHosts(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSPrivateKeyKey: []byte(caKey), + corev1.TLSPrivateKeyKey: []byte(caKeyEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -428,7 +457,7 @@ func TestRun_CreatesServerCertificatesWithSpecifiedDomainAndDC(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSCertKey: []byte(caCert), + corev1.TLSCertKey: []byte(caCertEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -440,7 +469,7 @@ func TestRun_CreatesServerCertificatesWithSpecifiedDomainAndDC(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - corev1.TLSPrivateKeyKey: []byte(caKey), + corev1.TLSPrivateKeyKey: []byte(caKeyEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -481,7 +510,7 @@ func TestRun_CreatesServerCertificatesInSpecifiedNamespace(t *testing.T) { Namespace: namespace, }, Data: map[string][]byte{ - corev1.TLSCertKey: []byte(caCert), + corev1.TLSCertKey: []byte(caCertEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -493,7 +522,7 @@ func TestRun_CreatesServerCertificatesInSpecifiedNamespace(t *testing.T) { Namespace: namespace, }, Data: map[string][]byte{ - corev1.TLSPrivateKeyKey: []byte(caKey), + corev1.TLSPrivateKeyKey: []byte(caKeyEC), }, Type: corev1.SecretTypeOpaque, }, metav1.CreateOptions{}) @@ -508,7 +537,7 @@ func TestRun_CreatesServerCertificatesInSpecifiedNamespace(t *testing.T) { } const ( - caCert string = `-----BEGIN CERTIFICATE----- + caCertEC string = `-----BEGIN CERTIFICATE----- MIIDPjCCAuWgAwIBAgIRAOjdIMIYBXgeoXBDydhFImcwCgYIKoZIzj0EAwIwgZEx CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj bzEaMBgGA1UECRMRMTAxIFNlY29uZCBTdHJlZXQxDjAMBgNVBBETBTk0MTA1MRcw @@ -529,7 +558,7 @@ WBJ2jlEV/kttcHlcHpvyO3GHCp3AE+G4f27NWqYdYeACIDJkx6OjZBU7i4K3HSrO qlxZIl+NFZSHr8XS6BFNB8vc -----END CERTIFICATE-----` - caKey string = `-----BEGIN EC PRIVATE KEY----- + caKeyEC string = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEIO+ASjxFB5gYZju94Ujx81ykp54K53b1TvQNQW/zgbFqoAoGCCqGSM49 AwEHoUQDQgAEvETlXGiuMdIH3nOTf/1RGYmBoZA9RaaDp1T9kcABGuzoxEA+P7VO rd4cnIiTnYqkslAdqcXWmoFEubPFTuKghw== @@ -560,4 +589,82 @@ MHcCAQEEINVuQ1fmOWH5HDG7wEqB1KObSs7q26czY7P+WLhtLDZPoAoGCCqGSM49 AwEHoUQDQgAEu6FUB4WbJpJmXe9cXC2vf9xlvLB5aj9lfAzg4uPL+yXHUpHMYyZy tdsZa0jHMVkDIrNoL8nmxu6X578xNY304w== -----END EC PRIVATE KEY-----` + + caCertRSA string = `-----BEGIN CERTIFICATE----- +MIIDGjCCAgICCQC9IJfDAbKSIjANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJD +QTEZMBcGA1UECAwQQnJpdGlzaCBDb2x1bWJpYTERMA8GA1UEBwwIVmFuY292ZXIx +EjAQBgNVBAoMCUhhc2hpQ29ycDAeFw0yMTExMDQyMjQ0MjJaFw0yMTEyMDQyMjQ0 +MjJaME8xCzAJBgNVBAYTAkNBMRkwFwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMREw +DwYDVQQHDAhWYW5jb3ZlcjESMBAGA1UECgwJSGFzaGlDb3JwMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxTVd5sFGuVOZxkPv3tE69khUToVcRb85NRWW +eWBSyrTE4UWr06kG4reSTVrGgR2hojrP4nzVynu7EslCJITb6Df5sn34bKVDpqWJ +gDFJoEYoTzajRxEDjSkwau+iPhuaJ6pB97+JimOg0Jnqe0QVZ2NtjwgpXYSGkevn +iVxZHaurLxnhDry5KyDJ79p48c7aKNxAxU2syrhKkrWNJaCg4WVTOc/eQU4elUZb +TwYIZ/Zi4gOkS+vz0ceggRmXg5MzYT6cBlccHRrA7BaSRkoD7bNbDh+mRTz67UgO +KUjIi+o1TsUhmvO2Know+zIGd1mfAf9qFT+4KXPFh3yN5DeMkQIDAQABMA0GCSqG +SIb3DQEBCwUAA4IBAQC6LAK0NnmnxuWvKZano3hI9DPRlktB4LVfYSBNFnQllxUC +ZYBIouJXFKK4dTccMkgLlQU7hFXj/YWdSRmf78w/w0GbWYAnUDnGfYro7+ZRUtFV +v7FT+xV2hFe+2cp4+btux5kfqD6OC58Gp9FWXMzRhJCWSDAk2rIYJ2MM7og+ad+Q +mJAYoOBLuY1rXc080v0Vdcl3tQ24UvvvLhuyOyL795OaZZl3uVvbaNHpM8lfJNEg +XfsbHpePEKd9ORLV6jUirl0YheqY8Mdx5hfwFHi1FL4eH6vzRm6GF2hUkkfjOUGO +x8JijLHx5rnkFyNOynhoH8QlwYeMPZbc4js7DWuG +-----END CERTIFICATE-----` + + caKeyRSA string = `-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAxTVd5sFGuVOZxkPv3tE69khUToVcRb85NRWWeWBSyrTE4UWr +06kG4reSTVrGgR2hojrP4nzVynu7EslCJITb6Df5sn34bKVDpqWJgDFJoEYoTzaj +RxEDjSkwau+iPhuaJ6pB97+JimOg0Jnqe0QVZ2NtjwgpXYSGkevniVxZHaurLxnh +Dry5KyDJ79p48c7aKNxAxU2syrhKkrWNJaCg4WVTOc/eQU4elUZbTwYIZ/Zi4gOk +S+vz0ceggRmXg5MzYT6cBlccHRrA7BaSRkoD7bNbDh+mRTz67UgOKUjIi+o1TsUh +mvO2Know+zIGd1mfAf9qFT+4KXPFh3yN5DeMkQIDAQABAoH/E0Ii6WX2giKn4bTA +uAG2wFZP5Vsgp68E5yo0h6Xgb+s3Tsh+/yyCf6FtqCA1QmaiYjVcF8IZHqz2l98P +loFi+Ep/F+81U2bQNHX1947Yoc44IYQ0bbw7nI1pLQg5z9biNv1pc8hApkMUcUqW +m3MKpA4RpOYnI/rNKXLgKYnbKgptyniJSyFm+pOLgpSnwZIiSIBqHnppHS1b8Bjq +H2ZYqEYNB7dHLu0HpHB1zGVC4CAzmBqtLw4fNDp1lsHTis0SJpRuBiD5IZU6+1TS +QvgkmfJKStRFIT+0YRap0J+rSYtqwPalPPbV4ePfZTpj4d9Ll0Xx97Pn3oinUQgl +auhBAoGBAOIeH5tEaj3U8DTNGGMWmMBVediJEhqDITyjPVGoKZAt+29tqwlDg+aw +hEEGgaIOPE+mQ3CEbvJnRC4Z/ntYRJpv1arBziRQKfznyb3yyFvy/JBSkNEkyFhz +KHYv/uQyg8XHIAIE41IpKdUk9MZ6BVvHjFdBbU01KerPl8Dfm7aJAoGBAN9FNFyv +A61q44oCwGRTxpYzRshHkk7GsO4Jg/vMKpU8bOCvz8GLD7cx38Xt1bV+uUlNGZc6 +4EZxKrv0P9Fsj//WREc+0K11U3aN6HIYNdVV9Vel2v6Bis8+zTzNY6fSF4sx1Dw9 +5q1BkE6sP3IPHz58Tt0pWNW8lmucZS7Q3KPJAoGBAJuy0GKytlFDOe+xtfQtEBuH +//GpWMzmtFEzujprB8uezf6JTnd/hOipbTf1SfgTw1W5D8D/gAHsN5djEMdQHVUW +YtNExjRc+ryJwnHIJkyiQWUDZXKN2GKHUToojGQHoJLkLVcWlIzziTmaS+4LAXuU +KT+/7op2bBmivkTx9B+5AoGBALyJxhPWPra8onSyqiCOlg3UMxuBRM18/3+jTW7e +E79+DTsXe8smURkT5rFPi739yx1ZHBkWwLj7a2jYcuO4V0lleLbpFnLDtr1QTE+8 +ngkO02U2S13LqpojoFCN6G+Y/ASxCVXtt9Pqn5+v2MvKdUng0v/zoG6tGCC7Kr6D +5S3xAoGAZTYnX/rV1n2YuVvd12T9Xs2EBD9Q3FfL/oDfchU2nWiXyQA8HXhb8aBw +5Mw4BOuo0JgkSTqIxqda10tAViNeqlNiQKvOMt9y8Ugl4eEd4SdutmdTZuVPwK4/ +yLi0ot+KP/8sbKAZjcAiJJIZFsqVY4wRdhSo3jzI72Zsx9CqJvE= +-----END RSA PRIVATE KEY-----` + + // caKeyPKCS8 is caKeyRSA converted to PKCS8 form. + caKeyPKCS8 string = `-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDFNV3mwUa5U5nG +Q+/e0Tr2SFROhVxFvzk1FZZ5YFLKtMThRavTqQbit5JNWsaBHaGiOs/ifNXKe7sS +yUIkhNvoN/myffhspUOmpYmAMUmgRihPNqNHEQONKTBq76I+G5onqkH3v4mKY6DQ +mep7RBVnY22PCCldhIaR6+eJXFkdq6svGeEOvLkrIMnv2njxztoo3EDFTazKuEqS +tY0loKDhZVM5z95BTh6VRltPBghn9mLiA6RL6/PRx6CBGZeDkzNhPpwGVxwdGsDs +FpJGSgPts1sOH6ZFPPrtSA4pSMiL6jVOxSGa87YqejD7MgZ3WZ8B/2oVP7gpc8WH +fI3kN4yRAgMBAAECgf8TQiLpZfaCIqfhtMC4AbbAVk/lWyCnrwTnKjSHpeBv6zdO +yH7/LIJ/oW2oIDVCZqJiNVwXwhkerPaX3w+WgWL4Sn8X7zVTZtA0dfX3jtihzjgh +hDRtvDucjWktCDnP1uI2/WlzyECmQxRxSpabcwqkDhGk5icj+s0pcuApidsqCm3K +eIlLIWb6k4uClKfBkiJIgGoeemkdLVvwGOofZlioRg0Ht0cu7QekcHXMZULgIDOY +Gq0vDh80OnWWwdOKzRImlG4GIPkhlTr7VNJC+CSZ8kpK1EUhP7RhFqnQn6tJi2rA +9qU89tXh499lOmPh30uXRfH3s+feiKdRCCVq6EECgYEA4h4fm0RqPdTwNM0YYxaY +wFV52IkSGoMhPKM9UagpkC37b22rCUOD5rCEQQaBog48T6ZDcIRu8mdELhn+e1hE +mm/VqsHOJFAp/OfJvfLIW/L8kFKQ0STIWHModi/+5DKDxccgAgTjUikp1ST0xnoF +W8eMV0FtTTUp6s+XwN+btokCgYEA30U0XK8DrWrjigLAZFPGljNGyEeSTsaw7gmD ++8wqlTxs4K/PwYsPtzHfxe3VtX65SU0ZlzrgRnEqu/Q/0WyP/9ZERz7QrXVTdo3o +chg11VX1V6Xa/oGKzz7NPM1jp9IXizHUPD3mrUGQTqw/cg8fPnxO3SlY1byWa5xl +LtDco8kCgYEAm7LQYrK2UUM577G19C0QG4f/8alYzOa0UTO6OmsHy57N/olOd3+E +6KltN/VJ+BPDVbkPwP+AAew3l2MQx1AdVRZi00TGNFz6vInCccgmTKJBZQNlco3Y +YodROiiMZAegkuQtVxaUjPOJOZpL7gsBe5QpP7/uinZsGaK+RPH0H7kCgYEAvInG +E9Y+tryidLKqII6WDdQzG4FEzXz/f6NNbt4Tv34NOxd7yyZRGRPmsU+Lvf3LHVkc +GRbAuPtraNhy47hXSWV4tukWcsO2vVBMT7yeCQ7TZTZLXcuqmiOgUI3ob5j8BLEJ +Ve230+qfn6/Yy8p1SeDS//Ogbq0YILsqvoPlLfECgYBlNidf+tXWfZi5W93XZP1e +zYQEP1DcV8v+gN9yFTadaJfJADwdeFvxoHDkzDgE66jQmCRJOojGp1rXS0BWI16q +U2JAq84y33LxSCXh4R3hJ262Z1Nm5U/Arj/IuLSi34o//yxsoBmNwCIkkhkWypVj +jBF2FKjePMjvZmzH0Kom8Q== +-----END PRIVATE KEY-----` ) diff --git a/control-plane/subcommand/version/command.go b/control-plane/subcommand/version/command.go index 3b53129762..58768a1f92 100644 --- a/control-plane/subcommand/version/command.go +++ b/control-plane/subcommand/version/command.go @@ -12,7 +12,7 @@ type Command struct { } func (c *Command) Run(_ []string) int { - c.UI.Output(fmt.Sprintf("consul-k8s %s", c.Version)) + c.UI.Output(fmt.Sprintf("consul-k8s-control-plane %s", c.Version)) return 0 } diff --git a/control-plane/subcommand/webhook-cert-manager/command.go b/control-plane/subcommand/webhook-cert-manager/command.go index 4db313a323..570a432ad9 100644 --- a/control-plane/subcommand/webhook-cert-manager/command.go +++ b/control-plane/subcommand/webhook-cert-manager/command.go @@ -157,8 +157,6 @@ func (c *Command) Run(args []string) int { } } - certCh := make(chan cert.MetaBundle) - // Create the certificate notifier so we can update certificates, // then start all the background routines for updating certificates. var notifiers []*cert.Notify @@ -179,13 +177,14 @@ func (c *Command) Run(args []string) int { Expiry: expiry, } } + + certCh := make(chan cert.MetaBundle) certNotify := &cert.Notify{Source: certSource, Ch: certCh, WebhookConfigName: config.Name, SecretName: config.SecretName, SecretNamespace: config.SecretNamespace} notifiers = append(notifiers, certNotify) go certNotify.Start(ctx) + go c.certWatcher(ctx, certCh, c.clientset, c.logger) } - go c.certWatcher(ctx, certCh, c.clientset, c.logger) - // We define a signal handler for OS interrupts, and when an SIGINT or SIGTERM is received, // we gracefully shut down, by first stopping our cert notifiers and then cancelling // all the contexts that have been created by the process. @@ -250,6 +249,7 @@ func (c *Command) reconcileCertificates(ctx context.Context, clientset kubernete UID: deployment.UID, }, }, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, Data: map[string][]byte{ corev1.TLSCertKey: bundle.Cert, @@ -280,6 +280,12 @@ func (c *Command) reconcileCertificates(ctx context.Context, clientset kubernete return nil } + if certSecret.ObjectMeta.Labels == nil { + certSecret.ObjectMeta.Labels = map[string]string{common.CLILabelKey: common.CLILabelValue} + } else { + certSecret.ObjectMeta.Labels[common.CLILabelKey] = common.CLILabelValue + } + certSecret.Data[corev1.TLSCertKey] = bundle.Cert certSecret.Data[corev1.TLSPrivateKeyKey] = bundle.Key // Update the Owner Reference on an existing secret in case the secret @@ -406,7 +412,7 @@ func (c *Command) Synopsis() string { } // interrupt sends os.Interrupt signal to the command -// so it can exit gracefully. This function is needed for tests +// so it can exit gracefully. This function is needed for tests. func (c *Command) interrupt() { c.sendSignal(syscall.SIGINT) } diff --git a/control-plane/subcommand/webhook-cert-manager/command_test.go b/control-plane/subcommand/webhook-cert-manager/command_test.go index 3fde5ffa8f..434db47958 100644 --- a/control-plane/subcommand/webhook-cert-manager/command_test.go +++ b/control-plane/subcommand/webhook-cert-manager/command_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" "github.com/hashicorp/consul-k8s/control-plane/subcommand/webhook-cert-manager/mocks" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/mitchellh/cli" @@ -276,7 +277,8 @@ func TestRun_SecretExists(t *testing.T) { secretOne := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretOneName, + Name: secretOneName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, StringData: map[string]string{ v1.TLSCertKey: "cert-1", @@ -286,7 +288,8 @@ func TestRun_SecretExists(t *testing.T) { } secretTwo := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretTwoName, + Name: secretTwoName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, StringData: map[string]string{ v1.TLSCertKey: "cert-2", @@ -401,7 +404,8 @@ func TestRun_SecretUpdates(t *testing.T) { secret1 := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretOne, + Name: secretOne, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, }, StringData: map[string]string{ v1.TLSCertKey: "cert-1", @@ -485,6 +489,105 @@ func TestRun_SecretUpdates(t *testing.T) { }) } +// Test that when the MutatingWebhookConfiguration is modified, that we correctly +// reset it to the expected CA bundle. +func TestRun_WebhookConfigModified(t *testing.T) { + t.Parallel() + + deploymentName := "deployment" + deploymentNamespace := "deploy-ns" + webhook1ConfigName := "webhookOne" + webhook2ConfigName := "webhookTwo" + caBundle1 := []byte("bootstrapped-CA1") + caBundle2 := []byte("bootstrapped-CA2") + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Namespace: deploymentNamespace, + UID: types.UID("this-is-a-uid"), + }, + } + + initialWebhook1Config := &admissionv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: webhook1ConfigName, + }, + Webhooks: []admissionv1.MutatingWebhook{ + { + Name: "webhook1-under-test", + ClientConfig: admissionv1.WebhookClientConfig{ + CABundle: caBundle1, + }, + }, + }, + } + initialWebhook2Config := &admissionv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: webhook2ConfigName, + }, + Webhooks: []admissionv1.MutatingWebhook{ + { + Name: "webhook2-under-test", + ClientConfig: admissionv1.WebhookClientConfig{ + CABundle: caBundle2, + }, + }, + }, + } + + // The k8s cluster will start with the two webhook configs and the deployment. + k8s := fake.NewSimpleClientset(initialWebhook1Config, initialWebhook2Config, deployment) + ctx := context.Background() + + // We don't want the certs to expire. This test is only checking if + // the MutatingWebhookConfiguration is modified that it gets reset. + certExpiry := 1 * time.Hour + + // Start the command. + cmd := Command{ + UI: cli.NewMockUi(), + clientset: k8s, + certExpiry: &certExpiry, + } + + configFile := common.WriteTempFile(t, configFile) + exitCh := runCommandAsynchronously(&cmd, []string{ + "-config-file", configFile, + "-deployment-name", deploymentName, + "-deployment-namespace", deploymentNamespace, + }) + defer stopCommand(t, &cmd, exitCh) + + // First, check that the mutatingwebhookconfiguration contents are updated when the cert-manager starts. + timer := &retry.Timer{Timeout: 10 * time.Second, Wait: 500 * time.Millisecond} + retry.RunWith(timer, t, func(r *retry.R) { + webhookConfig1, err := k8s.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(ctx, webhook1ConfigName, metav1.GetOptions{}) + require.NoError(r, err) + require.NotEqual(r, webhookConfig1.Webhooks[0].ClientConfig.CABundle, caBundle1) + + webhookConfig2, err := k8s.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(ctx, webhook2ConfigName, metav1.GetOptions{}) + require.NoError(r, err) + require.NotEqual(r, webhookConfig2.Webhooks[0].ClientConfig.CABundle, caBundle2) + }) + + // Now, edit the mutatingwebhookconfigurations and reset the caBundle fields. + k8s.AdmissionregistrationV1().MutatingWebhookConfigurations().Update(ctx, initialWebhook1Config, metav1.UpdateOptions{}) + k8s.AdmissionregistrationV1().MutatingWebhookConfigurations().Update(ctx, initialWebhook2Config, metav1.UpdateOptions{}) + + // Check that both mutatingwebhookconfigurations have their caBundle fields reset. + timer = &retry.Timer{Timeout: 10 * time.Second, Wait: 500 * time.Millisecond} + retry.RunWith(timer, t, func(r *retry.R) { + webhookConfig1, err := k8s.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(ctx, webhook1ConfigName, metav1.GetOptions{}) + require.NoError(r, err) + require.NotEqual(r, webhookConfig1.Webhooks[0].ClientConfig.CABundle, caBundle1) + + webhookConfig2, err := k8s.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(ctx, webhook2ConfigName, metav1.GetOptions{}) + require.NoError(r, err) + require.NotEqual(r, webhookConfig2.Webhooks[0].ClientConfig.CABundle, caBundle2) + }) +} + // This test verifies that when there is an error while attempting to update // the certs or the webhook config, it retries the update every second until // it succeeds. diff --git a/control-plane/version/version.go b/control-plane/version/version.go index 6107828dca..b3a31b883f 100644 --- a/control-plane/version/version.go +++ b/control-plane/version/version.go @@ -14,7 +14,7 @@ var ( // // Version must conform to the format expected by // github.com/hashicorp/go-version for tests to work. - Version = "0.33.0" + Version = "0.41.1" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release diff --git a/docs/admin-partitions-with-acls.md b/docs/admin-partitions-with-acls.md new file mode 100644 index 0000000000..fb282fa38d --- /dev/null +++ b/docs/admin-partitions-with-acls.md @@ -0,0 +1,98 @@ +## Installing Admin Partitions with ACLs enabled + +To enable ACLs on the server cluster use the following config: +```yaml +global: + enableConsulNamespaces: true + tls: + enabled: true + image: hashicorp/consul-enterprise:1.11.1 + adminPartitions: + enabled: true + acls: + manageSystemACLs: true +server: + exposeGossipAndRPCPorts: true + enterpriseLicense: + secretName: license + secretKey: key + replicas: 1 +connectInject: + enabled: true + transparentProxy: + defaultEnabled: false + consulNamespaces: + mirroringK8S: true +controller: + enabled: true +meshGateway: + enabled: true +``` + +Identify the LoadBalancer External IP of the `partition-service` +```bash +kubectl get svc consul-consul-partition-service -o json | jq -r '.status.loadBalancer.ingress[0].ip' +``` + +Migrate the TLS CA credentials from the server cluster to the workload clusters +```bash +kubectl get secret consul-consul-ca-key --context "server-context" -o json | kubectl apply --context "workload-context" -f - +kubectl get secret consul-consul-ca-cert --context "server-context" -o json | kubectl apply --context "workload-context" -f - +``` + +Migrate the Partition token from the server cluster to the workload clusters +```bash +kubectl get secret consul-consul-partitions-acl-token --context "server-context" -o json | kubectl apply --context "workload-context" -f - +``` + +Identify the Kubernetes AuthMethod URL of the workload cluster to use as the `k8sAuthMethodHost`: +```bash +kubectl config view -o "jsonpath={.clusters[?(@.name=='workload-cluster-name')].cluster.server}" +``` + +Configure the workload cluster using the following: + +```yaml +global: + enabled: false + enableConsulNamespaces: true + image: hashicorp/consul-enterprise:1.11.1 + adminPartitions: + enabled: true + name: "partition-name" + tls: + enabled: true + caCert: + secretName: consul-consul-ca-cert + secretKey: tls.crt + caKey: + secretName: consul-consul-ca-key + secretKey: tls.key + acls: + manageSystemACLs: true + bootstrapToken: + secretName: consul-consul-partitions-acl-token + secretKey: token +server: + enterpriseLicense: + secretName: license + secretKey: key +externalServers: + enabled: true + hosts: [ "loadbalancer IP" ] + tlsServerName: server.dc1.consul + k8sAuthMethodHost: "authmethod-host IP" +client: + enabled: true + exposeGossipPorts: true + join: [ "loadbalancer IP" ] +connectInject: + enabled: true + consulNamespaces: + mirroringK8S: true +controller: + enabled: true +meshGateway: + enabled: true +``` +This should create clusters that have Admin Partitions deployed on them with ACLs enabled. diff --git a/charts/consul/hack/aws-acceptance-test-cleanup/go.mod b/hack/aws-acceptance-test-cleanup/go.mod similarity index 100% rename from charts/consul/hack/aws-acceptance-test-cleanup/go.mod rename to hack/aws-acceptance-test-cleanup/go.mod diff --git a/charts/consul/hack/aws-acceptance-test-cleanup/go.sum b/hack/aws-acceptance-test-cleanup/go.sum similarity index 100% rename from charts/consul/hack/aws-acceptance-test-cleanup/go.sum rename to hack/aws-acceptance-test-cleanup/go.sum diff --git a/charts/consul/hack/aws-acceptance-test-cleanup/main.go b/hack/aws-acceptance-test-cleanup/main.go similarity index 64% rename from charts/consul/hack/aws-acceptance-test-cleanup/main.go rename to hack/aws-acceptance-test-cleanup/main.go index 49b6c21301..45198b8ea5 100644 --- a/charts/consul/hack/aws-acceptance-test-cleanup/main.go +++ b/hack/aws-acceptance-test-cleanup/main.go @@ -219,12 +219,25 @@ func realMain(ctx context.Context) error { } if err := destroyBackoff(ctx, "NAT gateway", *gateway.NatGatewayId, func() error { + // We only care about Nat gateways whose state is not "deleted." + // Deleted Nat gateways will show in the output for about 1hr + // (https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html#nat-gateway-deleting), + // but we can proceed with deleting other resources once its state is deleted. currNatGateways, err := ec2Client.DescribeNatGatewaysWithContext(ctx, &ec2.DescribeNatGatewaysInput{ Filter: []*ec2.Filter{ { Name: aws.String("vpc-id"), Values: []*string{vpcID}, }, + { + Name: aws.String("state"), + Values: []*string{ + aws.String(ec2.NatGatewayStatePending), + aws.String(ec2.NatGatewayStateFailed), + aws.String(ec2.NatGatewayStateDeleting), + aws.String(ec2.NatGatewayStateAvailable), + }, + }, }, }) if err != nil { @@ -238,6 +251,18 @@ func realMain(ctx context.Context) error { return err } fmt.Printf("NAT gateway: Destroyed [id=%s]\n", *gateway.NatGatewayId) + + // Release Elastic IP associated with the NAT gateway (if any). + for _, address := range gateway.NatGatewayAddresses { + if address.AllocationId != nil { + fmt.Printf("NAT gateway: Releasing Elastic IP... [id=%s]\n", *address.AllocationId) + _, err := ec2Client.ReleaseAddressWithContext(ctx, &ec2.ReleaseAddressInput{AllocationId: address.AllocationId}) + if err != nil { + return err + } + fmt.Printf("NAT gateway: Elastic IP released [id=%s]\n", *address.AllocationId) + } + } } // Delete ELBs (usually left from mesh gateway tests). @@ -276,6 +301,124 @@ func realMain(ctx context.Context) error { fmt.Printf("ELB: Destroyed [id=%s]\n", *elbDescrip.LoadBalancerName) } + // Delete internet gateways. + igws, err := ec2Client.DescribeInternetGatewaysWithContext(ctx, &ec2.DescribeInternetGatewaysInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("attachment.vpc-id"), + Values: []*string{vpcID}, + }, + }, + }) + for _, igw := range igws.InternetGateways { + fmt.Printf("Internet gateway: Detaching from VPC... [id=%s]\n", *igw.InternetGatewayId) + _, err := ec2Client.DetachInternetGatewayWithContext(ctx, &ec2.DetachInternetGatewayInput{ + InternetGatewayId: igw.InternetGatewayId, + VpcId: vpcID, + }) + if err != nil { + return err + } + fmt.Printf("Internet gateway: Detached [id=%s]\n", *igw.InternetGatewayId) + + fmt.Printf("Internet gateway: Destroying... [id=%s]\n", *igw.InternetGatewayId) + _, err = ec2Client.DeleteInternetGatewayWithContext(ctx, &ec2.DeleteInternetGatewayInput{ + InternetGatewayId: igw.InternetGatewayId, + }) + if err != nil { + return err + } + fmt.Printf("Internet gateway: Destroyed [id=%s]\n", *igw.InternetGatewayId) + } + + // Delete subnets. + subnets, err := ec2Client.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpcID}, + }, + }, + }) + for _, subnet := range subnets.Subnets { + fmt.Printf("Subnet: Destroying... [id=%s]\n", *subnet.SubnetId) + _, err := ec2Client.DeleteSubnetWithContext(ctx, &ec2.DeleteSubnetInput{ + SubnetId: subnet.SubnetId, + }) + if err != nil { + return err + } + fmt.Printf("Subnet: Destroyed [id=%s]\n", *subnet.SubnetId) + } + + // Delete route tables. + routeTables, err := ec2Client.DescribeRouteTablesWithContext(ctx, &ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpcID}, + }, + }, + }) + for _, routeTable := range routeTables.RouteTables { + // Find out if this is the main route table. + var mainRouteTable bool + for _, association := range routeTable.Associations { + if association.Main != nil && *association.Main { + mainRouteTable = true + break + } + } + + if mainRouteTable { + fmt.Printf("Route table: Skipping the main route table [id=%s]\n", *routeTable.RouteTableId) + } else { + fmt.Printf("Route table: Destroying... [id=%s]\n", *routeTable.RouteTableId) + _, err := ec2Client.DeleteRouteTableWithContext(ctx, &ec2.DeleteRouteTableInput{ + RouteTableId: routeTable.RouteTableId, + }) + if err != nil { + return err + } + fmt.Printf("Route table: Destroyed [id=%s]\n", *routeTable.RouteTableId) + } + } + + // Delete security groups. + sgs, err := ec2Client.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpcID}, + }, + }, + }) + for _, sg := range sgs.SecurityGroups { + revokeSGInput := &ec2.RevokeSecurityGroupIngressInput{GroupId: sg.GroupId} + revokeSGInput.SetIpPermissions(sg.IpPermissions) + fmt.Printf("Security group: Removing security group rules... [id=%s]\n", *sg.GroupId) + _, err := ec2Client.RevokeSecurityGroupIngressWithContext(ctx, revokeSGInput) + if err != nil { + return err + } + fmt.Printf("Security group: Removed security group rules [id=%s]\n", *sg.GroupId) + } + + for _, sg := range sgs.SecurityGroups { + if sg.GroupName != nil && *sg.GroupName == "default" { + fmt.Printf("Security group: Skipping default security group [id=%s]\n", *sg.GroupId) + continue + } + fmt.Printf("Security group: Destroying... [id=%s]\n", *sg.GroupId) + _, err = ec2Client.DeleteSecurityGroupWithContext(ctx, &ec2.DeleteSecurityGroupInput{ + GroupId: sg.GroupId, + }) + if err != nil { + return err + } + fmt.Printf("Security group: Destroyed [id=%s]\n", *sg.GroupId) + } + // Delete VPC. Sometimes there's a race condition where AWS thinks // the VPC still has dependencies but they've already been deleted so // we may need to retry a couple times. @@ -290,28 +433,12 @@ func realMain(ctx context.Context) error { break } fmt.Printf("VPC: Destroy error... [id=%s,err=%q,retry=%d]\n", *vpcID, err, retryCount) - time.Sleep(1 * time.Second) + time.Sleep(5 * time.Second) } if retryCount == 10 { return errors.New("reached max retry count deleting VPC") } - // Now that the destroy request went through we still need to wait for - // the deletion to complete. - if err := destroyBackoff(ctx, "VPC", *vpcID, func() error { - currVPCs, err := ec2Client.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{ - VpcIds: []*string{vpcID}, - }) - if err != nil { - return err - } - if len(currVPCs.Vpcs) > 0 { - return errNotDestroyed - } - return nil - }); err != nil { - return err - } fmt.Printf("VPC: Destroyed [id=%s]\n", *vpcID) } diff --git a/hack/copy-crds-to-chart/go.mod b/hack/copy-crds-to-chart/go.mod new file mode 100644 index 0000000000..f88fccc7a9 --- /dev/null +++ b/hack/copy-crds-to-chart/go.mod @@ -0,0 +1,3 @@ +module github.com/hashicorp/consul-k8s/hack/copy-crds-to-chart + +go 1.16 diff --git a/control-plane/hack/crds-to-consul-helm/main.go b/hack/copy-crds-to-chart/main.go similarity index 64% rename from control-plane/hack/crds-to-consul-helm/main.go rename to hack/copy-crds-to-chart/main.go index a9ec363d6a..efdf2dbf9f 100644 --- a/control-plane/hack/crds-to-consul-helm/main.go +++ b/hack/copy-crds-to-chart/main.go @@ -1,5 +1,5 @@ -// Script to move generated CRD yaml into consul-helm and modify it to match -// the expected consul-helm format. +// Script to copy generated CRD yaml into chart directory and modify it to match +// the expected chart format (e.g. formatted YAML). package main import ( @@ -11,33 +11,20 @@ import ( ) func main() { - if len(os.Args) < 2 { - fmt.Println("Usage: go run ./... ") + if len(os.Args) != 1 { + fmt.Println("Usage: go run ./...") os.Exit(1) } - helmRepoPath := os.Args[1] - if !filepath.IsAbs(helmRepoPath) { - var err error - // NOTE: Must add ../.. to a relative path because this program is in - // hack/crds-to-consul-helm. - helmRepoPath, err = filepath.Abs(filepath.Join("../..", helmRepoPath)) - if err != nil { - fmt.Printf("Error: %s\n", err) - os.Exit(1) - } - } - fmt.Printf("Using consul-helm repo path: %s\n", helmRepoPath) - - if err := realMain(helmRepoPath); err != nil { + if err := realMain("../../charts/consul"); err != nil { fmt.Printf("Error: %s\n", err) os.Exit(1) } os.Exit(0) } -func realMain(helmPathAbs string) error { - return filepath.Walk("../../config/crd/bases", func(path string, info os.FileInfo, err error) error { +func realMain(helmPath string) error { + return filepath.Walk("../../control-plane/config/crd/bases", func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -77,7 +64,7 @@ func realMain(helmPathAbs string) error { // Construct the destination filename. filenameSplit := strings.Split(info.Name(), "_") crdName := filenameSplit[1] - destinationPath := filepath.Join(helmPathAbs, "templates", fmt.Sprintf("crd-%s", crdName)) + destinationPath := filepath.Join(helmPath, "templates", fmt.Sprintf("crd-%s", crdName)) // Write it. printf("writing to %s", destinationPath) diff --git a/charts/consul/hack/helm-reference-gen/doc_node.go b/hack/helm-reference-gen/doc_node.go similarity index 100% rename from charts/consul/hack/helm-reference-gen/doc_node.go rename to hack/helm-reference-gen/doc_node.go diff --git a/charts/consul/hack/helm-reference-gen/fixtures/full-values.golden b/hack/helm-reference-gen/fixtures/full-values.golden similarity index 99% rename from charts/consul/hack/helm-reference-gen/fixtures/full-values.golden rename to hack/helm-reference-gen/fixtures/full-values.golden index 08f25c945f..e29c4d4f5e 100644 --- a/charts/consul/hack/helm-reference-gen/fixtures/full-values.golden +++ b/hack/helm-reference-gen/fixtures/full-values.golden @@ -1,3 +1,23 @@ +## Top-Level Stanzas + +Use these links to navigate to a particular top-level stanza. + +- [`global`](#global) +- [`server`](#server) +- [`externalServers`](#externalservers) +- [`client`](#client) +- [`dns`](#dns) +- [`ui`](#ui) +- [`syncCatalog`](#synccatalog) +- [`connectInject`](#connectinject) +- [`controller`](#controller) +- [`meshGateway`](#meshgateway) +- [`ingressGateways`](#ingressgateways) +- [`terminatingGateways`](#terminatinggateways) +- [`tests`](#tests) + +## All Values + ### global - `global` ((#v-global)) - Holds values that affect multiple components of the chart. @@ -1541,4 +1561,4 @@ When using helm install, the test Pod is not submitted to the cluster so this is only useful when running helm template. - - `enabled` ((#v-tests-enabled)) (`boolean: true`) \ No newline at end of file + - `enabled` ((#v-tests-enabled)) (`boolean: true`) diff --git a/charts/consul/hack/helm-reference-gen/fixtures/full-values.yaml b/hack/helm-reference-gen/fixtures/full-values.yaml similarity index 100% rename from charts/consul/hack/helm-reference-gen/fixtures/full-values.yaml rename to hack/helm-reference-gen/fixtures/full-values.yaml diff --git a/charts/consul/hack/helm-reference-gen/go.mod b/hack/helm-reference-gen/go.mod similarity index 63% rename from charts/consul/hack/helm-reference-gen/go.mod rename to hack/helm-reference-gen/go.mod index a63889328b..138cc52e5a 100644 --- a/charts/consul/hack/helm-reference-gen/go.mod +++ b/hack/helm-reference-gen/go.mod @@ -1,4 +1,4 @@ -module github.com/hashicorp/consul-helm/hack/helm-reference-gen +module github.com/hashicorp/consul-k8s/hack/helm-reference-gen go 1.15 diff --git a/charts/consul/hack/helm-reference-gen/go.sum b/hack/helm-reference-gen/go.sum similarity index 100% rename from charts/consul/hack/helm-reference-gen/go.sum rename to hack/helm-reference-gen/go.sum diff --git a/charts/consul/hack/helm-reference-gen/main.go b/hack/helm-reference-gen/main.go similarity index 93% rename from charts/consul/hack/helm-reference-gen/main.go rename to hack/helm-reference-gen/main.go index ca9c8f8dd2..9560a281c8 100644 --- a/charts/consul/hack/helm-reference-gen/main.go +++ b/hack/helm-reference-gen/main.go @@ -3,7 +3,7 @@ package main // This script generates markdown documentation out of the values.yaml file // for use on consul.io. // -// Usage: make gen-docs [consul-repo-path] [-validate] +// Usage: make gen-helm-docs [consul-repo-path] [-validate] // Where [consul-repo-path] is the location of the hashicorp/consul repo. Defaults to ../../../consul. // If -validate is set, the generated docs won't be output anywhere. // This is useful in CI to ensure the generation will succeed. @@ -22,6 +22,11 @@ import ( "gopkg.in/yaml.v3" ) +const ( + tocPrefix = "## Top-Level Stanzas\n\nUse these links to navigate to a particular top-level stanza.\n\n" + tocSuffix = "\n## All Values" +) + var ( // typeAnnotation matches the @type annotation. It captures the value of @type. typeAnnotation = regexp.MustCompile(`(?m).*@type: (.*)$`) @@ -90,7 +95,7 @@ func main() { } // Parse the values.yaml file. - inputBytes, err := ioutil.ReadFile("../../values.yaml") + inputBytes, err := ioutil.ReadFile("../../charts/consul/values.yaml") if err != nil { fmt.Println(err.Error()) os.Exit(1) @@ -147,7 +152,15 @@ func GenerateDocs(yamlStr string) (string, error) { } children, err := generateDocsFromNode(docNodeTmpl, node) - return strings.ReplaceAll(strings.Join(children, "\n\n"), "[Enterprise Only]", ""), err + if err != nil { + return "", err + } + + enterpriseSubst := strings.ReplaceAll(strings.Join(children, "\n\n"), "[Enterprise Only]", "") + + // Add table of contents. + toc := generateTOC(node) + return toc + "\n\n" + enterpriseSubst + "\n", nil } // Parse parses yamlStr into a tree of DocNode's. @@ -389,3 +402,13 @@ func buildDocNode(nodeContentIdx int, currNode *yaml.Node, nodeContent []*yaml.N } return DocNode{}, fmt.Errorf("fell through cases unexpectedly at breadcrumb: %s", parentBreadcrumb) } + +func generateTOC(node DocNode) string { + toc := tocPrefix + + for _, c := range node.Children { + toc += fmt.Sprintf("- [`%s`](#%s)\n", c.Key, strings.ToLower(c.Key)) + } + + return toc + tocSuffix +} diff --git a/charts/consul/hack/helm-reference-gen/main_test.go b/hack/helm-reference-gen/main_test.go similarity index 75% rename from charts/consul/hack/helm-reference-gen/main_test.go rename to hack/helm-reference-gen/main_test.go index affb4cfe7d..ed8b00fb90 100644 --- a/charts/consul/hack/helm-reference-gen/main_test.go +++ b/hack/helm-reference-gen/main_test.go @@ -20,27 +20,42 @@ func Test(t *testing.T) { # Line 1 # Line 2 key: value`, - Exp: `### key + Exp: `- [$key$](#key) -- $key$ ((#v-key)) ($string: value$) - Line 1\n Line 2`, +## All Values + +### key + +- $key$ ((#v-key)) ($string: value$) - Line 1\n Line 2 +`, }, "integer value": { Input: `--- # Line 1 # Line 2 replicas: 3`, - Exp: `### replicas + Exp: `- [$replicas$](#replicas) + +## All Values + +### replicas -- $replicas$ ((#v-replicas)) ($integer: 3$) - Line 1\n Line 2`, +- $replicas$ ((#v-replicas)) ($integer: 3$) - Line 1\n Line 2 +`, }, "boolean value": { Input: `--- # Line 1 # Line 2 enabled: true`, - Exp: `### enabled + Exp: `- [$enabled$](#enabled) + +## All Values -- $enabled$ ((#v-enabled)) ($boolean: true$) - Line 1\n Line 2`, +### enabled + +- $enabled$ ((#v-enabled)) ($boolean: true$) - Line 1\n Line 2 +`, }, "map": { Input: `--- @@ -50,11 +65,16 @@ map: # Key line 1 # Key line 2 key: value`, - Exp: `### map + Exp: `- [$map$](#map) + +## All Values + +### map - $map$ ((#v-map)) - Map line 1\n Map line 2 - - $key$ ((#v-map-key)) ($string: value$) - Key line 1\n Key line 2`, + - $key$ ((#v-map-key)) ($string: value$) - Key line 1\n Key line 2 +`, }, "map with multiple keys": { Input: `--- @@ -68,7 +88,11 @@ map: int: 1 # Bool docs bool: true`, - Exp: `### map + Exp: `- [$map$](#map) + +## All Values + +### map - $map$ ((#v-map)) - Map line 1\n Map line 2 @@ -77,16 +101,22 @@ map: - $int$ ((#v-map-int)) ($integer: 1$) - Int docs - - $bool$ ((#v-map-bool)) ($boolean: true$) - Bool docs`, + - $bool$ ((#v-map-bool)) ($boolean: true$) - Bool docs +`, }, "null value": { Input: `--- # key docs # @type: string key: null`, - Exp: `### key + Exp: `- [$key$](#key) + +## All Values -- $key$ ((#v-key)) ($string: null$) - key docs`, +### key + +- $key$ ((#v-key)) ($string: null$) - key docs +`, }, "description with empty line": { Input: `--- @@ -94,9 +124,14 @@ key: null`, # # line 2 key: value`, - Exp: `### key + Exp: `- [$key$](#key) -- $key$ ((#v-key)) ($string: value$) - line 1\n\n line 2`, +## All Values + +### key + +- $key$ ((#v-key)) ($string: value$) - line 1\n\n line 2 +`, }, "array of strings": { Input: `--- @@ -104,9 +139,14 @@ key: value`, # @type: array serverAdditionalDNSSANs: [] `, - Exp: `### serverAdditionalDNSSANs + Exp: `- [$serverAdditionalDNSSANs$](#serveradditionaldnssans) + +## All Values + +### serverAdditionalDNSSANs -- $serverAdditionalDNSSANs$ ((#v-serveradditionaldnssans)) ($array: []$) - line 1`, +- $serverAdditionalDNSSANs$ ((#v-serveradditionaldnssans)) ($array: []$) - line 1 +`, }, "map with empty string values": { Input: `--- @@ -117,13 +157,18 @@ gossipEncryption: # secretKey secretKey: "" `, - Exp: `### gossipEncryption + Exp: `- [$gossipEncryption$](#gossipencryption) + +## All Values + +### gossipEncryption - $gossipEncryption$ ((#v-gossipencryption)) - gossipEncryption - $secretName$ ((#v-gossipencryption-secretname)) ($string: ""$) - secretName - - $secretKey$ ((#v-gossipencryption-secretkey)) ($string: ""$) - secretKey`, + - $secretKey$ ((#v-gossipencryption-secretkey)) ($string: ""$) - secretKey +`, }, "map with null string values": { Input: `--- @@ -133,13 +178,18 @@ bootstrapToken: # @type: string secretKey: null `, - Exp: `### bootstrapToken + Exp: `- [$bootstrapToken$](#bootstraptoken) + +## All Values + +### bootstrapToken - $bootstrapToken$ ((#v-bootstraptoken)) - $secretName$ ((#v-bootstraptoken-secretname)) ($string: null$) - - $secretKey$ ((#v-bootstraptoken-secretkey)) ($string: null$)`, + - $secretKey$ ((#v-bootstraptoken-secretkey)) ($string: null$) +`, }, "resource settings": { Input: `--- @@ -167,7 +217,11 @@ lifecycleSidecarContainer: memory: "50Mi" cpu: "20m" `, - Exp: `### lifecycleSidecarContainer + Exp: `- [$lifecycleSidecarContainer$](#lifecyclesidecarcontainer) + +## All Values + +### lifecycleSidecarContainer - $lifecycleSidecarContainer$ ((#v-lifecyclesidecarcontainer)) - lifecycle @@ -196,7 +250,8 @@ lifecycleSidecarContainer: - $memory$ ((#v-lifecyclesidecarcontainer-resources-limits-memory)) ($string: 50Mi$) - - $cpu$ ((#v-lifecyclesidecarcontainer-resources-limits-cpu)) ($string: 20m$)`, + - $cpu$ ((#v-lifecyclesidecarcontainer-resources-limits-cpu)) ($string: 20m$) +`, }, "default as dash": { Input: `--- @@ -208,22 +263,32 @@ server: # @type: boolean enabled: "-" `, - Exp: `### server + Exp: `- [$server$](#server) + +## All Values + +### server - $server$ ((#v-server)) - $enabled$ ((#v-server-enabled)) ($boolean: global.enabled$) - If true, the chart will install all the resources necessary for a Consul server cluster. If you're running Consul externally and want agents - within Kubernetes to join that cluster, this should probably be false.`, + within Kubernetes to join that cluster, this should probably be false. +`, }, "extraConfig {}": { Input: `--- extraConfig: | {} `, - Exp: `### extraConfig + Exp: `- [$extraConfig$](#extraconfig) + +## All Values + +### extraConfig -- $extraConfig$ ((#v-extraconfig)) ($string: {}$)`, +- $extraConfig$ ((#v-extraconfig)) ($string: {}$) +`, }, "affinity": { Input: `--- @@ -238,36 +303,56 @@ affinity: | component: server topologyKey: kubernetes.io/hostname `, - Exp: `### affinity + Exp: `- [$affinity$](#affinity) + +## All Values -- $affinity$ ((#v-affinity)) ($string$) - Affinity Settings`, +### affinity + +- $affinity$ ((#v-affinity)) ($string$) - Affinity Settings +`, }, "k8sAllowNamespaces": { Input: `--- # @type: array k8sAllowNamespaces: ["*"]`, - Exp: `### k8sAllowNamespaces + Exp: `- [$k8sAllowNamespaces$](#k8sallownamespaces) + +## All Values + +### k8sAllowNamespaces -- $k8sAllowNamespaces$ ((#v-k8sallownamespaces)) ($array: ["*"]$)`, +- $k8sAllowNamespaces$ ((#v-k8sallownamespaces)) ($array: ["*"]$) +`, }, "k8sDenyNamespaces": { Input: `--- # @type: array k8sDenyNamespaces: ["kube-system", "kube-public"]`, - Exp: `### k8sDenyNamespaces + Exp: `- [$k8sDenyNamespaces$](#k8sdenynamespaces) + +## All Values + +### k8sDenyNamespaces -- $k8sDenyNamespaces$ ((#v-k8sdenynamespaces)) ($array: ["kube-system", "kube-public"]$)`, +- $k8sDenyNamespaces$ ((#v-k8sdenynamespaces)) ($array: ["kube-system", "kube-public"]$) +`, }, "gateways": { Input: `--- # @type: array gateways: - name: ingress-gateway`, - Exp: `### gateways + Exp: `- [$gateways$](#gateways) + +## All Values + +### gateways - $gateways$ ((#v-gateways)) ($array$) - - $name$ ((#v-gateways-name)) ($string: ingress-gateway$)`, + - $name$ ((#v-gateways-name)) ($string: ingress-gateway$) +`, }, "enterprise alert": { Input: `--- @@ -275,9 +360,14 @@ gateways: # line 2 key: value `, - Exp: `### key + Exp: `- [$key$](#key) + +## All Values + +### key -- $key$ ((#v-key)) ($string: value$) - line 1\n line 2`, +- $key$ ((#v-key)) ($string: value$) - line 1\n line 2 +`, }, "yaml comments in examples": { Input: `--- @@ -291,7 +381,11 @@ key: value # $$$ key: value `, - Exp: `### key + Exp: `- [$key$](#key) + +## All Values + +### key - $key$ ((#v-key)) ($string: value$) - Examples: @@ -300,7 +394,8 @@ key: value image: "consul:1.5.0" # Consul Enterprise 1.5.0 image: "hashicorp/consul-enterprise:1.5.0-ent" - $$$`, + $$$ +`, }, "type override uses last match": { Input: `--- @@ -308,9 +403,14 @@ key: value # @type: override-2 key: value `, - Exp: `### key + Exp: `- [$key$](#key) + +## All Values -- $key$ ((#v-key)) ($override-2: value$)`, +### key + +- $key$ ((#v-key)) ($override-2: value$) +`, }, "recurse false": { Input: `--- @@ -324,33 +424,49 @@ ports: - port: 8443 nodePort: null `, - Exp: `### key + Exp: `- [$key$](#key) +- [$ports$](#ports) + +## All Values + +### key - $key$ ((#v-key)) ($string: value$) ### ports -- $ports$ ((#v-ports)) ($array$) - port docs`, +- $ports$ ((#v-ports)) ($array$) - port docs +`, }, "@type: map": { Input: `--- # @type: map key: null `, - Exp: `### key + Exp: `- [$key$](#key) + +## All Values -- $key$ ((#v-key)) ($map$)`, +### key + +- $key$ ((#v-key)) ($map$) +`, }, "if of type map and not annotated with @type": { Input: `--- key: foo: bar `, - Exp: `### key + Exp: `- [$key$](#key) + +## All Values + +### key - $key$ ((#v-key)) - - $foo$ ((#v-key-foo)) ($string: bar$)`, + - $foo$ ((#v-key-foo)) ($string: bar$) +`, }, } @@ -368,6 +484,8 @@ key: // Swap \n for real \n. exp = strings.Replace(exp, "\\n", "\n", -1) + exp = tocPrefix + exp + require.Equal(t, exp, out) }) } diff --git a/charts/consul/hack/helm-reference-gen/parse_error.go b/hack/helm-reference-gen/parse_error.go similarity index 100% rename from charts/consul/hack/helm-reference-gen/parse_error.go rename to hack/helm-reference-gen/parse_error.go