From 6047d932e90b11b5e567ff183f57865e4512cb8a Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 12:13:56 -0700 Subject: [PATCH] chore(oauth): Refactor Command Issuer to use kubebuilder v4 & implement oauth/WI as auth mechanism Signed-off-by: Hayden Roszell --- .dockerignore | 1 - .github/workflows/helm.yml | 74 ++ .../keyfactor-bootstrap-workflow.yml | 23 + .github/workflows/release.yml | 213 ----- .golangci.yml | 40 + Dockerfile | 6 +- Makefile | 161 ++-- PROJECT | 5 +- README.md | 123 ++- api/v1alpha1/clusterissuer_types.go | 18 +- api/v1alpha1/groupversion_info.go | 2 +- api/v1alpha1/issuer_types.go | 111 ++- api/v1alpha1/issuer_types_test.go | 180 ++++ api/v1alpha1/zz_generated.deepcopy.go | 3 +- main.go => cmd/main.go | 113 ++- ...icenseheader.go.txt => boilerplate.go.txt} | 4 +- ...d-issuer.keyfactor.com_clusterissuers.yaml | 91 +- .../command-issuer.keyfactor.com_issuers.yaml | 91 +- config/crd/kustomization.yaml | 16 +- .../cainjection_in_clusterissuers.yaml | 7 - .../crd/patches/cainjection_in_issuers.yaml | 7 - .../patches/webhook_in_clusterissuers.yaml | 16 - config/crd/patches/webhook_in_issuers.yaml | 16 - config/default/kustomization.yaml | 146 +++- config/default/manager_auth_proxy_patch.yaml | 20 +- config/manager/kustomization.yaml | 2 +- config/manager/manager.yaml | 12 +- config/prometheus/monitor.yaml | 5 +- .../rbac/auth_proxy_client_clusterrole.yaml | 4 +- config/rbac/auth_proxy_role.yaml | 4 +- config/rbac/auth_proxy_role_binding.yaml | 4 +- config/rbac/auth_proxy_service.yaml | 4 +- config/rbac/clusterissuer_editor_role.yaml | 4 +- config/rbac/clusterissuer_viewer_role.yaml | 4 +- config/rbac/issuer_editor_role.yaml | 4 +- config/rbac/issuer_viewer_role.yaml | 4 +- config/rbac/leader_election_role.yaml | 16 +- config/rbac/leader_election_role_binding.yaml | 4 +- config/rbac/role.yaml | 1 - config/rbac/role_binding.yaml | 4 +- config/rbac/service_account.yaml | 6 +- config/samples/certificate.yaml | 15 - config/samples/certificaterequest.yaml | 10 - ...command-issuer_v1alpha1_clusterissuer.yaml | 12 +- .../command-issuer_v1alpha1_issuer.yaml | 12 +- config/samples/kustomization.yaml | 5 + docs/annotations.markdown | 63 -- docs/config_usage.markdown | 243 ------ docs/example.markdown | 189 ---- docs/install.markdown | 128 --- docs/testing.markdown | 32 - docsource/overview.md | 364 ++++++++ go.mod | 132 +-- go.sum | 495 ++++++----- integration-manifest.json | 10 +- internal/command/client.go | 165 ++++ internal/command/command.go | 499 +++++++++++ internal/command/command_test.go | 807 ++++++++++++++++++ .../certificaterequest_controller.go | 149 ++-- .../certificaterequest_controller_test.go | 478 +++++++---- internal/controller/issuer_controller.go | 255 ++++++ internal/controller/issuer_controller_test.go | 621 ++++++++++++++ .../controllers/fake_configclient_test.go | 57 -- internal/controllers/issuer_controller.go | 177 ---- .../controllers/issuer_controller_test.go | 293 ------- internal/controllers/suite_test.go | 80 -- internal/issuer/signer/signer.go | 438 ---------- internal/issuer/signer/signer_test.go | 559 ------------ internal/issuer/util/configclient.go | 164 ---- internal/issuer/util/configclient_test.go | 88 -- internal/issuer/util/util.go | 106 --- 71 files changed, 4457 insertions(+), 3758 deletions(-) create mode 100644 .github/workflows/helm.yml create mode 100644 .github/workflows/keyfactor-bootstrap-workflow.yml delete mode 100644 .github/workflows/release.yml create mode 100644 .golangci.yml create mode 100644 api/v1alpha1/issuer_types_test.go rename main.go => cmd/main.go (68%) rename config/{licenseheader.go.txt => boilerplate.go.txt} (94%) delete mode 100644 config/crd/patches/cainjection_in_clusterissuers.yaml delete mode 100644 config/crd/patches/cainjection_in_issuers.yaml delete mode 100644 config/crd/patches/webhook_in_clusterissuers.yaml delete mode 100644 config/crd/patches/webhook_in_issuers.yaml delete mode 100644 config/samples/certificate.yaml delete mode 100644 config/samples/certificaterequest.yaml create mode 100644 config/samples/kustomization.yaml delete mode 100644 docs/annotations.markdown delete mode 100644 docs/config_usage.markdown delete mode 100644 docs/example.markdown delete mode 100644 docs/install.markdown delete mode 100644 docs/testing.markdown create mode 100644 docsource/overview.md create mode 100644 internal/command/client.go create mode 100644 internal/command/command.go create mode 100644 internal/command/command_test.go rename internal/{controllers => controller}/certificaterequest_controller.go (69%) rename internal/{controllers => controller}/certificaterequest_controller_test.go (56%) create mode 100644 internal/controller/issuer_controller.go create mode 100644 internal/controller/issuer_controller_test.go delete mode 100644 internal/controllers/fake_configclient_test.go delete mode 100644 internal/controllers/issuer_controller.go delete mode 100644 internal/controllers/issuer_controller_test.go delete mode 100644 internal/controllers/suite_test.go delete mode 100644 internal/issuer/signer/signer.go delete mode 100644 internal/issuer/signer/signer_test.go delete mode 100644 internal/issuer/util/configclient.go delete mode 100644 internal/issuer/util/configclient_test.go delete mode 100644 internal/issuer/util/util.go diff --git a/.dockerignore b/.dockerignore index 0f04682..a3aab7a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file # Ignore build and test binaries. bin/ -testbin/ diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml new file mode 100644 index 0000000..e1cd857 --- /dev/null +++ b/.github/workflows/helm.yml @@ -0,0 +1,74 @@ +name: Build and Release +on: + push: + branches: + - '*' + pull_request: + branches: + - 'v*' + types: + # action should run when the pull request is closed + # (regardless of whether it was merged or just closed) + - closed + # Make sure the action runs every time new commits are + # pushed to the pull request's branch + - synchronize + +env: + REGISTRY: ghcr.io + +jobs: + helm: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - name: Set IMAGE_NAME + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} + + # Checkout code + # https://github.com/actions/checkout + - name: Checkout code + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + + # Extract metadata (tags, labels) to use in Helm chart + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Set version from DOCKER_METADATA_OUTPUT_VERSION as environment variable + - name: Set Version + run: | + echo "VERSION=${DOCKER_METADATA_OUTPUT_VERSION:1}" >> $GITHUB_ENV + + # Change version and appVersion in Chart.yaml to the tag in the closed PR + - name: Update Helm App/Chart Version + shell: bash + run: | + sed -i "s/^version: .*/version: ${{ env.VERSION }}/g" deploy/charts/command-cert-manager-issuer/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"${{ env.DOCKER_METADATA_OUTPUT_VERSION }}\"/g" deploy/charts/command-cert-manager-issuer/Chart.yaml + + # Setup Helm + # https://github.com/Azure/setup-helm + - name: Install Helm + uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + # Helm requires an ident name to be set for chart-releaser to work + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + # Build and release Helm chart to GitHub Pages + # https://github.com/helm/chart-releaser-action + - name: Run chart-releaser + uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + with: + charts_dir: deploy/charts diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml new file mode 100644 index 0000000..573db7e --- /dev/null +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -0,0 +1,23 @@ +name: Keyfactor Bootstrap Workflow + +on: + workflow_dispatch: + pull_request: + types: [opened, closed, synchronize, edited, reopened] + push: + create: + branches: + - 'release-*.*' + +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@v3 + secrets: + token: ${{ secrets.V2BUILDTOKEN}} + APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} + docker-user: ${{ secrets.DOCKER_USER }} + docker-token: ${{ secrets.DOCKER_PWD }} + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 65f622a..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,213 +0,0 @@ -name: Build and Release -on: - push: - branches: - - '*' - pull_request: - branches: - - 'v*' - types: - # action should run when the pull request is closed - # (regardless of whether it was merged or just closed) - - closed - # Make sure the action runs every time new commits are - # pushed to the pull request's branch - - synchronize - -env: - REGISTRY: ghcr.io - -jobs: - build: - name: Build Containers - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - platform: - - linux/arm64 - - linux/amd64 - - linux/s390x - - linux/ppc64le - - permissions: - contents: read - packages: write - - steps: - - - name: Set IMAGE_NAME - run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} - - # Checkout code - # https://github.com/actions/checkout - - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - # Set up QEMU - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - - # Set up BuildKit Docker container builder to be able to build - # multi-platform images and export cache - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - - # Login to Docker registry - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Build and push Docker image with Buildx - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build - uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 - with: - context: . - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - push: ${{ github.event.pull_request.merged == true }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true - - # Export digest - - name: Export digest - if: github.event.pull_request.merged == true - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - # Upload digest - - name: Upload digest - if: github.event.pull_request.merged == true - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 - with: - name: digests - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge: - runs-on: ubuntu-latest - if: github.event.pull_request.merged == true - needs: - - build - steps: - - name: Set IMAGE_NAME - run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} - - # Download digests - # https://github.com/actions/download-artifact - - name: Download digests - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 - with: - name: digests - path: /tmp/digests - - # Set up BuildKit Docker container builder to be able to build - # multi-platform images and export cache - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - # Login to Docker registry - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Create manifest list and push - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - helm: - runs-on: ubuntu-latest - if: github.event.pull_request.merged == true - needs: - - merge - steps: - - name: Set IMAGE_NAME - run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} - - # Checkout code - # https://github.com/actions/checkout - - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - - # Extract metadata (tags, labels) to use in Helm chart - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - # Set version from DOCKER_METADATA_OUTPUT_VERSION as environment variable - - name: Set Version - run: | - echo "VERSION=${DOCKER_METADATA_OUTPUT_VERSION:1}" >> $GITHUB_ENV - - # Change version and appVersion in Chart.yaml to the tag in the closed PR - - name: Update Helm App/Chart Version - shell: bash - run: | - sed -i "s/^version: .*/version: ${{ env.VERSION }}/g" deploy/charts/command-cert-manager-issuer/Chart.yaml - sed -i "s/^appVersion: .*/appVersion: \"${{ env.DOCKER_METADATA_OUTPUT_VERSION }}\"/g" deploy/charts/command-cert-manager-issuer/Chart.yaml - - # Setup Helm - # https://github.com/Azure/setup-helm - - name: Install Helm - uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - # Helm requires an ident name to be set for chart-releaser to work - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - # Build and release Helm chart to GitHub Pages - # https://github.com/helm/chart-releaser-action - - name: Run chart-releaser - uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0 - env: - CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - with: - charts_dir: deploy/charts \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..aed8644 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,40 @@ +run: + deadline: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - exportloopref + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - staticcheck + - typecheck + - unconvert + - unparam + - unused diff --git a/Dockerfile b/Dockerfile index 0598be6..cce20f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.19 as builder +FROM golang:1.23.4 AS builder ARG TARGETOS ARG TARGETARCH @@ -12,7 +12,7 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY main.go main.go +COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/ internal/ @@ -21,7 +21,7 @@ COPY internal/ internal/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/Makefile b/Makefile index 4f6b288..ab65716 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,8 @@ -# The version which will be reported by the --version argument of each binary -# and which will be used as the Docker image tag -VERSION ?= latest -# The Docker repository name, overridden in CI. -DOCKER_REGISTRY ?= "" -DOCKER_IMAGE_NAME ?= "" -# Image URL to use all building/pushing image targets -IMG ?= ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${VERSION} +# Image URL to use all building/pushing image targets +IMG ?= command-issuer:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.26.0 +ENVTEST_K8S_VERSION = 1.29.0 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -17,6 +11,12 @@ else GOBIN=$(shell go env GOBIN) endif +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + # Setting SHELL to bash allows bash commands to be executed by recipes. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail @@ -29,7 +29,7 @@ all: build # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the -# target descriptions by '##'. The awk commands is responsible for reading the +# target descriptions by '##'. The awk command is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. @@ -50,7 +50,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(CONTROLLER_GEN) object:headerFile="config/licenseheader.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile="config/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. @@ -62,51 +62,69 @@ vet: ## Run go vet against code. .PHONY: test test: manifests generate fmt vet envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out -##@ Build +# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. +.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. +test-e2e: + go test ./test/e2e/ -v -ginkgo.v + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter & yamllint + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix -.PHONY: regcheck -regcheck: ## Check if the docker registry is set. - @test -n "$(DOCKER_REGISTRY)" || (echo "DOCKER_REGISTRY is not set" && exit 1) - @test -n "$(DOCKER_IMAGE_NAME)" || (echo "DOCKER_IMAGE_NAME is not set" && exit 1) +##@ Build .PHONY: build build: manifests generate fmt vet ## Build manager binary. - go build -o bin/manager main.go + go build -o bin/manager cmd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./main.go + go run ./cmd/main.go -# If you wish built the manager image targeting other platforms you can use the --platform flag. -# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build -docker-build: regcheck ## Build docker image with the manager. - docker build -t ${IMG} . +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t ${IMG} . -.PHONY: docker-push regcheck +.PHONY: docker-push docker-push: ## Push docker image with the manager. - docker push ${IMG} + $(CONTAINER_TOOL) push ${IMG} -# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: -# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ -# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ -# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) -# To properly provided solutions that supports more than one platform you should use this option. +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le .PHONY: docker-buildx -docker-buildx: regcheck ## Build and push docker image for the manager for cross-platform support +docker-buildx: ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - - docker buildx create --name project-v3-builder - docker buildx use project-v3-builder - - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - - docker buildx rm project-v3-builder + - $(CONTAINER_TOOL) buildx create --name project-v3-builder + $(CONTAINER_TOOL) buildx use project-v3-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm project-v3-builder rm Dockerfile.cross +.PHONY: build-installer +build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. + mkdir -p dist + @if [ -d "config/crd" ]; then \ + $(KUSTOMIZE) build config/crd > dist/install.yaml; \ + fi + echo "---" >> dist/install.yaml # Add a document separator before appending + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default >> dist/install.yaml + ##@ Deployment ifndef ignore-not-found @@ -115,62 +133,71 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | kubectl apply -f - + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default | kubectl apply -f - - -# Build the manager image for local development. This image is not intended to be used in production. -# Then, install it into the K8s cluster -.PHONY: deploy-local -deploy-local: manifests kustomize ## Build docker image with the manager. - docker build -t command-issuer-dev:latest -f Dockerfile . - cd config/manager && $(KUSTOMIZE) edit set image controller=command-issuer-dev:latest - $(KUSTOMIZE) build config/default | kubectl apply -f - + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - .PHONY: undeploy -undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - -##@ Build Dependencies +##@ Dependencies ## Location to install dependencies to -LOCALBIN ?= "$(shell pwd)/bin" +LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p $(LOCALBIN) ## Tool Binaries -KUSTOMIZE ?= $(LOCALBIN)/kustomize -CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen -ENVTEST ?= $(LOCALBIN)/setup-envtest +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) +ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) ## Tool Versions -KUSTOMIZE_VERSION ?= v3.8.7 -CONTROLLER_TOOLS_VERSION ?= v0.11.1 +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 +ENVTEST_VERSION ?= latest +GOLANGCI_LINT_VERSION ?= v1.54.2 -KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" .PHONY: kustomize -kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. $(KUSTOMIZE): $(LOCALBIN) - @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ - echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ - rm -rf $(LOCALBIN)/kustomize; \ - fi - test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) .PHONY: controller-gen -controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. $(CONTROLLER_GEN): $(LOCALBIN) - test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ - GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) .PHONY: envtest -envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. $(ENVTEST): $(LOCALBIN) - test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary (ideally with version) +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f $(1) ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ +} +endef diff --git a/PROJECT b/PROJECT index b72e279..529b0c6 100644 --- a/PROJECT +++ b/PROJECT @@ -4,8 +4,8 @@ # More info: https://book.kubebuilder.io/reference/project-config.html domain: keyfactor.com layout: -- go.kubebuilder.io/v3 -projectName: command-issuer +- go.kubebuilder.io/v4 +projectName: command-cert-manager-issuer repo: github.com/Keyfactor/command-issuer resources: - api: @@ -19,6 +19,7 @@ resources: version: v1alpha1 - api: crdVersion: v1 + namespaced: true controller: true domain: keyfactor.com group: command-issuer diff --git a/README.md b/README.md index 5b27c16..f63ae52 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,114 @@ - - Terraform logo - +# command-cert-manager-issuer +// TODO(user): Add simple overview of use/purpose -# Keyfactor Command Issuer for cert-manager +## Description +// TODO(user): An in-depth paragraph about your project and overview of use -[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) +## Getting Started -The Command external issuer for cert-manager allows users to enroll certificates with a CA managed by Keyfactor Command using cert-manager. This allows security administrators to manage the lifecycle of certificates for Kubernetes applications. +### Prerequisites +- go version v1.21.0+ +- docker version 17.03+. +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. -Cert-manager is a native Kubernetes certificate management controller which allows applications to get their certificates from a variety of CAs (Certification Authorities). It ensures certificates are valid and up to date, it also attempts to renew certificates at a configured time before expiration. +### To Deploy on the cluster +**Build and push your image to the location specified by `IMG`:** -## Community supported -We welcome contributions. +```sh +make docker-build docker-push IMG=/command-cert-manager-issuer:tag +``` -The cert-manager external issuer for Keyfactor command is open source and community supported, meaning that there is **no SLA** applicable for these tools. +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. -###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, see the [contribution guidelines](https://github.com/Keyfactor/command-k8s-csr-signer/blob/main/CONTRIBUTING.md) and use the **[Pull requests](../../pulls)** tab. +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy the Manager to the cluster with the image specified by `IMG`:** + +```sh +make deploy IMG=/command-cert-manager-issuer:tag +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. + +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +>**NOTE**: Ensure that the samples has default values to test it out. + +### To Uninstall +**Delete the instances (CRs) from the cluster:** + +```sh +kubectl delete -k config/samples/ +``` + +**Delete the APIs(CRDs) from the cluster:** + +```sh +make uninstall +``` + +**UnDeploy the controller from the cluster:** + +```sh +make undeploy +``` + +## Project Distribution + +Following are the steps to build the installer and distribute this project to users. + +1. Build the installer for the image built and published in the registry: + +```sh +make build-installer IMG=/command-cert-manager-issuer:tag +``` + +NOTE: The makefile target mentioned above generates an 'install.yaml' +file in the dist directory. This file contains all the resources built +with Kustomize, which are necessary to install this project without +its dependencies. + +2. Using the installer + +Users can just run kubectl apply -f to install the project, i.e.: + +```sh +kubectl apply -f https://raw.githubusercontent.com//command-cert-manager-issuer//dist/install.yaml +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +**NOTE:** Run `make help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. -* [Installation](docs/install.markdown) -* [Usage](docs/config_usage.markdown) -* [Example Usage](docs/example.markdown) -* [Customization](docs/annotations.markdown) -* [Testing the Source](docs/testing.markdown) diff --git a/api/v1alpha1/clusterissuer_types.go b/api/v1alpha1/clusterissuer_types.go index 7df7fd5..e5239d8 100644 --- a/api/v1alpha1/clusterissuer_types.go +++ b/api/v1alpha1/clusterissuer_types.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Keyfactor +Copyright © 2024 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,6 +20,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +var ( + _ IssuerLike = &ClusterIssuer{} +) + //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:scope=Cluster @@ -33,6 +37,18 @@ type ClusterIssuer struct { Status IssuerStatus `json:"status,omitempty"` } +func (c *ClusterIssuer) GetStatus() *IssuerStatus { + return &c.Status +} + +func (c *ClusterIssuer) GetSpec() *IssuerSpec { + return &c.Spec +} + +func (c *ClusterIssuer) IsClusterScoped() bool { + return true +} + //+kubebuilder:object:root=true // ClusterIssuerList contains a list of ClusterIssuer diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 235a9d6..90651cb 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Keyfactor +Copyright © 2024 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1alpha1/issuer_types.go b/api/v1alpha1/issuer_types.go index 159f3b7..d54634b 100644 --- a/api/v1alpha1/issuer_types.go +++ b/api/v1alpha1/issuer_types.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Keyfactor +Copyright © 2024 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,21 +17,46 @@ limitations under the License. package v1alpha1 import ( + "context" + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// +kubebuilder:object:generate=false +type IssuerLike interface { + GetStatus() *IssuerStatus + GetSpec() *IssuerSpec + IsClusterScoped() bool + client.Object +} + +var ( + _ IssuerLike = &Issuer{} ) // IssuerSpec defines the desired state of Issuer type IssuerSpec struct { // Hostname is the hostname of a Keyfactor Command instance. Hostname string `json:"hostname,omitempty"` + + // APIPath is the base path of the Command API. KeyfactorAPI by default + // +kubebuilder:default:=KeyfactorAPI + APIPath string `json:"apiPath,omitempty"` + // CertificateTemplate is the name of the certificate template to use. // Refer to the Keyfactor Command documentation for more information. CertificateTemplate string `json:"certificateTemplate,omitempty"` + // CertificateAuthorityLogicalName is the logical name of the certificate authority to use // E.g. "Keyfactor Root CA" or "Intermediate CA" CertificateAuthorityLogicalName string `json:"certificateAuthorityLogicalName,omitempty"` + // CertificateAuthorityHostname is the hostname associated with the Certificate Authority specified by // CertificateAuthorityLogicalName E.g. "ca.example.com" + // +optional CertificateAuthorityHostname string `json:"certificateAuthorityHostname,omitempty"` // A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth @@ -41,13 +66,33 @@ type IssuerSpec struct { // with the given name in the configured 'cluster resource namespace', which // is set as a flag on the controller component (and defaults to the // namespace that the controller runs in). + // +optional SecretName string `json:"commandSecretName,omitempty"` // The name of the secret containing the CA bundle to use when verifying // Command's server certificate. If specified, the CA bundle will be added to // the client trust roots for the Command issuer. // +optional - CaSecretName string `json:"caSecretName"` + CaSecretName string `json:"caSecretName,omitempty"` + + // A list of comma separated scopes used when requesting a Bearer token from an ambient token provider implied + // by the environment, rather than by commandSecretName. For example, could be set to + // api://{tenant ID}/.default when requesting an access token for Entra ID (DefaultAzureCredential). Has no + // effect on OAuth 2.0 Client Credential configuration - please specify the scopes for this method in an Opaque secret. + // +optional + Scopes string `json:"scopes,omitempty"` +} + +func (i *Issuer) GetStatus() *IssuerStatus { + return &i.Status +} + +func (i *Issuer) GetSpec() *IssuerSpec { + return &i.Spec +} + +func (i *Issuer) IsClusterScoped() bool { + return false } // IssuerStatus defines the observed state of Issuer @@ -58,6 +103,56 @@ type IssuerStatus struct { Conditions []IssuerCondition `json:"conditions,omitempty"` } +func (is *IssuerStatus) SetCondition(ctx context.Context, conditionType IssuerConditionType, state ConditionStatus, reason, message string) { + log := ctrl.LoggerFrom(ctx) + var condition *IssuerCondition + + for i := range is.Conditions { + if is.Conditions[i].Type == conditionType { + condition = &is.Conditions[i] + break + } + } + + // If the status object doesn't already have a conditionType, add it + if condition == nil { + condition = &IssuerCondition{ + Type: conditionType, + } + is.Conditions = append(is.Conditions, *condition) + condition = &is.Conditions[len(is.Conditions)-1] + } + + if condition.Status != state { + log.Info(fmt.Sprintf("Changing %s Condition from %q -> %q; %q", conditionType, condition.Status, state, message)) + + condition.Status = state + now := metav1.Now() + condition.LastTransitionTime = &now + } + condition.Reason = reason + condition.Message = message +} + +func (is *IssuerStatus) HasCondition(conditionType IssuerConditionType, state ConditionStatus) bool { + for _, c := range is.Conditions { + if c.Type == conditionType && c.Status == state { + return true + } + } + return false +} + +func (is *IssuerStatus) UnsetCondition(conditionType IssuerConditionType) { + conditions := is.Conditions + for i, c := range conditions { + if c.Type == conditionType { + is.Conditions = append(conditions[:i], conditions[i+1:]...) + return + } + } +} + //+kubebuilder:object:root=true //+kubebuilder:subresource:status @@ -103,6 +198,14 @@ type IssuerCondition struct { Message string `json:"message,omitempty"` } +const ( + OAuthTokenURLKey = "tokenUrl" + OAuthClientIDKey = "clientId" + OAuthClientSecretKey = "clientSecret" + OAuthScopesKey = "scopes" + OAuthAudienceKey = "audience" +) + // IssuerConditionType represents an Issuer condition value. type IssuerConditionType string @@ -112,6 +215,10 @@ const ( // If the `status` of this condition is `False`, CertificateRequest controllers // should prevent attempts to sign certificates. IssuerConditionReady IssuerConditionType = "Ready" + + // IssuerConditionSupportsMetadata represents the fact that the connected Command platform supports + // the pre-defined metadata fields that Command Issuer populates. + IssuerConditionSupportsMetadata IssuerConditionType = "SupportsMetadata" ) // ConditionStatus represents a condition's status. diff --git a/api/v1alpha1/issuer_types_test.go b/api/v1alpha1/issuer_types_test.go new file mode 100644 index 0000000..261df05 --- /dev/null +++ b/api/v1alpha1/issuer_types_test.go @@ -0,0 +1,180 @@ +/* +Copyright © 2024 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func TestIssuerStatus_SetCondition_NewCondition(t *testing.T) { + ctx := ctrl.LoggerInto(context.Background(), log.Log) + issuerStatus := &IssuerStatus{} // no conditions initially + + issuerStatus.SetCondition(ctx, IssuerConditionReady, ConditionTrue, "InitialReason", "InitialMessage") + + assert.Len(t, issuerStatus.Conditions, 1, "Expected exactly one condition to be set.") + cond := issuerStatus.Conditions[0] + assert.Equal(t, IssuerConditionReady, cond.Type) + assert.Equal(t, ConditionTrue, cond.Status) + assert.Equal(t, "InitialReason", cond.Reason) + assert.Equal(t, "InitialMessage", cond.Message) + assert.NotNil(t, cond.LastTransitionTime, "LastTransitionTime should be set for a new condition.") +} + +func TestIssuerStatus_SetCondition_UpdateConditionStatus(t *testing.T) { + ctx := ctrl.LoggerInto(context.Background(), log.Log) + now := v1.Now() + + issuerStatus := &IssuerStatus{ + Conditions: []IssuerCondition{ + { + Type: IssuerConditionReady, + Status: ConditionFalse, + LastTransitionTime: &now, // simulate an existing condition with some prior time + Reason: "OldReason", + Message: "OldMessage", + }, + }, + } + + issuerStatus.SetCondition(ctx, IssuerConditionReady, ConditionTrue, "NewReason", "NewMessage") + + assert.Len(t, issuerStatus.Conditions, 1) + cond := issuerStatus.Conditions[0] + assert.Equal(t, IssuerConditionReady, cond.Type) + assert.Equal(t, ConditionTrue, cond.Status) + assert.Equal(t, "NewReason", cond.Reason) + assert.Equal(t, "NewMessage", cond.Message) + + // LastTransitionTime should be updated because status changed from ConditionFalse -> ConditionTrue + assert.True(t, cond.LastTransitionTime.Time.After(now.Time), "LastTransitionTime should be more recent if the status changed.") +} + +func TestIssuerStatus_SetCondition_NoStatusChange(t *testing.T) { + ctx := ctrl.LoggerInto(context.Background(), log.Log) + oldTime := v1.NewTime(time.Now().Add(-10 * time.Minute)) + + issuerStatus := &IssuerStatus{ + Conditions: []IssuerCondition{ + { + Type: IssuerConditionReady, + Status: ConditionTrue, + LastTransitionTime: &oldTime, + Reason: "ExistingReason", + Message: "ExistingMessage", + }, + }, + } + + issuerStatus.SetCondition(ctx, IssuerConditionReady, ConditionTrue, "UpdatedReason", "UpdatedMessage") + + assert.Len(t, issuerStatus.Conditions, 1) + cond := issuerStatus.Conditions[0] + assert.Equal(t, IssuerConditionReady, cond.Type) + assert.Equal(t, ConditionTrue, cond.Status) + + // Because status didn't actually change (still ConditionTrue), + // LastTransitionTime should NOT be updated. + assert.Equal(t, oldTime.Time, cond.LastTransitionTime.Time, "LastTransitionTime should remain unchanged if status didn't change.") + + // However, reason and message should be updated. + assert.Equal(t, "UpdatedReason", cond.Reason) + assert.Equal(t, "UpdatedMessage", cond.Message) +} + +func TestIssuerStatus_HasCondition(t *testing.T) { + issuerStatus := &IssuerStatus{ + Conditions: []IssuerCondition{ + { + Type: IssuerConditionReady, + Status: ConditionTrue, + }, + { + Type: IssuerConditionSupportsMetadata, + Status: ConditionFalse, + }, + }, + } + + assert.True(t, issuerStatus.HasCondition(IssuerConditionReady, ConditionTrue), "Should find Ready=True condition.") + assert.False(t, issuerStatus.HasCondition(IssuerConditionReady, ConditionFalse), "Ready=False does not exist.") + assert.True(t, issuerStatus.HasCondition(IssuerConditionSupportsMetadata, ConditionFalse), "Should find SupportsMetadata=False condition.") + assert.False(t, issuerStatus.HasCondition(IssuerConditionSupportsMetadata, ConditionTrue), "SupportsMetadata=True does not exist.") + assert.False(t, issuerStatus.HasCondition("NonExistent", ConditionTrue), "Non-existent type should be false.") +} + +func TestIssuerStatus_UnsetCondition(t *testing.T) { + issuerStatus := &IssuerStatus{ + Conditions: []IssuerCondition{ + { + Type: IssuerConditionReady, + Status: ConditionTrue, + }, + { + Type: IssuerConditionSupportsMetadata, + Status: ConditionFalse, + }, + }, + } + + issuerStatus.UnsetCondition(IssuerConditionReady) + + assert.Len(t, issuerStatus.Conditions, 1, "Expected to remove 1 condition.") + assert.Equal(t, IssuerConditionSupportsMetadata, issuerStatus.Conditions[0].Type, "SupportsMetadata should remain.") + + // Trying to unset a condition that no longer exists should do nothing + issuerStatus.UnsetCondition(IssuerConditionReady) + assert.Len(t, issuerStatus.Conditions, 1, "No further removal should occur for missing condition.") +} + +func TestIssuerStatus_UnsetCondition_NoConditions(t *testing.T) { + issuerStatus := &IssuerStatus{} + + issuerStatus.UnsetCondition(IssuerConditionReady) + + assert.Empty(t, issuerStatus.Conditions, "No conditions to remove, so it should remain empty.") +} + +func TestIssuerStatus_SetCondition_AddsNewConditionIfNotFound(t *testing.T) { + ctx := ctrl.LoggerInto(context.Background(), log.Log) + issuerStatus := &IssuerStatus{ + Conditions: []IssuerCondition{ + { + Type: IssuerConditionReady, + Status: ConditionTrue, + }, + }, + } + + issuerStatus.SetCondition(ctx, IssuerConditionSupportsMetadata, ConditionFalse, "SomeReason", "SomeMessage") + + assert.Len(t, issuerStatus.Conditions, 2, "Expected a new condition to be appended.") + + newCond := issuerStatus.Conditions[1] + assert.Equal(t, IssuerConditionSupportsMetadata, newCond.Type) + assert.Equal(t, ConditionFalse, newCond.Status) + assert.Equal(t, "SomeReason", newCond.Reason) + assert.Equal(t, "SomeMessage", newCond.Message) + assert.NotNil(t, newCond.LastTransitionTime, "Newly added condition should set LastTransitionTime.") +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 73e01d8..3eb08e1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,8 +1,7 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* -Copyright 2023 Keyfactor. +Copyright © 2024 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/main.go b/cmd/main.go similarity index 68% rename from main.go rename to cmd/main.go index f51db11..49d5314 100644 --- a/main.go +++ b/cmd/main.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Keyfactor +Copyright 2024. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,20 +17,16 @@ limitations under the License. package main import ( - "context" + "crypto/tls" "errors" "flag" "fmt" - "github.com/Keyfactor/command-issuer/internal/controllers" - "github.com/Keyfactor/command-issuer/internal/issuer/signer" - "github.com/Keyfactor/command-issuer/internal/issuer/util" - cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" - "k8s.io/utils/clock" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/utils/clock" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -38,14 +34,20 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" - commandissuerv1alpha1 "github.com/Keyfactor/command-issuer/api/v1alpha1" + commandissuerv1alpha1 "github.com/Keyfactor/command-cert-manager-issuer/api/v1alpha1" + "github.com/Keyfactor/command-cert-manager-issuer/internal/command" + "github.com/Keyfactor/command-cert-manager-issuer/internal/controller" + cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" //+kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") + _ = cmapi.AddToScheme(scheme) ) func init() { @@ -53,16 +55,15 @@ func init() { utilruntime.Must(commandissuerv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme - - _ = cmapi.AddToScheme(scheme) } func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string + var secureMetrics bool + var enableHTTP2 bool var clusterResourceNamespace string - var printVersion bool var disableApprovedCheck bool var secretAccessGrantedAtClusterLevel bool @@ -71,13 +72,15 @@ func main() { flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", false, + "If set the metrics endpoint is served securely") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") flag.StringVar(&clusterResourceNamespace, "cluster-resource-namespace", "", "The namespace for secrets in which cluster-scoped resources are found.") - flag.BoolVar(&printVersion, "version", false, "Print version to stdout and exit") flag.BoolVar(&disableApprovedCheck, "disable-approved-check", false, "Disables waiting for CertificateRequests to have an approved condition before signing.") flag.BoolVar(&secretAccessGrantedAtClusterLevel, "secret-access-granted-at-cluster-level", false, "Set this flag to true if the secret access is granted at cluster level. This will allow the controller to access secrets in any namespace. ") - opts := zap.Options{ Development: true, } @@ -86,11 +89,31 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancelation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + tlsOpts := []func(*tls.Config){} + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: tlsOpts, + }) + if clusterResourceNamespace == "" { var err error - clusterResourceNamespace, err = util.GetInClusterNamespace() + clusterResourceNamespace, err = getInClusterNamespace() if err != nil { - if errors.Is(err, util.ErrNotInCluster) { + if errors.Is(err, ErrNotInCluster) { setupLog.Error(err, "please supply --cluster-resource-namespace") } else { setupLog.Error(err, "unexpected error while getting in-cluster Namespace") @@ -105,16 +128,14 @@ func main() { setupLog.Info(fmt.Sprintf("expecting secret access at namespace level (%s)", clusterResourceNamespace)) } - ctx := context.Background() - configClient, err := util.NewConfigClient(ctx) - if err != nil { - setupLog.Error(err, "error creating config client") - } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, - MetricsBindAddress: metricsAddr, - Port: 9443, + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + }, + WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "b68cef20.keyfactor.com", @@ -135,36 +156,33 @@ func main() { os.Exit(1) } - if err = (&controllers.IssuerReconciler{ - Kind: "Issuer", + if err = (&controller.IssuerReconciler{ Client: mgr.GetClient(), - ConfigClient: configClient, - Scheme: mgr.GetScheme(), + Kind: "Issuer", ClusterResourceNamespace: clusterResourceNamespace, SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, - HealthCheckerBuilder: signer.CommandHealthCheckerFromIssuerAndSecretData, + Scheme: mgr.GetScheme(), + HealthCheckerBuilder: command.NewHealthChecker, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Issuer") os.Exit(1) } - if err = (&controllers.IssuerReconciler{ - Kind: "ClusterIssuer", + if err = (&controller.IssuerReconciler{ Client: mgr.GetClient(), - ConfigClient: configClient, Scheme: mgr.GetScheme(), + Kind: "ClusterIssuer", ClusterResourceNamespace: clusterResourceNamespace, SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, - HealthCheckerBuilder: signer.CommandHealthCheckerFromIssuerAndSecretData, + HealthCheckerBuilder: command.NewHealthChecker, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterIssuer") os.Exit(1) } - if err = (&controllers.CertificateRequestReconciler{ + if err = (&controller.CertificateRequestReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - ConfigClient: configClient, ClusterResourceNamespace: clusterResourceNamespace, - SignerBuilder: signer.CommandSignerFromIssuerAndSecretData, + SignerBuilder: command.NewSignerBuilder, CheckApprovedCondition: !disableApprovedCheck, SecretAccessGrantedAtClusterLevel: secretAccessGrantedAtClusterLevel, Clock: clock.RealClock{}, @@ -189,3 +207,26 @@ func main() { os.Exit(1) } } + +var ErrNotInCluster = errors.New("not running in-cluster") + +const inClusterNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + +// Copied from controller-runtime/pkg/leaderelection +func getInClusterNamespace() (string, error) { + // Check whether the namespace file exists. + // If not, we are not running in cluster so can't guess the namespace. + _, err := os.Stat(inClusterNamespacePath) + if os.IsNotExist(err) { + return "", ErrNotInCluster + } else if err != nil { + return "", fmt.Errorf("error checking namespace file: %w", err) + } + + // Load the namespace file and return its content + namespace, err := os.ReadFile(inClusterNamespacePath) + if err != nil { + return "", fmt.Errorf("error reading namespace file: %w", err) + } + return string(namespace), nil +} diff --git a/config/licenseheader.go.txt b/config/boilerplate.go.txt similarity index 94% rename from config/licenseheader.go.txt rename to config/boilerplate.go.txt index b421450..f9ad5dd 100644 --- a/config/licenseheader.go.txt +++ b/config/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2023 Keyfactor. +Copyright © 2024 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -12,4 +12,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -*/ \ No newline at end of file +*/ diff --git a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml index 217acfe..665085e 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.1 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: clusterissuers.command-issuer.keyfactor.com spec: group: command-issuer.keyfactor.com @@ -21,74 +20,98 @@ spec: description: ClusterIssuer is the Schema for the clusterissuers 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' + 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' + 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: IssuerSpec defines the desired state of Issuer properties: + apiPath: + default: KeyfactorAPI + description: APIPath is the base path of the Command API. KeyfactorAPI + by default + type: string caSecretName: - description: The name of the secret containing the CA bundle to use - when verifying Command's server certificate. If specified, the CA - bundle will be added to the client trust roots for the Command issuer. + description: |- + The name of the secret containing the CA bundle to use when verifying + Command's server certificate. If specified, the CA bundle will be added to + the client trust roots for the Command issuer. type: string certificateAuthorityHostname: - description: CertificateAuthorityHostname is the hostname associated - with the Certificate Authority specified by CertificateAuthorityLogicalName - E.g. "ca.example.com" + description: |- + CertificateAuthorityHostname is the hostname associated with the Certificate Authority specified by + CertificateAuthorityLogicalName E.g. "ca.example.com" type: string certificateAuthorityLogicalName: - description: CertificateAuthorityLogicalName is the logical name of - the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate - CA" + description: |- + CertificateAuthorityLogicalName is the logical name of the certificate authority to use + E.g. "Keyfactor Root CA" or "Intermediate CA" type: string certificateTemplate: - description: CertificateTemplate is the name of the certificate template - to use. Refer to the Keyfactor Command documentation for more information. + description: |- + CertificateTemplate is the name of the certificate template to use. + Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: - description: A reference to a K8s kubernetes.io/basic-auth Secret - containing basic auth credentials for the Command instance configured - in Hostname. The secret must be in the same namespace as the referent. - If the referent is a ClusterIssuer, the reference instead refers - to the resource with the given name in the configured 'cluster resource - namespace', which is set as a flag on the controller component (and - defaults to the namespace that the controller runs in). + description: |- + A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth + credentials for the Command instance configured in Hostname. The secret must + be in the same namespace as the referent. If the + referent is a ClusterIssuer, the reference instead refers to the resource + with the given name in the configured 'cluster resource namespace', which + is set as a flag on the controller component (and defaults to the + namespace that the controller runs in). type: string hostname: description: Hostname is the hostname of a Keyfactor Command instance. type: string + scopes: + description: |- + A list of comma separated scopes used when requesting a Bearer token from an ambient token provider implied + by the environment, rather than by commandSecretName. For example, could be set to + api://{tenant ID}/.default when requesting an access token for Entra ID (DefaultAzureCredential). Has no + effect on OAuth 2.0 Client Credential configuration - please specify the scopes for this method in an Opaque secret. + type: string type: object status: description: IssuerStatus defines the observed state of Issuer properties: conditions: - description: List of status conditions to indicate the status of a - CertificateRequest. Known condition types are `Ready`. + description: |- + List of status conditions to indicate the status of a CertificateRequest. + Known condition types are `Ready`. items: description: IssuerCondition contains condition information for an Issuer. properties: lastTransitionTime: - description: LastTransitionTime is the timestamp corresponding - to the last status change of this condition. + description: |- + LastTransitionTime is the timestamp corresponding to the last status + change of this condition. format: date-time type: string message: - description: Message is a human readable description of the - details of the last transition, complementing reason. + description: |- + Message is a human readable description of the details of the last + transition, complementing reason. type: string reason: - description: Reason is a brief machine readable explanation - for the condition's last transition. + description: |- + Reason is a brief machine readable explanation for the condition's last + transition. type: string status: description: Status of the condition, one of ('True', 'False', diff --git a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml index ffcc231..986b0e1 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.1 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: issuers.command-issuer.keyfactor.com spec: group: command-issuer.keyfactor.com @@ -21,74 +20,98 @@ spec: description: Issuer is the Schema for the issuers 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' + 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' + 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: IssuerSpec defines the desired state of Issuer properties: + apiPath: + default: KeyfactorAPI + description: APIPath is the base path of the Command API. KeyfactorAPI + by default + type: string caSecretName: - description: The name of the secret containing the CA bundle to use - when verifying Command's server certificate. If specified, the CA - bundle will be added to the client trust roots for the Command issuer. + description: |- + The name of the secret containing the CA bundle to use when verifying + Command's server certificate. If specified, the CA bundle will be added to + the client trust roots for the Command issuer. type: string certificateAuthorityHostname: - description: CertificateAuthorityHostname is the hostname associated - with the Certificate Authority specified by CertificateAuthorityLogicalName - E.g. "ca.example.com" + description: |- + CertificateAuthorityHostname is the hostname associated with the Certificate Authority specified by + CertificateAuthorityLogicalName E.g. "ca.example.com" type: string certificateAuthorityLogicalName: - description: CertificateAuthorityLogicalName is the logical name of - the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate - CA" + description: |- + CertificateAuthorityLogicalName is the logical name of the certificate authority to use + E.g. "Keyfactor Root CA" or "Intermediate CA" type: string certificateTemplate: - description: CertificateTemplate is the name of the certificate template - to use. Refer to the Keyfactor Command documentation for more information. + description: |- + CertificateTemplate is the name of the certificate template to use. + Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: - description: A reference to a K8s kubernetes.io/basic-auth Secret - containing basic auth credentials for the Command instance configured - in Hostname. The secret must be in the same namespace as the referent. - If the referent is a ClusterIssuer, the reference instead refers - to the resource with the given name in the configured 'cluster resource - namespace', which is set as a flag on the controller component (and - defaults to the namespace that the controller runs in). + description: |- + A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth + credentials for the Command instance configured in Hostname. The secret must + be in the same namespace as the referent. If the + referent is a ClusterIssuer, the reference instead refers to the resource + with the given name in the configured 'cluster resource namespace', which + is set as a flag on the controller component (and defaults to the + namespace that the controller runs in). type: string hostname: description: Hostname is the hostname of a Keyfactor Command instance. type: string + scopes: + description: |- + A list of comma separated scopes used when requesting a Bearer token from an ambient token provider implied + by the environment, rather than by commandSecretName. For example, could be set to + api://{tenant ID}/.default when requesting an access token for Entra ID (DefaultAzureCredential). Has no + effect on OAuth 2.0 Client Credential configuration - please specify the scopes for this method in an Opaque secret. + type: string type: object status: description: IssuerStatus defines the observed state of Issuer properties: conditions: - description: List of status conditions to indicate the status of a - CertificateRequest. Known condition types are `Ready`. + description: |- + List of status conditions to indicate the status of a CertificateRequest. + Known condition types are `Ready`. items: description: IssuerCondition contains condition information for an Issuer. properties: lastTransitionTime: - description: LastTransitionTime is the timestamp corresponding - to the last status change of this condition. + description: |- + LastTransitionTime is the timestamp corresponding to the last status + change of this condition. format: date-time type: string message: - description: Message is a human readable description of the - details of the last transition, complementing reason. + description: |- + Message is a human readable description of the details of the last + transition, complementing reason. type: string reason: - description: Reason is a brief machine readable explanation - for the condition's last transition. + description: |- + Reason is a brief machine readable explanation for the condition's last + transition. type: string status: description: Status of the condition, one of ('True', 'False', diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b01a906..f24e57f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,19 +6,21 @@ resources: - bases/command-issuer.keyfactor.com_clusterissuers.yaml #+kubebuilder:scaffold:crdkustomizeresource -patchesStrategicMerge: +patches: # [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_issuers.yaml -#- patches/webhook_in_clusterissuers.yaml +#- path: patches/webhook_in_issuers.yaml +#- path: patches/webhook_in_clusterissuers.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -#- patches/cainjection_in_issuers.yaml -#- patches/cainjection_in_clusterissuers.yaml +#- path: patches/cainjection_in_issuers.yaml +#- path: patches/cainjection_in_clusterissuers.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch +# [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. -configurations: -- kustomizeconfig.yaml + +#configurations: +#- kustomizeconfig.yaml diff --git a/config/crd/patches/cainjection_in_clusterissuers.yaml b/config/crd/patches/cainjection_in_clusterissuers.yaml deleted file mode 100644 index 299f859..0000000 --- a/config/crd/patches/cainjection_in_clusterissuers.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: clusterissuers.command-issuer.keyfactor.com diff --git a/config/crd/patches/cainjection_in_issuers.yaml b/config/crd/patches/cainjection_in_issuers.yaml deleted file mode 100644 index e978cdf..0000000 --- a/config/crd/patches/cainjection_in_issuers.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# The following patch adds a directive for certmanager to inject CA into the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: issuers.command-issuer.keyfactor.com diff --git a/config/crd/patches/webhook_in_clusterissuers.yaml b/config/crd/patches/webhook_in_clusterissuers.yaml deleted file mode 100644 index 1aa7338..0000000 --- a/config/crd/patches/webhook_in_clusterissuers.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: clusterissuers.command-issuer.keyfactor.com -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/crd/patches/webhook_in_issuers.yaml b/config/crd/patches/webhook_in_issuers.yaml deleted file mode 100644 index 9751996..0000000 --- a/config/crd/patches/webhook_in_issuers.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: issuers.command-issuer.keyfactor.com -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert - conversionReviewVersions: - - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 1b264db..4c4a296 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -9,10 +9,12 @@ namespace: command-issuer-system namePrefix: command-issuer- # Labels to add to all resources and selectors. -#commonLabels: -# someName: someValue +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue -bases: +resources: - ../crd - ../rbac - ../manager @@ -24,49 +26,113 @@ bases: # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus -patchesStrategicMerge: -# 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. -- manager_auth_proxy_patch.yaml - - +patches: [] # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- manager_webhook_patch.yaml +#- path: manager_webhook_patch.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 -#- webhookcainjection_patch.yaml +#- path: webhookcainjection_patch.yaml -# the following config is for teaching kustomize how to do var substitution -vars: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldref: -# fieldpath: metadata.namespace -#- name: CERTIFICATE_NAME -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -#- name: SERVICE_NAMESPACE # namespace of the service -# objref: -# kind: Service -# version: v1 -# name: webhook-service -# fieldref: -# fieldpath: metadata.namespace -#- name: SERVICE_NAME -# objref: -# kind: Service -# version: v1 -# name: webhook-service +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml index b751266..70c3437 100644 --- a/config/default/manager_auth_proxy_patch.yaml +++ b/config/default/manager_auth_proxy_patch.yaml @@ -8,30 +8,14 @@ metadata: spec: template: spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/arch - operator: In - values: - - amd64 - - arm64 - - ppc64le - - s390x - - key: kubernetes.io/os - operator: In - values: - - linux containers: - name: kube-rbac-proxy securityContext: allowPrivilegeEscalation: false capabilities: drop: - - "ALL" - image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 args: - "--secure-listen-address=0.0.0.0:8443" - "--upstream=http://127.0.0.1:8080/" diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index ace19ce..187e964 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: ghcr.io/keyfactor/command-cert-manager-issuer + newName: command-issuer newTag: latest diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 14cc446..2bb1556 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: namespace app.kubernetes.io/instance: system app.kubernetes.io/component: manager - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: system --- @@ -21,8 +21,8 @@ metadata: app.kubernetes.io/name: deployment app.kubernetes.io/instance: controller-manager app.kubernetes.io/component: manager - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize spec: selector: @@ -71,13 +71,13 @@ spec: args: - --leader-elect image: controller:latest - #imagePullPolicy: Never # TODO dev parameter + imagePullPolicy: Never name: manager securityContext: allowPrivilegeEscalation: false capabilities: drop: - - "ALL" + - "ALL" livenessProbe: httpGet: path: /healthz diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml index ac16c41..2a1c4e3 100644 --- a/config/prometheus/monitor.yaml +++ b/config/prometheus/monitor.yaml @@ -1,4 +1,3 @@ - # Prometheus Monitor Service (Metrics) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor @@ -8,8 +7,8 @@ metadata: app.kubernetes.io/name: servicemonitor app.kubernetes.io/instance: controller-manager-metrics-monitor app.kubernetes.io/component: metrics - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system diff --git a/config/rbac/auth_proxy_client_clusterrole.yaml b/config/rbac/auth_proxy_client_clusterrole.yaml index 537854c..557e563 100644 --- a/config/rbac/auth_proxy_client_clusterrole.yaml +++ b/config/rbac/auth_proxy_client_clusterrole.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: metrics-reader app.kubernetes.io/component: kube-rbac-proxy - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: metrics-reader rules: diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml index 4d0a1ca..a3de401 100644 --- a/config/rbac/auth_proxy_role.yaml +++ b/config/rbac/auth_proxy_role.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: proxy-role app.kubernetes.io/component: kube-rbac-proxy - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: proxy-role rules: diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml index 453ded7..3fc03ca 100644 --- a/config/rbac/auth_proxy_role_binding.yaml +++ b/config/rbac/auth_proxy_role_binding.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: clusterrolebinding app.kubernetes.io/instance: proxy-rolebinding app.kubernetes.io/component: kube-rbac-proxy - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: proxy-rolebinding roleRef: diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml index 6f62356..cd012ae 100644 --- a/config/rbac/auth_proxy_service.yaml +++ b/config/rbac/auth_proxy_service.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: service app.kubernetes.io/instance: controller-manager-metrics-service app.kubernetes.io/component: kube-rbac-proxy - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-service namespace: system diff --git a/config/rbac/clusterissuer_editor_role.yaml b/config/rbac/clusterissuer_editor_role.yaml index 0001445..7f24331 100644 --- a/config/rbac/clusterissuer_editor_role.yaml +++ b/config/rbac/clusterissuer_editor_role.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: clusterissuer-editor-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: clusterissuer-editor-role rules: diff --git a/config/rbac/clusterissuer_viewer_role.yaml b/config/rbac/clusterissuer_viewer_role.yaml index 40153aa..d689518 100644 --- a/config/rbac/clusterissuer_viewer_role.yaml +++ b/config/rbac/clusterissuer_viewer_role.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: clusterissuer-viewer-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: clusterissuer-viewer-role rules: diff --git a/config/rbac/issuer_editor_role.yaml b/config/rbac/issuer_editor_role.yaml index 881d6d0..fcad0b2 100644 --- a/config/rbac/issuer_editor_role.yaml +++ b/config/rbac/issuer_editor_role.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: issuer-editor-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: issuer-editor-role rules: diff --git a/config/rbac/issuer_viewer_role.yaml b/config/rbac/issuer_viewer_role.yaml index 8bcb393..8aa1854 100644 --- a/config/rbac/issuer_viewer_role.yaml +++ b/config/rbac/issuer_viewer_role.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: issuer-viewer-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: issuer-viewer-role rules: diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml index bbeff7a..181727c 100644 --- a/config/rbac/leader_election_role.yaml +++ b/config/rbac/leader_election_role.yaml @@ -6,11 +6,23 @@ metadata: app.kubernetes.io/name: role app.kubernetes.io/instance: leader-election-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - coordination.k8s.io resources: diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml index d388603..985ea5f 100644 --- a/config/rbac/leader_election_role_binding.yaml +++ b/config/rbac/leader_election_role_binding.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: rolebinding app.kubernetes.io/instance: leader-election-rolebinding app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3606afe..8daaeb6 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -2,7 +2,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: manager-role rules: - apiGroups: diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml index 0344239..7d5e7d8 100644 --- a/config/rbac/role_binding.yaml +++ b/config/rbac/role_binding.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: clusterrolebinding app.kubernetes.io/instance: manager-rolebinding app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml index b190f20..f1c0785 100644 --- a/config/rbac/service_account.yaml +++ b/config/rbac/service_account.yaml @@ -3,10 +3,10 @@ kind: ServiceAccount metadata: labels: app.kubernetes.io/name: serviceaccount - app.kubernetes.io/instance: controller-manager + app.kubernetes.io/instance: controller-manager-sa app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: command-issuer - app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-cert-manager-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer app.kubernetes.io/managed-by: kustomize name: controller-manager namespace: system diff --git a/config/samples/certificate.yaml b/config/samples/certificate.yaml deleted file mode 100644 index 4a11be7..0000000 --- a/config/samples/certificate.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: command-certificate - annotations: - command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" - command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" - metadata.command-issuer.keyfactor.com/ResponsibleTeam: "theResponsibleTeam@example.com" -spec: - commonName: command-issuer-sample - secretName: command-certificate - issuerRef: - name: issuer-sample - group: command-issuer.keyfactor.com - kind: Issuer \ No newline at end of file diff --git a/config/samples/certificaterequest.yaml b/config/samples/certificaterequest.yaml deleted file mode 100644 index ffb42f9..0000000 --- a/config/samples/certificaterequest.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: CertificateRequest -metadata: - name: issuer-sample -spec: - request: LS0tLS1CRU... # base64 encoded CSR - issuerRef: - name: issuer-sample - group: command-issuer.keyfactor.com - kind: Issuer \ No newline at end of file diff --git a/config/samples/command-issuer_v1alpha1_clusterissuer.yaml b/config/samples/command-issuer_v1alpha1_clusterissuer.yaml index 4cce43e..1f4084a 100644 --- a/config/samples/command-issuer_v1alpha1_clusterissuer.yaml +++ b/config/samples/command-issuer_v1alpha1_clusterissuer.yaml @@ -4,13 +4,9 @@ metadata: labels: app.kubernetes.io/name: clusterissuer app.kubernetes.io/instance: clusterissuer-sample - app.kubernetes.io/part-of: command-issuer - app.kubernetes.io/created-by: command-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: command-cert-manager-issuer name: clusterissuer-sample spec: - hostname: "" - commandSecretName: "" - certificateTemplate: "" - certificateAuthorityLogicalName: "" - certificateAuthorityHostname: "" - caSecretName: "" + # TODO(user): Add fields here diff --git a/config/samples/command-issuer_v1alpha1_issuer.yaml b/config/samples/command-issuer_v1alpha1_issuer.yaml index faa4d88..276114e 100644 --- a/config/samples/command-issuer_v1alpha1_issuer.yaml +++ b/config/samples/command-issuer_v1alpha1_issuer.yaml @@ -4,13 +4,9 @@ metadata: labels: app.kubernetes.io/name: issuer app.kubernetes.io/instance: issuer-sample - app.kubernetes.io/part-of: command-issuer - app.kubernetes.io/created-by: command-issuer + app.kubernetes.io/part-of: command-cert-manager-issuer + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: command-cert-manager-issuer name: issuer-sample spec: - hostname: "" - commandSecretName: "" - certificateTemplate: "" - certificateAuthorityLogicalName: "" - certificateAuthorityHostname: "" - caSecretName: "" + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 0000000..1465052 --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,5 @@ +## Append samples of your project ## +resources: +- command-issuer_v1alpha1_issuer.yaml +- command-issuer_v1alpha1_clusterissuer.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/docs/annotations.markdown b/docs/annotations.markdown deleted file mode 100644 index afa9a63..0000000 --- a/docs/annotations.markdown +++ /dev/null @@ -1,63 +0,0 @@ - - Terraform logo - - -# Annotation Overrides for Issuer and ClusterIssuer Resources - -[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) - -The Keyfactor Command external issuer for cert-manager allows you to override default settings in the Issuer and ClusterIssuer resources through the use of annotations. This gives you more granular control on a per-Certificate/CertificateRequest basis. - -### Documentation Tree -* [Installation](install.markdown) -* [Usage](config_usage.markdown) -* [Example Usage](example.markdown) -* [Testing the Source](testing.markdown) - -### Supported Annotations -Here are the supported annotations that can override the default values: - -- **`command-issuer.keyfactor.com/certificateTemplate`**: Overrides the `certificateTemplate` field from the resource spec. - - ```yaml - command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" - ``` - -- **`command-issuer.keyfactor.com/certificateAuthorityLogicalName`**: Specifies the Certificate Authority (CA) logical name to use, overriding the default CA specified in the resource spec. - - ```yaml - command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" - ``` - -- **`command-issuer.keyfactor.com/certificateAuthorityHostname`**: Specifies the Certificate Authority (CA) hostname to use, overriding the default CA specified in the resource spec. - - ```yaml - command-issuer.keyfactor.com/certificateAuthorityHostname: "example.com" - ``` - -### Metadata Annotations - -The Keyfactor Command external issuer for cert-manager also allows you to specify Command Metadata through the use of annotations. Metadata attached to a certificate request will be stored in Command and can be used for reporting and auditing purposes. The syntax for specifying metadata is as follows: -```yaml -metadata.command-issuer.keyfactor.com/: -``` - -###### :pushpin: The metadata field name must match a name of a metadata field in Command exactly. If the metadata field name does not match, the CSR enrollment will fail. - -### How to Apply Annotations - -To apply these annotations, include them in the metadata section of your CertificateRequest resource: - -```yaml -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - annotations: - command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" - command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" - metadata.command-issuer.keyfactor.com/ResponsibleTeam: "theResponsibleTeam@example.com" - # ... other annotations -spec: -# ... the rest of the spec -``` \ No newline at end of file diff --git a/docs/config_usage.markdown b/docs/config_usage.markdown deleted file mode 100644 index 6e1c299..0000000 --- a/docs/config_usage.markdown +++ /dev/null @@ -1,243 +0,0 @@ - - Terraform logo - - -# Command Cert Manager Issuer Usage - -[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) - -The cert-manager external issuer for Keyfactor Command can be used to issue certificates from Keyfactor Command using cert-manager. - -### Documentation Tree -* [Installation](install.markdown) -* [Example Usage](example.markdown) -* [Customization](annotations.markdown) -* [Testing the Source](testing.markdown) - -### Keyfactor Command Configuration -The Command Issuer for cert-manager populates metadata fields on issued certificates in Command pertaining to the K8s cluster and cert-manager Issuer/ClusterIssuer. Before deploying Issuers/ClusterIssuers, these metadata fields must be created in Command. To easily create these metadata fields, use the `kfutil` Keyfactor command line tool that offers convenient and powerful command line access to the Keyfactor platform. Before proceeding, ensure that `kfutil` is installed and configured by following the instructions here: [https://github.com/Keyfactor/kfutil](https://github.com/Keyfactor/kfutil). - -Then, use the `import` command to import the metadata fields into Command: -```shell -cat <> metadata.json -{ - "Collections": [], - "MetadataFields": [ - { - "AllowAPI": true, - "DataType": 1, - "Description": "The namespace that the issuer resource was created in that .", - "Name": "Issuer-Namespace" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The certificate reconcile ID that the controller used to issue this certificate.", - "Name": "Controller-Reconcile-Id" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The namespace that the CertificateSigningRequest resource was created in.", - "Name": "Certificate-Signing-Request-Namespace" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The namespace that the controller container is running in.", - "Name": "Controller-Namespace" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The type of issuer that the controller used to issue this certificate.", - "Name": "Controller-Kind" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The group name of the resource that the Issuer or ClusterIssuer controller is managing.", - "Name": "Controller-Resource-Group-Name" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The name of the K8s issuer resource", - "Name": "Issuer-Name" - } - ], - "ExpirationAlerts": [], - "IssuedCertAlerts": [], - "DeniedCertAlerts": [], - "PendingCertAlerts": [], - "Networks": [], - "WorkflowDefinitions": [], - "BuiltInReports": [], - "CustomReports": [], - "SecurityRoles": [] -} -EOF -kfutil import --metadata --file metadata.json -``` - -### Authentication -Authentication to the Command platform is done using basic authentication. The credentials must be provided as a Kubernetes `kubernetes.io/basic-auth` secret. These credentials should be for a user with "Certificate Enrollment: Enroll CSR" and "API: Read" permissions in Command. -If the Helm chart was deployed with the `--set "secretConfig.useClusterRoleForSecretAccess=true"` flag, the secret must be created in the same namespace as any Issuer resources deployed. Otherwise, the secret must be created in the same namespace as the controller. - -Create a `kubernetes.io/basic-auth` secret with the Keyfactor Command username and password: -```shell -cat < - password: -EOF -``` - -If the Command server is configured to use a self-signed certificate or with a certificate signed by an untrusted root, the CA certificate must be provided as a Kubernetes secret. -```shell -kubectl -n command-issuer-system create secret generic command-ca-secret --from-file=ca.crt -``` - -### Creating Issuer and ClusterIssuer resources -The `command-issuer.keyfactor.com/v1alpha1` API version supports Issuer and ClusterIssuer resources. -The Command controller will automatically detect and process resources of both types. - -The Issuer resource is namespaced, while the ClusterIssuer resource is cluster-scoped. -For example, ClusterIssuer resources can be used to issue certificates for resources in multiple namespaces, whereas Issuer resources can only be used to issue certificates for resources in the same namespace. - -The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: -* `hostname` - The hostname of the Keyfactor Command server - The signer sets the protocol to `https` and automatically trims the trailing path from this field, if it exists. Additionally, the base Command API path is automatically set to `/KeyfactorAPI` and cannot be changed. -* `commandSecretName` - The name of the Kubernetes `kubernetes.io/basic-auth` secret containing credentials to the Keyfactor instance -* `certificateTemplate` - The short name corresponding to a template in Command that will be used to issue certificates. -* `certificateAuthorityLogicalName` - The logical name of the CA to use to sign the certificate request -* `certificateAuthorityHostname` - The CAs hostname to use to sign the certificate request -* `caSecretName` - The name of the Kubernetes secret containing the CA certificate. This field is optional and only required if the Command server is configured to use a self-signed certificate or with a certificate signed by an untrusted root. - -###### If a different combination of hostname/certificate authority/certificate profile/end entity profile is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. - -The following is an example of an Issuer resource: -```shell -cat <> command-issuer.yaml -apiVersion: command-issuer.keyfactor.com/v1alpha1 -kind: Issuer -metadata: - labels: - app.kubernetes.io/name: issuer - app.kubernetes.io/instance: issuer-sample - app.kubernetes.io/part-of: command-issuer - app.kubernetes.io/created-by: command-issuer -name: issuer-sample -spec: - hostname: "" - commandSecretName: "" - certificateTemplate: "" - certificateAuthorityLogicalName: "" - certificateAuthorityHostname: "" - caSecretName: "" -EOF -kubectl -n command-issuer-system apply -f command-issuer.yaml -``` - -###### :pushpin: Issuers can only issue certificates in the same namespace as the issuer resource. To issue certificates in multiple namespaces, use a ClusterIssuer. - -The following is an example of a ClusterIssuer resource: -```shell -cat <> command-clusterissuer.yaml -apiVersion: command-issuer.keyfactor.com/v1alpha1 -kind: ClusterIssuer -metadata: - labels: - app.kubernetes.io/name: clusterissuer - app.kubernetes.io/instance: clusterissuer-sample - app.kubernetes.io/part-of: command-issuer - app.kubernetes.io/created-by: command-issuer - name: clusterissuer-sample -spec: - hostname: "" - commandSecretName: "" - certificateTemplate: "" - certificateAuthorityLogicalName: "" - certificateAuthorityHostname: "" - caSecretName: "" -EOF -kubectl -n command-issuer-system apply -f command-clusterissuer.yaml -``` - -###### :pushpin: ClusterIssuers can issue certificates in any namespace. To issue certificates in a single namespace, use an Issuer. - -To create new resources from the above examples, replace the empty strings with the appropriate values and apply the resources to the cluster: -```shell -kubectl -n command-issuer-system apply -f issuer.yaml -kubectl -n command-issuer-system apply -f clusterissuer.yaml -``` - -### Using Issuer and ClusterIssuer resources -Once the Issuer and ClusterIssuer resources are created, they can be used to issue certificates using cert-manager. -The two most important concepts are `Certificate` and `CertificateRequest` resources. `Certificate` -resources represent a single X.509 certificate and its associated attributes, and automatically renews the certificate -and keeps it up to date. When `Certificate` resources are created, they create `CertificateRequest` resources, which -use an Issuer or ClusterIssuer to actually issue the certificate. - -###### To learn more about cert-manager, see the [cert-manager documentation](https://cert-manager.io/docs/). - -The following is an example of a Certificate resource. This resource will create a corresponding CertificateRequest resource, -and will use the `issuer-sample` Issuer resource to issue the certificate. Once issued, the certificate will be stored in a -Kubernetes secret named `command-certificate`. -```yaml -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: command-certificate -spec: - commonName: command-issuer-sample - secretName: command-certificate - issuerRef: - name: issuer-sample - group: command-issuer.keyfactor.com - kind: Issuer -``` - -###### :pushpin: Certificate resources support many more fields than the above example. See the [Certificate resource documentation](https://cert-manager.io/docs/usage/certificate/) for more information. - -###### :pushpin: Since this certificate request called `command-certificate` is configured to use `issuer-sample`, it must be deployed in the same namespace as `issuer-sample`. - -Similarly, a CertificateRequest resource can be created directly. The following is an example of a CertificateRequest resource. -```yaml -apiVersion: cert-manager.io/v1 -kind: CertificateRequest -metadata: - name: command-certificate -spec: - request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ2REQ0NBVndDQVFBd0x6RUxNQWtHQTFVRUN4TUNTVlF4SURBZUJnTlZCQU1NRjJWcVltTmhYM1JsY25KaApabTl5YlY5MFpYTjBZV05qTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4blNSCklqZDZSN2NYdUNWRHZscXlFcUhKalhIazljN21pNTdFY3A1RXVnblBXa0YwTHBVc25PMld6WTE1bjV2MHBTdXMKMnpYSURhS3NtZU9ZQzlNOWtyRjFvOGZBelEreHJJWk5SWmg0cUZXRmpyNFV3a0EySTdUb05veitET2lWZzJkUgo1cnNmaFdHMmwrOVNPT3VscUJFcWVEcVROaWxyNS85OVpaemlBTnlnL2RiQXJibWRQQ1o5OGhQLzU0NDZhci9NCjdSd2ludjVCMnNRcWM0VFZwTTh3Nm5uUHJaQXA3RG16SktZbzVOQ3JyTmw4elhIRGEzc3hIQncrTU9DQUw0T00KTkJuZHpHSm5KenVyS0c3RU5UT3FjRlZ6Z3liamZLMktyMXRLS3pyVW5keTF1bTlmTWtWMEZCQnZ0SGt1ZG0xdwpMUzRleW1CemVtakZXQi9yRVFJREFRQUJvQUF3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUJhdFpIVTdOczg2Cmgxc1h0d0tsSi95MG1peG1vRWJhUTNRYXAzQXVFQ2x1U09mdjFDZXBQZjF1N2dydEp5ZGRha1NLeUlNMVNzazAKcWNER2NncUsxVVZDR21vRkp2REZEaEUxMkVnM0ZBQ056UytFNFBoSko1N0JBSkxWNGZaeEpZQ3JyRDUxWnk3NgpPd01ORGRYTEVib0w0T3oxV3k5ZHQ3bngyd3IwWTNZVjAyL2c0dlBwaDVzTHl0NVZOWVd6eXJTMzJYckJwUWhPCnhGMmNNUkVEMUlaRHhuMjR2ZEtINjMzSFo1QXd0YzRYamdYQ3N5VW5mVUE0ZjR1cHBEZWJWYmxlRFlyTW1iUlcKWW1NTzdLTjlPb0MyZ1lVVVpZUVltdHlKZTJkYXlZSHVyUUlpK0ZsUU5zZjhna1hYeG45V2drTnV4ZTY3U0x5dApVNHF4amE4OCs1ST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0t - issuerRef: - name: issuer-sample - group: command-issuer.keyfactor.com - kind: Issuer -``` - -### Approving Certificate Requests -Unless the cert-manager internal approver automatically approves the request, newly created CertificateRequest resources -will be in a `Pending` state until they are approved. CertificateRequest resources can be approved manually by using -[cmctl](https://cert-manager.io/docs/reference/cmctl/#approve-and-deny-certificaterequests). The following is an example -of approving a CertificateRequest resource named `command-certificate` in the `command-issuer-system` namespace. -```shell -cmctl -n command-issuer-system approve ejbca-certificate -``` - -Once a certificate request has been approved, the certificate will be issued and stored in the secret specified in the -CertificateRequest resource. The following is an example of retrieving the certificate from the secret. -```shell -kubectl get secret command-certificate -n command-issuer-system -o jsonpath='{.data.tls\.crt}' | base64 -d -``` - -###### To learn more about certificate approval and RBAC configuration, see the [cert-manager documentation](https://cert-manager.io/docs/concepts/certificaterequest/#approval). - -###### :pushpin: If the certificate was issued successfully, the Approved and Ready field will both be set to `True`. - -Next, see the [example usage](example.markdown) documentation for a complete example of using the Command Issuer for cert-manager. \ No newline at end of file diff --git a/docs/example.markdown b/docs/example.markdown deleted file mode 100644 index 63cacf5..0000000 --- a/docs/example.markdown +++ /dev/null @@ -1,189 +0,0 @@ - - Terraform logo - - -# Demo ClusterIssuer Usage with K8s Ingress - -[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) - -### Documentation Tree -* [Installation](install.markdown) -* [Usage](config_usage.markdown) -* [Customization](annotations.markdown) -* [Testing the Source](testing.markdown) - -This demo will show how to use a ClusterIssuer to issue a certificate for an Ingress resource. The demo uses the Kubernetes -`ingress-nginx` Ingress controller. If Minikube is being used, run the following command to enable the controller. -```shell -minikube addons enable ingress -kubectl get pods -n ingress-nginx -``` - -To manually deploy `ingress-nginx`, run the following command: -```shell -kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.0/deploy/static/provider/cloud/deploy.yaml -``` - -Create a namespace for the demo: -```shell -kubectl create ns command-clusterissuer-demo -``` - -Deploy two Pods running the `hashicorp/http-echo` image: -```shell -cat < -``` - -Validate that the certificate was created: -```shell -kubectl -n command-clusterissuer-demo describe ingress command-ingress-demo -``` - -Test it out -```shell -curl -k https://localhost/apple -curl -k https://localhost/banana -``` - -Clean up -```shell -kubectl -n command-clusterissuer-demo delete ingress command-ingress-demo -kubectl -n command-clusterissuer-demo delete service apple-service banana-service -kubectl -n command-clusterissuer-demo delete pod apple-app banana-app -kubectl delete ns command-clusterissuer-demo -kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.0/deploy/static/provider/cloud/deploy.yaml -``` - -## Cleanup -To list the certificates and certificate requests created, run the following commands: -```shell -kubectl get certificates -n command-issuer-system -kubectl get certificaterequests -n command-issuer-system -``` - -To remove the certificate and certificate request resources, run the following commands: -```shell -kubectl delete certificate command-certificate -n command-issuer-system -kubectl delete certificaterequest command-certificate -n command-issuer-system -``` - -To list the issuer and cluster issuer resources created, run the following commands: -```shell -kubectl -n command-issuer-system get issuers.command-issuer.keyfactor.com -kubectl -n command-issuer-system get clusterissuers.command-issuer.keyfactor.com -``` - -To remove the issuer and cluster issuer resources, run the following commands: -```shell -kubectl -n command-issuer-system delete issuers.command-issuer.keyfactor.com -kubectl -n command-issuer-system delete clusterissuers.command-issuer.keyfactor.com -``` - -To remove the controller from the cluster, run: -```shell -make undeploy -``` - -To remove the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor Command, run: -```shell -make uninstall -``` \ No newline at end of file diff --git a/docs/install.markdown b/docs/install.markdown deleted file mode 100644 index 9fc4303..0000000 --- a/docs/install.markdown +++ /dev/null @@ -1,128 +0,0 @@ - - Terraform logo - - -# Installing the Keyfactor Command Issuer for cert-manager - -[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) - -### Documentation Tree -* [Usage](config_usage.markdown) -* [Example Usage](example.markdown) -* [Customization](annotations.markdown) -* [Testing the Source](testing.markdown) - -### Requirements -* [Git](https://git-scm.com/) -* [Make](https://www.gnu.org/software/make/) -* [Docker](https://docs.docker.com/engine/install/) >= v20.10.0 -* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) >= v1.11.3 -* Kubernetes >= v1.19 - * [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), or [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) -* [Keyfactor Command](https://www.keyfactor.com/products/command/) >= v10.1.0 -* [cert-manager](https://cert-manager.io/docs/installation/) >= v1.11.0 -* [cmctl](https://cert-manager.io/docs/reference/cmctl/) - -Before starting, ensure that all of the above requirements are met, and that Keyfactor Command is properly configured according to the [product docs](https://software.keyfactor.com/Content/MasterTopics/Home.htm). Additionally, verify that at least one Kubernetes node is running by running the following command: - -```shell -kubectl get nodes -``` - -A static installation of cert-manager can be installed with the following command: - -```shell -kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml -``` - -###### :pushpin: Running the static cert-manager configuration is not recommended for production use. For more information, see [Installing cert-manager](https://cert-manager.io/docs/installation/). - -### Building the Container Image - -The cert-manager external issuer for Keyfactor Command is distributed as source code, and the container must be built manually. The container image can be built using the following command: -```shell -make docker-build DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/command-cert-manager-issuer VERSION= -``` - -###### :pushpin: The container image can be built using Docker Buildx by running `make docker-buildx`. This will build the image for all supported platforms. - -To push the container image to a container registry, run the following command: -```shell -docker login -make docker-push DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/command-cert-manager-issuer VERSION= -``` - -### Installation from Manifests - -The cert-manager external issuer for Keyfactor Command can be installed using the manifests in the `config/` directory. - -1. Install the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor Command: - - ```shell - make install - ``` - -2. Finally, deploy the controller to the cluster: - - ```shell - make deploy DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/command-cert-manager-issuer VERSION= - ``` - -### Installation from Helm Chart - -The cert-manager external issuer for Keyfactor Command can also be installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](https://keyfactor.github.io/command-cert-manager-issuer/). - -1. Add the Helm repository: - - ```shell - helm repo add command-issuer https://keyfactor.github.io/command-cert-manager-issuer - helm repo update - ``` - -2. Then, install the chart: - - ```shell - helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ - --namespace command-issuer-system \ - --create-namespace \ - --set image.repository=/keyfactor/command-cert-manager-issuer \ - --set image.tag= \ - --set crd.create=true \ - # --set image.pullPolicy=Never # Only required if using a local image - ``` - - 1. Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `secretConfig.useClusterRoleForSecretAccess` to configure the chart to use a cluster role for secret access, run the following command: - - ```shell - helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ - --namespace command-issuer-system \ - --create-namespace \ - --set image.repository=/keyfactor/command-cert-manager-issuer \ - --set image.tag= \ - --set crd.create=true \ - --set secretConfig.useClusterRoleForSecretAccess=true - ``` - - 2. Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the `secretConfig.useClusterRoleForSecretAccess` value to configure the chart to use a cluster role for secret access, modify the `secretConfig.useClusterRoleForSecretAccess` value in the `values.yaml` file by creating an override file: - - ```yaml - cat < override.yaml - image: - repository: /keyfactor/command-cert-manager-issuer - pullPolicy: Never - tag: "" - secretConfig: - useClusterRoleForSecretAccess: true - EOF - ``` - - Then, use the `-f` flag to specify the `values.yaml` file: - - ```shell - helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ - --namespace command-issuer-system \ - -f override.yaml - ``` - -Next, complete the [Usage](config_usage.markdown) steps to configure the cert-manager external issuer for Keyfactor Command. diff --git a/docs/testing.markdown b/docs/testing.markdown deleted file mode 100644 index e633da2..0000000 --- a/docs/testing.markdown +++ /dev/null @@ -1,32 +0,0 @@ - - Terraform logo - - -# Testing the Controller Source Code - -[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) - - -### Documentation Tree -* [Installation](install.markdown) -* [Usage](config_usage.markdown) -* [Example Usage](example.markdown) -* [Customization](annotations.markdown) - -The test cases for the controller require a set of environment variables to be set. These variables are used to -authenticate to the Command server and to enroll a certificate. The test cases are run using the `make test` command. - -The following environment variables must be exported before testing the controller: -* `COMMAND_HOSTNAME` - The hostname of the Command server to use for testing. -* `COMMAND_USERNAME` - The username of an authorized Command user to use for testing. -* `COMMAND_PASSWORD` - The password of the authorized Command user to use for testing. -* `COMMAND_CERTIFICATE_TEMPLATE` - The name of the certificate template to use for testing. -* `COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME` - The logical name of the certificate authority to use for testing. -* `COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME` - The hostname of the certificate authority to use for testing. -* `COMMAND_CA_CERT_PATH` - A relative or absolute path to the CA certificate that the Command server uses for TLS. The file must include the certificate in PEM format. - -To build the cert-manager external issuer for Keyfactor Command, run: -```shell -make test -``` \ No newline at end of file diff --git a/docsource/overview.md b/docsource/overview.md new file mode 100644 index 0000000..cd65b4e --- /dev/null +++ b/docsource/overview.md @@ -0,0 +1,364 @@ +# Overview + +The Command Issuer for [cert-manager](https://cert-manager.io/) is a [CertificateRequest](https://cert-manager.io/docs/usage/certificaterequest/) controller that issues certificates using [Keyfactor Command](https://www.keyfactor.com/products/command/). + +# Requirements + +Before starting, ensure that the following requirements are met: + +- [Keyfactor Command](https://www.keyfactor.com/products/command/) >= 10.5 + - Command must be properly configured according to the [product docs](https://software.keyfactor.com/Core-OnPrem/Current/Content/MasterTopics/Portal.htm). + - You have access to the Command REST API. The following endpoints must be available: + - `/Status/Endpoints` + - `/Enrollment/CSR` + - `/MetadataFields` +- Kubernetes >= v1.19 + - [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/), etc. + > You must have permission to create [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) in your Kubernetes cluster. +- [Supported cert-manager release](https://cert-manager.io/docs/releases/) installed in your cluster. Please see the [cert-manager installation](https://cert-manager.io/docs/installation/) for details. +- [Supported version of Helm](https://helm.sh/docs/topics/version_skew/) for your Kubernetes version + +# Badges + +Latest Release +Go Report Card +License Apache 2.0 + +# Getting Started + +## Configuring Command + +Command Issuer enrolls certificates by submitting a POST request to the CSR Enrollment endpoint. Before using Command Issuer, you must create or identify a Certificate Authority _and_ Certificate Template suitable for your usecase. Additionally, you should ensure that the identity used by the Issuer/ClusterIssuer has the appropriate permissions in Command. + +1. **Create or identify a Certificate Authority** + + A certificate authority (CA) is an entity that issues digital certificates. Within Keyfactor Command, a CA may be a Microsoft CA, EJBCA, or a Keyfactor gateway to a cloud-based or remote CA. + + - If you haven't created a Certificate Authority before, refer to the [Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/CA-Operations.htm) to learn how, or reach out to your Keyfactor support representative. + + The CA that you choose must be configured to allow CSR Enrollment. + +2. **Identify a Certificate Template** + + Certificate Templates in Command define properties and constraints of the certificates being issued. This includes settings like key usage, extended key usage, validity period, allowed key algorithms, and signature algorithms. They also control the type of information that end entities must provide and how that information is validated before issuing certificates. + + - If you don't have any suitable Certificate Templates, refer to the [Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm?Highlight=Certificate%20Template) or reach out to your Keyfactor support representative to learn more. + + The Certificate Template that you shoose must be configured to allow CSR Enrollment. + + You should make careful note of the allowed Key Types and Key Sizes on the Certificate Template. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the key `algorithm` and `size` are allowed by your Certificate Template in Command. + + The same goes for **Subject DN Attributes** and **Other Subject Attributes** allowed by your Certificate Template. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the `subject`, `commonName`, `dnsNames`, etc. are allowed and/or configured correctly by your Certificate Template in Command. + +3. **Configure Command Security Roles and Claims** + + In Command, Security Roles define groups of users or administrators with specific permissions. Users and subjects are identified by Claims. By adding a Claim to a Security Role, you can dictate what actions the user or subject can perform and what parts of the system it can interact with. + + - If you haven't created Roles and Access rules before, [this guide](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) provides a primer on these concepts in Command. + + If your security policy requires fine-grain access control, Command Issuer requires the following Access Rules. + + | Global Permissions | + |-----------------------------------------| + | `CertificateMetadataTypes:Read` | + | `CertificateEnrollment:EnrollCSR` | + +## Installing Command Issuer + +Command Issuer is installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](https://keyfactor.github.io/command-cert-manager-issuer/). + +1. Verify that at least one Kubernetes node is running + + ```shell + kubectl get nodes + ``` + +2. Add the Helm repository: + + ```shell + helm repo add command-issuer https://keyfactor.github.io/command-cert-manager-issuer + helm repo update + ``` + +3. Then, install the chart: + + ```shell + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace command-issuer-system \ + --create-namespace + ``` + +> The Helm chart installs the Command Issuer CRDs by default. The CRDs can be installed manually with the `make install` target. + +# Authentication + +Command Issuer supports authentication to Command using one of the following methods: + +- Basic Authentication (username and password) +- OAuth 2.0 "client credentials" token flow (sometimes called two-legged OAuth 2.0) + +These credentials must be configured using a Kubernetes Secret. By default, the secret is expected to exist in the same namespace as the issuer controller (`command-issuer-system` by default). + +> Command Issuer can read secrets in the Issuer namespace if `--set "secretConfig.useClusterRoleForSecretAccess=true"` flag is set when installing the Helm chart. + +Command Issuer also supports ambient authentication, where a token is fetched from an Authorization Server using a cloud provider's auth infrastructure and passed to Command directly. + +- Azure Workload Identity (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) + +## Basic Auth + +Create a `kubernetes.io/basic-auth` secret with the Keyfactor Command username and password: +```shell +cat < + password: +EOF +``` + +## OAuth + +Create an Opaque secret containing the client ID and client secret to authenticate with Command: + +```shell +token_url="" +client_id="" +client_secret="" +audience="" +scopes="" # comma separated list of scopes + +kubectl -n command-issuer-system create secret generic command-secret \ + "--from-literal=tokenUrl=$token_url" \ + "--from-literal=clientId=$client_id" \ + "--from-literal=clientSecret=$client_secret" \ + "--from-literal=audience=$audience" \ + "--from-literal=scopes=$scopes" +``` + +> Audience and Scopes are optional + +# CA Bundle + +If the Command API is configured to use a self-signed certificate or with a certificate whose issuer isn't widely trusted, the CA certificate must be provided as a Kubernetes secret. + +```shell +kubectl -n command-issuer-system create secret generic command-ca-secret --from-file=ca.crt +``` + +# Creating Issuer and ClusterIssuer resources + +The `command-issuer.keyfactor.com/v1alpha1` API version supports Issuer and ClusterIssuer resources. The Issuer resource is namespaced, while the ClusterIssuer resource is cluster-scoped. + +For example, ClusterIssuer resources can be used to issue certificates for resources in multiple namespaces, whereas Issuer resources can only be used to issue certificates for resources in the same namespace. + +1. **Prepare the `spec`** + + ```shell + export HOSTNAME="" + export COMMAND_CA_HOSTNAME="" # Only required for non-HTTPS CA types + export COMMAND_CA_LOGICAL_NAME="" + export CERTIFICATE_TEMPLATE_SHORT_NAME="" + ``` + + The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: + | Field Name | Description | + |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| + | hostname | The hostname of the Command API Server. | + | apiPath | (optional) The base path of the Command REST API. Defaults to `KeyfactorAPI`. | + | commandSecretName | The name of the Kubernetes secret containing basic auth credentials or OAuth 2.0 credentials | + | caSecretName | (optional) The name of the Kubernetes secret containing the CA certificate. Required if the Command API uses a self-signed certificate or it was signed by a CA that is not widely trusted. | + | certificateAuthorityLogicalName | The logical name of the Certificate Authority to use in Command. For example, `Sub-CA` | + | certificateAuthorityHostname | (optional) The hostname of the Certificate Authority specified by `certificateAuthorityLogicalName`. This field is usually only required if the CA in Command is a DCOM (MSCA-like) CA. | + | certificateTemplate | The Short Name of the Certificate Template to use when this Issuer/ClusterIssuer enrolls CSRs. | + + > If a different combination of hostname/certificate authority/certificate template is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. + +2. **Create an Issuer or ClusterIssuer** + + - **Issuer** + + Create an Issuer resource using the environment variables prepared in step 1. + + ```yaml + cat < ./issuer.yaml + apiVersion: command-issuer.keyfactor.com/v1alpha1 + kind: Issuer + metadata: + name: issuer-sample + namespace: default + spec: + hostname: "$HOSTNAME" + apiPath: "/KeyfactorAPI" # Preceding & trailing slashes are handled automatically + commandSecretName: "command-secret" # references the secret created above + caSecretName: "command-ca-secret" # references the secret created above + + # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required + certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" + certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" + EOF + + kubectl -n default apply -f issuer.yaml + ``` + + - **ClusterIssuer** + + Create a ClusterIssuer resource using the environment variables prepared in step 1. + + ```yaml + cat < ./clusterissuer.yaml + apiVersion: command-issuer.keyfactor.com/v1alpha1 + kind: ClusterIssuer + metadata: + name: clusterissuer-sample + spec: + hostname: "$HOSTNAME" + apiPath: "/KeyfactorAPI" # Preceding & trailing slashes are handled automatically + commandSecretName: "command-secret" # references the secret created above + caSecretName: "command-ca-secret" # references the secret created above + + # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required + certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" + certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" + EOF + + kubectl apply -f clusterissuer.yaml + ``` + +> **Overriding the `spec` using Kubernetes Annotations on CertificateRequest Resources** +> +> The +> +>
Notes +> The GoDaddy AnyCA Gateway REST plugin requires several custom enrollment parameters that are passed to GoDaddy upon the submission of a new PFX/CSR enrollment request. These custom enrollment parameters configure the domain/organization/extended validation procedure required to complete the certificate enrollment. +> +> Prior to Command v12.3, custom enrollment parameters are not supported on a per-request basis for PFX/CSR Enrollment. If your Keyfactor Command version is less than v12.3, the only way to configure custom enrollment parameters is to set default parameter values on the Certificate Template in the Keyfactor AnyCA Gateway REST. +> +> Before continuing with installation prior to Command 12.3, users should consider the following: +> +> * Each combination of custom enrollment parameters will require the creation of a new Certificate Template and Certificate Profile in the Keyfactor AnyCA Gateway REST. +> * If you have multiple combinations of custom enrollment parameters, consider the operational complexity of managing multiple Certificate Templates and Certificate Profiles. +> * If your certificate workflows mostly consist of certificate renewal, re-issuance, and revocation, the GoDaddy AnyCA Gateway REST plugin is fully supported. +>
+ +# Creating a Certificate + +Once an Issuer or ClusterIssuer resource is created, they can be used to issue certificates using cert-manager. The two most important concepts are `Certificate` and `CertificateRequest` resources. + +1. `Certificate` resources represent a single X.509 certificate and its associated attributes. cert-manager maintains the corresponding certificate, including renewal when appropriate. +2. When `Certificate` resources are created, cert-manager creates a corresponding `CertificateRequest` that targets a specific Issuer or ClusterIssuer to actually issue the certificate. + +> To learn more about cert-manager, see the [cert-manager documentation](https://cert-manager.io/docs/). + +The following is an example of a Certificate resource. This resource will create a corresponding CertificateRequest resource, and will use the `issuer-sample` Issuer resource to issue the certificate. Once issued, the certificate will be stored in a Kubernetes secret named `command-certificate`. + +```yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: command-certificate +spec: + issuerRef: + name: issuer-sample + group: command-issuer.keyfactor.com + kind: Issuer + commonName: example.com + secretName: command-certificate +``` + +> Certificate resources support many more fields than the above example. See the [Certificate resource documentation](https://cert-manager.io/docs/usage/certificate/) for more information. + +Similarly, a CertificateRequest resource can be created directly. The following is an example of a CertificateRequest resource. +```yaml +apiVersion: cert-manager.io/v1 +kind: CertificateRequest +metadata: + name: command-certificate +spec: + issuerRef: + name: issuer-sample + group: command-issuer.keyfactor.com + kind: Issuer + request: +``` + +> All fields in Command Issuer and ClusterIssuer `spec` can be overridden by applying Kubernetes Annotations to Certificates _and_ CertificateRequests. See [runtime customization for more](docs/annotations.md) + +## Approving Certificate Requests + +Unless the cert-manager internal approver automatically approves the request, newly created CertificateRequest resources +will be in a `Pending` state until they are approved. CertificateRequest resources can be approved manually by using +[cmctl](https://cert-manager.io/docs/reference/cmctl/#approve-and-deny-certificaterequests). The following is an example +of approving a CertificateRequest resource named `command-certificate`. +```shell +cmctl approve command-certificate +``` + +Once a certificate request has been approved, the certificate will be issued and stored in the secret specified in the +CertificateRequest resource. The following is an example of retrieving the certificate from the secret. +```shell +kubectl get secret command-certificate -o jsonpath='{.data.tls\.crt}' | base64 -d +``` + +> To learn more about certificate approval and RBAC configuration, see the [cert-manager documentation](https://cert-manager.io/docs/concepts/certificaterequest/#approval). + +## Overriding the Issuer/ClusterIssuer `spec` using Kubernetes Annotations on CertificateRequest Resources + +Command Issuer allows you to override the `certificateAuthorityHostname`, `certificateAuthorityLogicalName`, and `certificateTemplate` by setting Kubernetes Annotations on CertificateRequest resources. This may be useful if certain enrollment scenarios require a different Certificate Authority or Certificate Template, but you don't want to create a new Issuer/ClusterIssuer. + +- `command-issuer.keyfactor.com/certificateAuthorityHostname` overrides `certificateAuthorityHostname` +- `command-issuer.keyfactor.com/certificateAuthorityLogicalName` overrides `certificateAuthorityLogicalName` +- `command-issuer.keyfactor.com/certificateTemplate` overrides `certificateTemplate` + +> cert-manager copies Annotations set on Certificate resources to the corresponding CertificateRequest. + +> **How to Apply Annotations** +>
Notes +> +> To apply these annotations, include them in the metadata section of your Certificate/CertificateRequest resource: +> +> ```yaml +> apiVersion: cert-manager.io/v1 +> kind: Certificate +> metadata: +> annotations: +> command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" +> command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" +> metadata.command-issuer.keyfactor.com/ResponsibleTeam: "theResponsibleTeam@example.com" +> # ... other annotations +> spec: +> # ... the rest of the spec +> ``` +>
+ +# Certificate Metadata + +Keyfactor Command allows users to [attach custom metadata to certificates](https://software.keyfactor.com/Core/Current/Content/ReferenceGuide/Certificate%20Metadata.htm) that can be used to tag certificates with additional information. Command Issuer can attach Certificate Metadata upon enrollment. + +- **Pre-defined Certificate Metadata** + + If **all of the following metadata fields are defined** in Command, Command Issuer will populate the fields upon certificate enrollment. All of the metadata fields are String types. Please refer to the [Command docs](https://software.keyfactor.com/Core/Current/Content/ReferenceGuide/Certificate%20Metadata.htm) to define these metadata fields in Command. + + | Field Name | Description | + |-------------------------------------|-----------------------------------------------------------------------------------------------------------------| + | Issuer-Namespace | The namespace that the Issuer resource was created in. Is always empty for ClusterIssuers. | + | Controller-Reconcile-Id | The GUID of the reconciliation run that corresponded to the issuance of this certificate. | + | Certificate-Signing-Request-Namespace | The namespace that the CertificateRequest resource was created in. | + | Controller-Namespace | The namespace that the controller container is running in. | + | Controller-Kind | The issuer type - Issuer or ClusterIssuer. | + | Controller-Resource-Group-Name | The group name of the Command Issuer CRD. Is always `command-issuer.keyfactor.com`. | + | Issuer-Name | The name of the K8s Issuer/ClusterIssuer resource. | + + > You don't need to re-create the Issuer/ClusterIssuer when metadata fields are added/removed in Command. Command Issuer automatically detects the presence of these fields and tracks the state in the `SupportsMetadata` resource condition. + +- **Custom Certificate Metadata** + + You can **_also_** configure Command Issuer to attach Certificate Metadata by annotating Certificate/CertificateRequest resources. Command Issuer does not check for the presence of custom metadata fields configured in Annotations, and you should take special care that fields defined in annotations exist in Command prior to use. Certificate issuance will fail if any of the metadata fields specified aren't configured in Command. The syntax for specifying metadata is as follows: + + ```yaml + metadata.command-issuer.keyfactor.com/: + ``` diff --git a/go.mod b/go.mod index 3d54f23..dee7558 100644 --- a/go.mod +++ b/go.mod @@ -1,82 +1,100 @@ -module github.com/Keyfactor/command-issuer +module github.com/Keyfactor/command-cert-manager-issuer -go 1.19 +go 1.23.4 require ( - github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 - github.com/cert-manager/cert-manager v1.11.0 - github.com/go-logr/logr v1.2.4 - github.com/onsi/ginkgo/v2 v2.6.1 - github.com/onsi/gomega v1.24.2 - github.com/stretchr/testify v1.8.2 - k8s.io/api v0.26.3 - k8s.io/apimachinery v0.26.3 - k8s.io/client-go v0.26.3 - k8s.io/klog/v2 v2.90.1 - k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 - sigs.k8s.io/controller-runtime v0.14.6 + github.com/Keyfactor/keyfactor-auth-client-go v1.1.2-rc.0 + github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0-rc.12 + github.com/cert-manager/cert-manager v1.16.2 + github.com/go-logr/logr v1.4.2 + github.com/stretchr/testify v1.10.0 + golang.org/x/oauth2 v0.24.0 + k8s.io/api v0.31.1 + k8s.io/apimachinery v0.31.1 + k8s.io/client-go v0.31.1 + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 + sigs.k8s.io/controller-runtime v0.19.0 ) require ( - github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect + cloud.google.com/go/compute/metadata v0.5.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.10.2 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-ldap/ldap/v3 v3.4.4 // indirect - github.com/go-logr/zapr v1.2.3 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.6 // indirect + github.com/go-ldap/ldap/v3 v3.4.8 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/imdario/mergo v0.3.15 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spbsoluble/go-pkcs12 v0.3.3 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - go.uber.org/atomic v1.10.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.mozilla.org/pkcs7 v0.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/oauth2 v0.6.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/time v0.3.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.6.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.26.3 // indirect - k8s.io/component-base v0.26.3 // indirect - k8s.io/kube-aggregator v0.26.3 // indirect - k8s.io/kube-openapi v0.0.0-20230327201221-f5883ff37f0c // indirect - sigs.k8s.io/gateway-api v0.6.2 // indirect + k8s.io/apiextensions-apiserver v0.31.1 // indirect + k8s.io/component-base v0.31.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect + sigs.k8s.io/gateway-api v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index f77f643..f65cd69 100644 --- a/go.sum +++ b/go.sum @@ -1,128 +1,150 @@ -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= -github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= -github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 h1:caLlzFCz2L4Dth/9wh+VlypFATmOMmCSQkCPKOKMxw8= -github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= +cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Keyfactor/keyfactor-auth-client-go v1.1.2-rc.0 h1:z4TfQErC+YLPujwHPNeAkK2bl6O5hd7m1mve+qGh2Ko= +github.com/Keyfactor/keyfactor-auth-client-go v1.1.2-rc.0/go.mod h1:yw92P9gSYVEyWkiUAJFsb7hjhXa8slN1+yTQgjSgovM= +github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0-rc.12 h1:L/IXsbVR+cGW8ACQuA8a3nebux2sLQ4rpCGvFF4sIfg= +github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0-rc.12/go.mod h1:BiX76zEZTgRaUPDiRjnUWKtpEPQlSuko6XKBpBZxmX8= +github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0 h1:yMChWRnnxmcgLt6kEQ3FZfteps05v/qot5KXLXxa6so= +github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0/go.mod h1:HWb+S60YAALFVSfB8QuQ8ugjsjr+FHLQET0/4K7EVWw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cert-manager/cert-manager v1.11.0 h1:sChJmoj9hhWuFkQMDYHnLHgYA/sSVil+hY+A1lnD3jY= -github.com/cert-manager/cert-manager v1.11.0/go.mod h1:JCy2jvRi3Kp+qnRfw8TVYkOocj1thw/aDWFEHPpv4Q4= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cert-manager/cert-manager v1.16.2 h1:c9UU2E+8XWGruyvC/mdpc1wuLddtgmNr8foKdP7a8Jg= +github.com/cert-manager/cert-manager v1.16.2/go.mod h1:MfLVTL45hFZsqmaT1O0+b2ugaNNQQZttSFV9hASHUb0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= -github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -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.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -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 v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= -github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= -github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.6 h1:CYsqysemXfEaQbyrLJmdsCRuufHoLa3P/gGWGl5TDrM= +github.com/go-asn1-ber/asn1-ber v1.5.6/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= +github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/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-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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/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.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -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/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= -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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= +github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 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= @@ -130,213 +152,188 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.6.1 h1:1xQPCjcqYw/J5LchOcp4/2q/jzJFjiAOc25chhnDw+Q= -github.com/onsi/ginkgo/v2 v2.6.1/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= -github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE= -github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= +github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= +go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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/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-20190108225652-1e06a53dbb7e/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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -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/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210330210617-4fbd30eecc44/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/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-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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= -gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -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.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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -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-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -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.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -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.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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.26.3 h1:emf74GIQMTik01Aum9dPP0gAypL8JTLl/lHa4V9RFSU= -k8s.io/api v0.26.3/go.mod h1:PXsqwPMXBSBcL1lJ9CYDKy7kIReUydukS5JiRlxC3qE= -k8s.io/apiextensions-apiserver v0.26.3 h1:5PGMm3oEzdB1W/FTMgGIDmm100vn7IaUP5er36dB+YE= -k8s.io/apiextensions-apiserver v0.26.3/go.mod h1:jdA5MdjNWGP+njw1EKMZc64xAT5fIhN6VJrElV3sfpQ= -k8s.io/apimachinery v0.26.3 h1:dQx6PNETJ7nODU3XPtrwkfuubs6w7sX0M8n61zHIV/k= -k8s.io/apimachinery v0.26.3/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= -k8s.io/client-go v0.26.3 h1:k1UY+KXfkxV2ScEL3gilKcF7761xkYsSD6BC9szIu8s= -k8s.io/client-go v0.26.3/go.mod h1:ZPNu9lm8/dbRIPAgteN30RSXea6vrCpFvq+MateTUuQ= -k8s.io/component-base v0.26.3 h1:oC0WMK/ggcbGDTkdcqefI4wIZRYdK3JySx9/HADpV0g= -k8s.io/component-base v0.26.3/go.mod h1:5kj1kZYwSC6ZstHJN7oHBqcJC6yyn41eR+Sqa/mQc8E= -k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= -k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-aggregator v0.26.3 h1:nc4H5ymGkWPU3c9U9UM468JcmNENY/s/mDYVW3t3uRo= -k8s.io/kube-aggregator v0.26.3/go.mod h1:SgBESB/+PfZAyceTPIanfQ7GtX9G/+mjfUbTHg3Twbo= -k8s.io/kube-openapi v0.0.0-20230327201221-f5883ff37f0c h1:EFfsozyzZ/pggw5qNx7ftTVZdp7WZl+3ih89GEjYEK8= -k8s.io/kube-openapi v0.0.0-20230327201221-f5883ff37f0c/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= -k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 h1:xMMXJlJbsU8w3V5N2FLDQ8YgU8s1EoULdbQBcAeNJkY= -k8s.io/utils v0.0.0-20230313181309-38a27ef9d749/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.14.6 h1:oxstGVvXGNnMvY7TAESYk+lzr6S3V5VFxQ6d92KcwQA= -sigs.k8s.io/controller-runtime v0.14.6/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= -sigs.k8s.io/gateway-api v0.6.2 h1:583XHiX2M2bKEA0SAdkoxL1nY73W1+/M+IAm8LJvbEA= -sigs.k8s.io/gateway-api v0.6.2/go.mod h1:EYJT+jlPWTeNskjV0JTki/03WX1cyAnBhwBJfYHpV/0= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= +k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= +k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= +sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/integration-manifest.json b/integration-manifest.json index 90778f1..53b7775 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,8 +1,10 @@ { - "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json", "integration_type": "api-client", "name": "command-cert-manager-issuer", - "status": "pilot", - "link_github":false, - "description": "cert-manager external issuer for the Keyfactor Command platform" + "status": "production", + "link_github": false, + "update_catalog": false, + "support_level": "community", + "description": "cert-manager external issuer for the Keyfactor Command platform" } diff --git a/internal/command/client.go b/internal/command/client.go new file mode 100644 index 0000000..5e52010 --- /dev/null +++ b/internal/command/client.go @@ -0,0 +1,165 @@ +/* +Copyright © 2024 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + commandsdk "github.com/Keyfactor/keyfactor-go-client/v3/api" + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var ( + tokenCredentialSource TokenCredentialSource +) + +func getAmbientTokenCredentialSource() TokenCredentialSource { + return tokenCredentialSource +} + +func setAmbientTokenCredentialSource(source TokenCredentialSource) { + tokenCredentialSource = source +} + +type Client interface { + EnrollCSR(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) + GetAllMetadataFields() ([]commandsdk.MetadataField, error) + TestConnection() error +} + +var ( + _ Client = &clientAdapter{} +) + +type clientAdapter struct { + enrollCSR func(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) + getAllMetadataFields func() ([]commandsdk.MetadataField, error) + testConnection func() error +} + +// EnrollCSR implements CertificateClient. +func (c *clientAdapter) EnrollCSR(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) { + return c.enrollCSR(ea) +} + +// GetAllMetadataFields implements Client. +func (c *clientAdapter) GetAllMetadataFields() ([]commandsdk.MetadataField, error) { + return c.getAllMetadataFields() +} + +// TestConnection implements CertificateClient. +func (c *clientAdapter) TestConnection() error { + return c.testConnection() +} + +type TokenCredentialSource interface { + GetAccessToken(context.Context) (string, error) +} + +var ( + _ TokenCredentialSource = &azure{} +) + +type azure struct { + cred azcore.TokenCredential + scopes []string +} + +// GetAccessToken implements TokenCredential. +func (a *azure) GetAccessToken(ctx context.Context) (string, error) { + // Lazily create the credential if needed + if a.cred == nil { + c, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return "", fmt.Errorf("%w: failed to set up Azure Default Credential: %w", errTokenFetchFailure, err) + } + a.cred = c + } + + // Request a token with the provided scopes + token, err := a.cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: a.scopes, + }) + if err != nil { + return "", fmt.Errorf("%w: failed to fetch token: %w", errTokenFetchFailure, err) + } + + log.FromContext(ctx).Info("fetched token using Azure DefaultAzureCredential") + return token.Token, nil +} + +func newAzureDefaultCredentialSource(ctx context.Context, scopes []string) (*azure, error) { + source := &azure{ + scopes: scopes, + } + _, err := source.GetAccessToken(ctx) + if err != nil { + return nil, err + } + + tokenCredentialSource = source + + return source, nil +} + +var ( + _ TokenCredentialSource = &gcp{} +) + +type gcp struct { + tokenSource oauth2.TokenSource + scopes []string +} + +// GetAccessToken implements TokenCredential. +func (g *gcp) GetAccessToken(ctx context.Context) (string, error) { + // Lazily create the TokenSource if it's nil. + if g.tokenSource == nil { + credentials, err := google.FindDefaultCredentials(ctx, g.scopes...) + if err != nil { + return "", fmt.Errorf("%w: failed to find GCP ADC: %w", errTokenFetchFailure, err) + } + g.tokenSource = credentials.TokenSource + } + + // Retrieve the token from the token source. + token, err := g.tokenSource.Token() + if err != nil { + return "", fmt.Errorf("%w: failed to fetch token from GCP ADC token source: %w", errTokenFetchFailure, err) + } + + log.FromContext(ctx).Info("fetched token using GCP ApplicationDefaultCredential") + return token.AccessToken, nil +} + +func newGCPDefaultCredentialSource(ctx context.Context, scopes []string) (*gcp, error) { + source := &gcp{ + scopes: scopes, + } + _, err := source.GetAccessToken(ctx) + if err != nil { + return nil, err + } + tokenCredentialSource = source + return source, nil +} diff --git a/internal/command/command.go b/internal/command/command.go new file mode 100644 index 0000000..cee0bc4 --- /dev/null +++ b/internal/command/command.go @@ -0,0 +1,499 @@ +/* +Copyright © 2024 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "context" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "strings" + "time" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" + commandsdk "github.com/Keyfactor/keyfactor-go-client/v3/api" + cmpki "github.com/cert-manager/cert-manager/pkg/util/pki" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + // Keyfactor enrollment PEM format + enrollmentPEMFormat = "PEM" + commandMetadataAnnotationPrefix = "metadata.command-issuer.keyfactor.com/" +) + +var ( + errInvalidConfig = errors.New("invalid config") + errInvalidSignerConfig = errors.New("invalid signer config") + errInvalidCSR = errors.New("csr is invalid") + errCommandEnrollmentFailure = errors.New("command enrollment failure") + errTokenFetchFailure = errors.New("couldn't fetch bearer token") + errAmbientCredentialCreationFailure = errors.New("failed to obtain ambient credentials") +) + +type K8sMetadata struct { + ControllerNamespace string + ControllerKind string + ControllerResourceGroupName string + IssuerName string + IssuerNamespace string + ControllerReconcileId string + CertificateSigningRequestNamespace string + CertManagerCertificateName string +} + +type HealthCheckerBuilder func(context.Context, *Config) (HealthChecker, error) +type HealthChecker interface { + Check(context.Context) error + CommandSupportsMetadata() (bool, error) +} + +type SignerBuilder func(context.Context, *Config) (Signer, error) +type Signer interface { + Sign(context.Context, []byte, *SignConfig) ([]byte, []byte, error) +} + +type newCommandClientFunc func(*auth_providers.Server, *context.Context) (*commandsdk.Client, error) + +type signer struct { + client Client +} + +type Config struct { + Hostname string + APIPath string + CaCertsBytes []byte + BasicAuth *BasicAuth + OAuth *OAuth + AmbientCredentialScopes []string +} + +func (c *Config) validate() error { + if c.Hostname == "" { + return fmt.Errorf("%w: hostname is required", errInvalidConfig) + } + if c.APIPath == "" { + return fmt.Errorf("%w: apiPath is required", errInvalidConfig) + } + + // Validate the optional BasicAuth fields if BasicAuth is provided + if err := c.BasicAuth.validate(); err != nil { + return err + } + + // Validate the optional OAuth fields if OAuth is provided + if err := c.OAuth.validate(); err != nil { + return err + } + + return nil +} + +type BasicAuth struct { + Username string + Password string +} + +func (b *BasicAuth) validate() error { + if b == nil { + return nil + } + if b.Username == "" { + return fmt.Errorf("%w: username is required", errInvalidConfig) + } + if b.Password == "" { + return fmt.Errorf("%w: password is required", errInvalidConfig) + } + return nil +} + +type OAuth struct { + TokenURL string + ClientID string + ClientSecret string + Scopes []string + Audience string +} + +func (o *OAuth) validate() error { + if o == nil { + return nil + } + if o.TokenURL == "" { + return fmt.Errorf("%w: tokenURL is required", errInvalidConfig) + } + if o.ClientID == "" { + return fmt.Errorf("%w: clientID is required", errInvalidConfig) + } + if o.ClientSecret == "" { + return fmt.Errorf("%w: clientSecret is required", errInvalidConfig) + } + return nil +} + +func newServerConfig(ctx context.Context, config *Config) (*auth_providers.Server, error) { + log := log.FromContext(ctx) + + if config == nil { + return nil, fmt.Errorf("%w: nil config - this is a bug", errInvalidConfig) + } + + var server *auth_providers.Server + + config.APIPath = strings.TrimLeft(config.APIPath, "/") + config.APIPath = strings.TrimRight(config.APIPath, "/") + + authConfig := auth_providers.CommandAuthConfig{} + authConfig.WithCommandHostName(config.Hostname) + authConfig.WithCommandAPIPath(config.APIPath) + authConfig.WithCommandCACert(string(config.CaCertsBytes)) + + nonAmbientCredentialsConfigured := false + + if config.BasicAuth != nil { + basicAuthConfig := auth_providers.NewBasicAuthAuthenticatorBuilder(). + WithUsername(config.BasicAuth.Username). + WithPassword(config.BasicAuth.Password) + basicAuthConfig.CommandAuthConfig = authConfig + server = basicAuthConfig.GetServerConfig() + + nonAmbientCredentialsConfigured = true + } + + if config.OAuth != nil { + oauthConfig := auth_providers.NewOAuthAuthenticatorBuilder(). + WithTokenUrl(config.OAuth.TokenURL). + WithClientId(config.OAuth.ClientID). + WithClientSecret(config.OAuth.ClientSecret) + + if len(config.OAuth.Scopes) > 0 { + oauthConfig.WithScopes(config.OAuth.Scopes) + } + if config.OAuth.Audience != "" { + oauthConfig.WithAudience(config.OAuth.Audience) + } + + oauthConfig.CommandAuthConfig = authConfig + server = oauthConfig.GetServerConfig() + + nonAmbientCredentialsConfigured = true + } + + // If direct basic-auth/OAuth credentials were configured, continue. Otherwise, + // we look for ambient credentials configured on the environment where we're running. + if !nonAmbientCredentialsConfigured { + source := getAmbientTokenCredentialSource() + if source == nil { + log.Info("no direct credentials provided; attempting to use ambient credentials. trying Azure DefaultAzureCredential first") + + var err error + source, err = newAzureDefaultCredentialSource(ctx, config.AmbientCredentialScopes) + if err != nil { + log.Info("couldn't obtain Azure DefaultAzureCredential. trying GCP ApplicationDefaultCredentials", "error", err) + + var innerErr error + source, innerErr = newGCPDefaultCredentialSource(ctx, config.AmbientCredentialScopes) + if innerErr != nil { + return nil, fmt.Errorf("%w: azure err: %w. gcp err: %w", errAmbientCredentialCreationFailure, err, innerErr) + } + } + + // Set the credential source globally + setAmbientTokenCredentialSource(source) + } + + token, err := source.GetAccessToken(ctx) + if err != nil { + return nil, err + } + + server = &auth_providers.Server{ + Host: config.Hostname, + APIPath: config.APIPath, + AccessToken: token, + AuthType: "oauth", + ClientID: "", + ClientSecret: "", + OAuthTokenUrl: "", + Scopes: nil, + Audience: "", + SkipTLSVerify: false, + CACertPath: "", + } + } + + log.Info("configuration was valid - successfully generated server config", "authMethod", server.AuthType, "hostname", server.Host, "apiPath", server.APIPath) + return server, nil +} + +type SignConfig struct { + CertificateTemplate string + CertificateAuthorityLogicalName string + CertificateAuthorityHostname string + Meta *K8sMetadata + Annotations map[string]string +} + +func (s *SignConfig) validate() error { + if s.CertificateTemplate == "" { + return errors.New("certificateTemplate is required") + } + if s.CertificateAuthorityLogicalName == "" { + return errors.New("certificateAuthorityLogicalName is required") + } + if s.CertificateAuthorityHostname == "" { + return errors.New("certificateAuthorityHostname is required") + } + return nil +} + +func newInternalSigner(ctx context.Context, config *Config, newClientFunc newCommandClientFunc) (*signer, error) { + if config == nil { + return nil, fmt.Errorf("%w: newClientFunc hook is nil - this is a bug. please report this to the Command authors", errInvalidConfig) + } + log := log.FromContext(ctx) + s := &signer{} + + err := config.validate() + if err != nil { + return nil, err + } + + serverConfig, err := newServerConfig(ctx, config) + if err != nil { + return nil, err + } + + client, err := newClientFunc(serverConfig, &ctx) + if err != nil { + return nil, fmt.Errorf("failed to create new Command API client: %w", err) + } + + adapter := &clientAdapter{ + enrollCSR: client.EnrollCSR, + getAllMetadataFields: client.GetAllMetadataFields, + testConnection: client.AuthClient.Authenticate, + } + + log.Info("successfully generated Command client") + s.client = adapter + + return s, nil +} + +func NewHealthChecker(ctx context.Context, config *Config) (HealthChecker, error) { + return newInternalSigner(ctx, config, commandsdk.NewKeyfactorClient) +} + +func NewSignerBuilder(ctx context.Context, config *Config) (Signer, error) { + return newInternalSigner(ctx, config, commandsdk.NewKeyfactorClient) +} + +// Check implements HealthChecker. +func (s *signer) Check(ctx context.Context) error { + err := s.client.TestConnection() + if err != nil { + return fmt.Errorf("failed to check status of connected Command instance: %w", err) + } + return nil +} + +// CommandSupportsMetadata implements HealthChecker. +func (s *signer) CommandSupportsMetadata() (bool, error) { + existingFields, err := s.client.GetAllMetadataFields() + if err != nil { + return false, fmt.Errorf("failed to fetch metadata fields from connected Command instance: %w", err) + } + + expectedFieldsSlice := []string{ + CommandMetaControllerNamespace, + CommandMetaControllerKind, + CommandMetaControllerResourceGroupName, + CommandMetaIssuerName, + CommandMetaIssuerNamespace, + CommandMetaControllerReconcileId, + CommandMetaCertificateSigningRequestNamespace, + } + + // Create a lookup map (set) of existing field names + existingFieldSet := make(map[string]struct{}, len(existingFields)) + for _, field := range existingFields { + existingFieldSet[field.Name] = struct{}{} + } + + // Check that every expected field is present + for _, expectedField := range expectedFieldsSlice { + if _, found := existingFieldSet[expectedField]; !found { + // As soon as one required field is missing, return false + return false, nil + } + } + + // If we've made it here, all required metadata fields are present + return true, nil +} + +const ( + CommandMetaControllerNamespace = "Controller-Namespace" + CommandMetaControllerKind = "Controller-Kind" + CommandMetaControllerResourceGroupName = "Controller-Resource-Group-Name" + CommandMetaIssuerName = "Issuer-Name" + CommandMetaIssuerNamespace = "Issuer-Namespace" + CommandMetaControllerReconcileId = "Controller-Reconcile-Id" + CommandMetaCertificateSigningRequestNamespace = "Certificate-Signing-Request-Namespace" +) + +// Sign implements Signer. +func (s *signer) Sign(ctx context.Context, csrBytes []byte, config *SignConfig) ([]byte, []byte, error) { + k8sLog := log.FromContext(ctx) + + err := config.validate() + if err != nil { + return nil, nil, err + } + + // Override defaults from annotations + if value, exists := config.Annotations["command-issuer.keyfactor.com/certificateTemplate"]; exists { + config.CertificateTemplate = value + } + if value, exists := config.Annotations["command-issuer.keyfactor.com/certificateAuthorityLogicalName"]; exists { + config.CertificateAuthorityLogicalName = value + } + if value, exists := config.Annotations["command-issuer.keyfactor.com/certificateAuthorityHostname"]; exists { + config.CertificateAuthorityHostname = value + } + + k8sLog.Info(fmt.Sprintf("Using certificate template %q and certificate authority %q (%s)", config.CertificateTemplate, config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname)) + + csr, err := parseCSR(csrBytes) + if err != nil { + k8sLog.Error(err, "failed to parse CSR") + return nil, nil, err + } + + // Log the common metadata of the CSR + k8sLog.Info(fmt.Sprintf("CSR has Common Name %q with %d DNS SANs, %d IP SANs, and %d URI SANs", csr.Subject.CommonName, len(csr.DNSNames), len(csr.IPAddresses), len(csr.URIs))) + + // Print the SANs + for _, dnsName := range csr.DNSNames { + k8sLog.Info(fmt.Sprintf("DNS SAN: %s", dnsName)) + } + + for _, ipAddress := range csr.IPAddresses { + k8sLog.Info(fmt.Sprintf("IP SAN: %s", ipAddress.String())) + } + + for _, uri := range csr.URIs { + k8sLog.Info(fmt.Sprintf("URI SAN: %s", uri.String())) + } + + modelRequest := commandsdk.EnrollCSRFctArgs{ + CSR: string(csrBytes), + Template: config.CertificateTemplate, + CertFormat: enrollmentPEMFormat, + Timestamp: time.Now().Format(time.RFC3339), + IncludeChain: true, + SANs: &commandsdk.SANs{}, + Metadata: map[string]interface{}{}, + } + + if config.Meta != nil { + modelRequest.Metadata[CommandMetaControllerNamespace] = config.Meta.ControllerNamespace + modelRequest.Metadata[CommandMetaControllerKind] = config.Meta.ControllerKind + modelRequest.Metadata[CommandMetaControllerResourceGroupName] = config.Meta.ControllerResourceGroupName + modelRequest.Metadata[CommandMetaIssuerName] = config.Meta.IssuerName + modelRequest.Metadata[CommandMetaIssuerNamespace] = config.Meta.IssuerNamespace + modelRequest.Metadata[CommandMetaControllerReconcileId] = config.Meta.ControllerReconcileId + modelRequest.Metadata[CommandMetaCertificateSigningRequestNamespace] = config.Meta.CertificateSigningRequestNamespace + } + + for metaName, value := range extractMetadataFromAnnotations(config.Annotations) { + k8sLog.Info(fmt.Sprintf("Adding metadata %q with value %q", metaName, value)) + modelRequest.Metadata[metaName] = value + } + + var caBuilder strings.Builder + if config.CertificateAuthorityHostname != "" { + caBuilder.WriteString(config.CertificateAuthorityHostname) + caBuilder.WriteString("\\") + } + caBuilder.WriteString(config.CertificateAuthorityLogicalName) + modelRequest.CertificateAuthority = caBuilder.String() + + commandCsrResponseObject, err := s.client.EnrollCSR(&modelRequest) + if err != nil { + detail := fmt.Sprintf("error enrolling certificate with Command. Verify that the certificate template %q exists and that the certificate authority %q (%s) is configured correctly", config.CertificateTemplate, config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname) + + if len(extractMetadataFromAnnotations(config.Annotations)) > 0 { + detail += ". Also verify that the metadata fields provided exist in Command" + } + + err = fmt.Errorf("%w: %s: %w", errCommandEnrollmentFailure, detail, err) + return nil, nil, err + } + + var certBytes []byte + for _, cert := range commandCsrResponseObject.CertificateInformation.Certificates { + block, _ := pem.Decode([]byte(cert)) + if block == nil { + return nil, nil, errors.New("failed to parse certificate PEM") + } + + certBytes = append(certBytes, block.Bytes...) + } + + certs, err := x509.ParseCertificates(certBytes) + if err != nil { + return nil, nil, err + } + + bundlePEM, err := cmpki.ParseSingleCertificateChain(certs) + if err != nil { + return nil, nil, err + } + k8sLog.Info(fmt.Sprintf("Successfully enrolled and serialized certificate with Command with subject %q. Certificate has %d SANs", certs[0].Subject, len(certs[0].DNSNames)+len(certs[0].IPAddresses)+len(certs[0].URIs))) + return bundlePEM.ChainPEM, bundlePEM.CAPEM, nil +} + +// extractMetadataFromAnnotations extracts metadata from the provided annotations +func extractMetadataFromAnnotations(annotations map[string]string) map[string]interface{} { + metadata := make(map[string]interface{}) + + for key, value := range annotations { + if strings.HasPrefix(key, commandMetadataAnnotationPrefix) { + metadata[strings.TrimPrefix(key, commandMetadataAnnotationPrefix)] = value + } + } + + return metadata +} + +// parseCSR takes a byte array containing a PEM encoded CSR and returns a x509.CertificateRequest object +func parseCSR(pemBytes []byte) (*x509.CertificateRequest, error) { + // extract PEM from request object + block, _ := pem.Decode(pemBytes) + if block == nil || block.Type != "CERTIFICATE REQUEST" { + return nil, fmt.Errorf("%w: PEM block type must be CERTIFICATE REQUEST", errInvalidCSR) + } + return x509.ParseCertificateRequest(block.Bytes) +} + +// ptr returns a pointer to the provided value +func ptr[T any](v T) *T { + return &v +} diff --git a/internal/command/command_test.go b/internal/command/command_test.go new file mode 100644 index 0000000..c429e99 --- /dev/null +++ b/internal/command/command_test.go @@ -0,0 +1,807 @@ +/* +Copyright © 2024 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" + commandsdk "github.com/Keyfactor/keyfactor-go-client/v3/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBasicAuthValidate(t *testing.T) { + tests := []struct { + name string + basicAuth *BasicAuth + wantErr string + }{ + { + name: "nil BasicAuth", + basicAuth: nil, + wantErr: "", + }, + { + name: "empty Username", + basicAuth: &BasicAuth{Username: "", Password: "pass"}, + wantErr: "invalid config: username is required", + }, + { + name: "empty Password", + basicAuth: &BasicAuth{Username: "user", Password: ""}, + wantErr: "invalid config: password is required", + }, + { + name: "valid BasicAuth", + basicAuth: &BasicAuth{Username: "user", Password: "pass"}, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.basicAuth.validate() + if tt.wantErr == "" && err != nil { + t.Errorf("expected no error, got %v", err) + } else if tt.wantErr != "" { + if err == nil { + t.Errorf("expected error %q, got nil", tt.wantErr) + } else if err.Error() != tt.wantErr { + t.Errorf("expected error %q, got %q", tt.wantErr, err.Error()) + } + } + }) + } +} + +func TestOAuthValidate(t *testing.T) { + tests := []struct { + name string + oauth *OAuth + wantErr string + }{ + { + name: "nil OAuth", + oauth: nil, + wantErr: "", + }, + { + name: "empty TokenURL", + oauth: &OAuth{TokenURL: "", ClientID: "id", ClientSecret: "secret"}, + wantErr: "invalid config: tokenURL is required", + }, + { + name: "empty ClientID", + oauth: &OAuth{TokenURL: "http://token.url", ClientID: "", ClientSecret: "secret"}, + wantErr: "invalid config: clientID is required", + }, + { + name: "empty ClientSecret", + oauth: &OAuth{TokenURL: "http://token.url", ClientID: "id", ClientSecret: ""}, + wantErr: "invalid config: clientSecret is required", + }, + { + name: "valid OAuth", + oauth: &OAuth{TokenURL: "http://token.url", ClientID: "id", ClientSecret: "secret", Scopes: []string{"scope"}}, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.oauth.validate() + if tt.wantErr == "" && err != nil { + t.Errorf("expected no error, got %v", err) + } else if tt.wantErr != "" { + if err == nil { + t.Errorf("expected error %q, got nil", tt.wantErr) + } else if err.Error() != tt.wantErr { + t.Errorf("expected error %q, got %q", tt.wantErr, err.Error()) + } + } + }) + } +} + +func TestConfigValidate(t *testing.T) { + tests := []struct { + name string + config *Config + wantErr string + }{ + { + name: "missing Hostname", + config: &Config{Hostname: "", APIPath: "/api"}, + wantErr: "invalid config: hostname is required", + }, + { + name: "missing APIPath", + config: &Config{Hostname: "example.com", APIPath: ""}, + wantErr: "invalid config: apiPath is required", + }, + { + name: "invalid BasicAuth", + config: &Config{Hostname: "example.com", APIPath: "/api", BasicAuth: &BasicAuth{Username: "", Password: "pass"}}, + wantErr: "invalid config: username is required", + }, + { + name: "invalid OAuth", + config: &Config{Hostname: "example.com", APIPath: "/api", OAuth: &OAuth{TokenURL: "", ClientID: "id", ClientSecret: "secret"}}, + wantErr: "invalid config: tokenURL is required", + }, + { + name: "all valid with no auth", + config: &Config{Hostname: "example.com", APIPath: "/api"}, + wantErr: "", + }, + { + name: "all valid with BasicAuth", + config: &Config{Hostname: "example.com", APIPath: "/api", BasicAuth: &BasicAuth{Username: "user", Password: "pass"}}, + wantErr: "", + }, + { + name: "all valid with OAuth", + config: &Config{Hostname: "example.com", APIPath: "/api", OAuth: &OAuth{TokenURL: "http://token.url", ClientID: "id", ClientSecret: "secret"}}, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.validate() + if tt.wantErr == "" && err != nil { + t.Errorf("expected no error, got %v", err) + } else if tt.wantErr != "" { + if err == nil { + t.Errorf("expected error %q, got nil", tt.wantErr) + } else if err.Error() != tt.wantErr { + t.Errorf("expected error %q, got %q", tt.wantErr, err.Error()) + } + } + }) + } +} + +func TestSignConfigValidate(t *testing.T) { + tests := []struct { + name string + config *SignConfig + wantErr string + }{ + { + name: "missing certificateTemplate", + config: &SignConfig{CertificateTemplate: "", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + wantErr: "certificateTemplate is required", + }, + { + name: "missing certificateAuthorityLogicalName", + config: &SignConfig{CertificateTemplate: "myTemplate", CertificateAuthorityLogicalName: "", CertificateAuthorityHostname: "ca.example.com"}, + wantErr: "certificateAuthorityLogicalName is required", + }, + { + name: "missing certificateAuthorityHostname", + config: &SignConfig{CertificateTemplate: "myTemplate", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: ""}, + wantErr: "certificateAuthorityHostname is required", + }, + { + name: "all valid fields", + config: &SignConfig{CertificateTemplate: "myTemplate", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + wantErr: "", + }, + { + name: "valid with optional fields", + config: &SignConfig{ + CertificateTemplate: "myTemplate", + CertificateAuthorityLogicalName: "ca-logical", + CertificateAuthorityHostname: "ca.example.com", + Annotations: map[string]string{"environment": "prod"}, + }, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.validate() + if tt.wantErr == "" && err != nil { + t.Errorf("expected no error, got %v", err) + } else if tt.wantErr != "" { + if err == nil { + t.Errorf("expected error %q, got nil", tt.wantErr) + } else if err.Error() != tt.wantErr { + t.Errorf("expected error %q, got %q", tt.wantErr, err.Error()) + } + } + }) + } +} + +var ( + _ commandsdk.AuthConfig = &fakeCommandAuthenticator{} +) + +type fakeCommandAuthenticator struct { + client *http.Client + config *auth_providers.Server +} + +// Authenticate implements api.AuthConfig. +func (f *fakeCommandAuthenticator) Authenticate() error { + return nil +} + +// GetHttpClient implements api.AuthConfig. +func (f *fakeCommandAuthenticator) GetHttpClient() (*http.Client, error) { + return f.client, nil +} + +// GetServerConfig implements api.AuthConfig. +func (f *fakeCommandAuthenticator) GetServerConfig() *auth_providers.Server { + return f.config +} + +func newFakeCommandClientFunc(httpClient *http.Client) newCommandClientFunc { + return newCommandClientFunc(func(s *auth_providers.Server, ctx *context.Context) (*commandsdk.Client, error) { + client := &commandsdk.Client{ + AuthClient: &fakeCommandAuthenticator{ + client: httpClient, + config: s, + }, + } + + return client, nil + }) +} + +func TestNewServerConfig(t *testing.T) { + + testCases := map[string]struct { + config *Config + + expectedAuthProviderServer *auth_providers.Server + expectedError error + }{ + "no-config": { + config: nil, + + expectedError: errInvalidConfig, + expectedAuthProviderServer: nil, + }, + "basic-auth": { + config: &Config{ + Hostname: "example.com", + APIPath: "///api//", // should remove preceding & trailing slashes + BasicAuth: &BasicAuth{ + Username: "domain\\username", + Password: "password", + }, + }, + + expectedAuthProviderServer: &auth_providers.Server{ + Host: "example.com", + Username: "domain\\username", + Password: "password", + Domain: "", + ClientID: "", + ClientSecret: "", + OAuthTokenUrl: "", + APIPath: "api", + Audience: "", + SkipTLSVerify: false, + AuthType: "basic", + }, + expectedError: nil, + }, + "oauth": { + config: &Config{ + Hostname: "example.com", + APIPath: "///api//", // should remove preceding & trailing slashes + OAuth: &OAuth{ + TokenURL: "http://token.url", + ClientID: "id", + ClientSecret: "secret", + Scopes: []string{"cert:issuer"}, + Audience: "example.com", + }, + }, + + expectedAuthProviderServer: &auth_providers.Server{ + Host: "example.com", + ClientID: "id", + ClientSecret: "secret", + AccessToken: "", + OAuthTokenUrl: "http://token.url", + APIPath: "api", + Scopes: []string{"cert:issuer"}, + Audience: "example.com", + SkipTLSVerify: false, + AuthType: "oauth", + }, + expectedError: nil, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + serverConfig, err := newServerConfig(context.Background(), tc.config) + if tc.expectedError != nil { + assertErrorIs(t, tc.expectedError, err) + } else { + assert.NoError(t, err) + assert.Equal(t, *tc.expectedAuthProviderServer, *serverConfig) + } + }) + } +} + +var ( + _ Client = &fakeClient{} +) + +type fakeClient struct { + enrollCallback func(*commandsdk.EnrollCSRFctArgs) + enrollResponse *commandsdk.EnrollResponse + + metadataFields []commandsdk.MetadataField + + err error +} + +// EnrollCSR implements Client. +func (f *fakeClient) EnrollCSR(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) { + if f.enrollCallback != nil { + f.enrollCallback(ea) + } + return f.enrollResponse, f.err +} + +// GetAllMetadataFields implements Client. +func (f *fakeClient) GetAllMetadataFields() ([]commandsdk.MetadataField, error) { + return f.metadataFields, f.err +} + +// TestConnection implements Client. +func (f *fakeClient) TestConnection() error { + return f.err +} + +func TestSign(t *testing.T) { + caCert, rootKey := issueTestCertificate(t, "Root-CA", nil, nil) + caCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}) + + issuingCert, issuingKey := issueTestCertificate(t, "Sub-CA", caCert, rootKey) + issuingCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: issuingCert.Raw}) + + leafCert, _ := issueTestCertificate(t, "LeafCert", issuingCert, issuingKey) + leafCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + expectedLeafAndChain := append([]*x509.Certificate{leafCert}, issuingCert) + + certificateTemplateName := "fake-cert-template" + certificateAuthorityLogicalName := "fake-issuing-ca" + certificateAuthorityHostname := "pki.example.com" + + testCases := map[string]struct { + enrollCSRFunctionError error + + // Request + config *SignConfig + + // Expected + expectedEnrollArgs *commandsdk.EnrollCSRFctArgs + expectedSignError error + }{ + "success-no-meta": { + // Request + config: &SignConfig{ + CertificateTemplate: certificateTemplateName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: nil, + }, + + // Expected + expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + Template: certificateTemplateName, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: &commandsdk.SANs{}, + Metadata: map[string]interface{}{}, + }, + expectedSignError: nil, + }, + "success-annotation-config-override": { + // Request + config: &SignConfig{ + CertificateTemplate: certificateTemplateName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: map[string]string{ + "command-issuer.keyfactor.com/certificateTemplate": "template-override", + "command-issuer.keyfactor.com/certificateAuthorityLogicalName": "logicalname-override", + "command-issuer.keyfactor.com/certificateAuthorityHostname": "hostname-override", + }, + }, + + // Expected + expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + Template: "template-override", + CertificateAuthority: fmt.Sprintf("%s\\%s", "hostname-override", "logicalname-override"), + SANs: &commandsdk.SANs{}, + Metadata: map[string]interface{}{}, + }, + expectedSignError: nil, + }, + "success-predefined-meta": { + // Request + config: &SignConfig{ + CertificateTemplate: certificateTemplateName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: &K8sMetadata{ + ControllerNamespace: "namespace", + ControllerKind: "Issuer", + ControllerResourceGroupName: "rg.test.com", + IssuerName: "test", + IssuerNamespace: "ns", + ControllerReconcileId: "alksdfjlasdljkf", + CertificateSigningRequestNamespace: "other-namespace", + CertManagerCertificateName: "cert-name", + }, + Annotations: nil, + }, + + // Expected + expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + Template: certificateTemplateName, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: &commandsdk.SANs{}, + Metadata: map[string]interface{}{ + CommandMetaControllerNamespace: "namespace", + CommandMetaControllerKind: "Issuer", + CommandMetaControllerResourceGroupName: "rg.test.com", + CommandMetaIssuerName: "test", + CommandMetaIssuerNamespace: "ns", + CommandMetaControllerReconcileId: "alksdfjlasdljkf", + CommandMetaCertificateSigningRequestNamespace: "other-namespace", + }, + }, + expectedSignError: nil, + }, + "success-custom-meta": { + // Request + config: &SignConfig{ + CertificateTemplate: certificateTemplateName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: map[string]string{ + fmt.Sprintf("%s%s", commandMetadataAnnotationPrefix, "testMetadata"): "test", + }, + }, + + // Expected + expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + Template: certificateTemplateName, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: &commandsdk.SANs{}, + Metadata: map[string]interface{}{ + "testMetadata": "test", + }, + }, + expectedSignError: nil, + }, + "enroll-csr-err": { + enrollCSRFunctionError: errors.New("an error from Command"), + // Request + config: &SignConfig{ + CertificateTemplate: certificateTemplateName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: nil, + }, + + // Expected + expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + Template: certificateTemplateName, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: &commandsdk.SANs{}, + Metadata: map[string]interface{}{}, + }, + expectedSignError: errCommandEnrollmentFailure, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + cb := func(ea *commandsdk.EnrollCSRFctArgs) { + require.Equal(t, tc.expectedEnrollArgs.CertificateAuthority, ea.CertificateAuthority) + require.Equal(t, tc.expectedEnrollArgs.Template, ea.Template) + + require.Equal(t, tc.expectedEnrollArgs.Metadata, ea.Metadata) + } + + client := fakeClient{ + err: tc.enrollCSRFunctionError, + + enrollResponse: certificateRestResponseFromExpectedCerts(t, expectedLeafAndChain, []*x509.Certificate{caCert}), + enrollCallback: cb, + } + signer := signer{ + client: &client, + } + + csrBytes, err := generateCSR("CN=command.example.org", nil, nil, nil) + require.NoError(t, err) + csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes.Raw}) + + leafAndCA, root, err := signer.Sign(context.Background(), csrPem, tc.config) + if tc.expectedSignError != nil { + assertErrorIs(t, tc.expectedSignError, err) + } else { + assert.NoError(t, err) + + require.Equal(t, leafAndCA, append(leafCertPem, issuingCertPem...)) + require.Equal(t, root, caCertPem) + } + }) + } +} + +func TestCommandSupportsMetadata(t *testing.T) { + testCases := map[string]struct { + presentMeta []commandsdk.MetadataField + + // Expected + expected bool + }{ + "success-no-meta": { + presentMeta: []commandsdk.MetadataField{}, + + // Expected + expected: false, + }, + "success-all-meta": { + presentMeta: []commandsdk.MetadataField{ + { + Name: CommandMetaControllerNamespace, + }, + { + Name: CommandMetaControllerKind, + }, + { + Name: CommandMetaControllerResourceGroupName, + }, + { + Name: CommandMetaIssuerName, + }, + { + Name: CommandMetaIssuerNamespace, + }, + { + Name: CommandMetaControllerReconcileId, + }, + { + Name: CommandMetaCertificateSigningRequestNamespace, + }, + }, + + // Expected + expected: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + client := fakeClient{ + err: nil, + + metadataFields: tc.presentMeta, + } + signer := signer{ + client: &client, + } + + supported, err := signer.CommandSupportsMetadata() + assert.NoError(t, err) + require.Equal(t, tc.expected, supported) + }) + } +} + +func assertErrorIs(t *testing.T, expectedError, actualError error) { + if !assert.Error(t, actualError) { + return + } + assert.Truef(t, errors.Is(actualError, expectedError), "unexpected error type. expected: %v, got: %v", expectedError, actualError) +} + +func certificateRestResponseFromExpectedCerts(t *testing.T, leafCertAndChain []*x509.Certificate, rootCAs []*x509.Certificate) *commandsdk.EnrollResponse { + require.NotEqual(t, 0, len(leafCertAndChain)) + leaf := string(pem.EncodeToMemory(&pem.Block{Bytes: leafCertAndChain[0].Raw, Type: "CERTIFICATE"})) + + certs := []string{leaf} + for _, cert := range leafCertAndChain[1:] { + certs = append(certs, string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) + } + for _, cert := range rootCAs { + certs = append(certs, string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) + } + + response := &commandsdk.EnrollResponse{ + Certificates: certs, + CertificateInformation: commandsdk.CertificateInformation{ + SerialNumber: "", + IssuerDN: "", + Thumbprint: "", + KeyfactorID: 0, + KeyfactorRequestID: 0, + PKCS12Blob: "", + Certificates: certs, + RequestDisposition: "", + DispositionMessage: "", + EnrollmentContext: nil, + }, + } + return response +} + +func generateCSR(subject string, dnsNames []string, uris []string, ipAddresses []string) (*x509.CertificateRequest, error) { + keyBytes, _ := rsa.GenerateKey(rand.Reader, 2048) + + var name pkix.Name + + if subject != "" { + // Split the subject into its individual parts + parts := strings.Split(subject, ",") + + for _, part := range parts { + // Split the part into key and value + keyValue := strings.SplitN(part, "=", 2) + + if len(keyValue) != 2 { + return nil, errors.New("invalid subject") + } + + key := strings.TrimSpace(keyValue[0]) + value := strings.TrimSpace(keyValue[1]) + + // Map the key to the appropriate field in the pkix.Name struct + switch key { + case "C": + name.Country = []string{value} + case "ST": + name.Province = []string{value} + case "L": + name.Locality = []string{value} + case "O": + name.Organization = []string{value} + case "OU": + name.OrganizationalUnit = []string{value} + case "CN": + name.CommonName = value + default: + // Ignore any unknown keys + } + } + } + + template := x509.CertificateRequest{ + Subject: name, + SignatureAlgorithm: x509.SHA256WithRSA, + } + + if len(dnsNames) > 0 { + template.DNSNames = dnsNames + } + + // Parse and add URIs + var uriPointers []*url.URL + for _, u := range uris { + if u == "" { + continue + } + uriPointer, err := url.Parse(u) + if err != nil { + return nil, err + } + uriPointers = append(uriPointers, uriPointer) + } + template.URIs = uriPointers + + // Parse and add IPAddresses + var ipAddrs []net.IP + for _, ipStr := range ipAddresses { + if ipStr == "" { + continue + } + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("invalid IP address: %s", ipStr) + } + ipAddrs = append(ipAddrs, ip) + } + template.IPAddresses = ipAddrs + + // Generate the CSR + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, keyBytes) + if err != nil { + return nil, err + } + + parsedCSR, err := x509.ParseCertificateRequest(csrBytes) + if err != nil { + return nil, err + } + + return parsedCSR, nil +} + +func issueTestCertificate(t *testing.T, cn string, parent *x509.Certificate, signingKey any) (*x509.Certificate, *ecdsa.PrivateKey) { + var err error + var key *ecdsa.PrivateKey + now := time.Now() + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + publicKey := &key.PublicKey + signerPrivateKey := key + if signingKey != nil { + signerPrivateKey = signingKey.(*ecdsa.PrivateKey) + } + + serial, _ := rand.Int(rand.Reader, big.NewInt(1337)) + certTemplate := &x509.Certificate{ + Subject: pkix.Name{CommonName: cn}, + SerialNumber: serial, + BasicConstraintsValid: true, + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24), + } + + if parent == nil { + parent = certTemplate + } + + certData, err := x509.CreateCertificate(rand.Reader, certTemplate, parent, publicKey, signerPrivateKey) + require.NoError(t, err) + + cert, err := x509.ParseCertificate(certData) + require.NoError(t, err) + + return cert, key +} diff --git a/internal/controllers/certificaterequest_controller.go b/internal/controller/certificaterequest_controller.go similarity index 69% rename from internal/controllers/certificaterequest_controller.go rename to internal/controller/certificaterequest_controller.go index b106a26..053d0b0 100644 --- a/internal/controllers/certificaterequest_controller.go +++ b/internal/controller/certificaterequest_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Keyfactor. +Copyright © 2024 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,19 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" "errors" "fmt" - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" - "github.com/Keyfactor/command-issuer/internal/issuer/signer" - issuerutil "github.com/Keyfactor/command-issuer/internal/issuer/util" + + commandissuer "github.com/Keyfactor/command-cert-manager-issuer/api/v1alpha1" + "github.com/Keyfactor/command-cert-manager-issuer/internal/command" cmutil "github.com/cert-manager/cert-manager/pkg/api/util" cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -47,9 +46,8 @@ var ( type CertificateRequestReconciler struct { client.Client - ConfigClient issuerutil.ConfigClient Scheme *runtime.Scheme - SignerBuilder signer.CommandSignerBuilder + SignerBuilder command.SignerBuilder ClusterResourceNamespace string SecretAccessGrantedAtClusterLevel bool Clock clock.Clock @@ -65,7 +63,7 @@ type CertificateRequestReconciler struct { func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { log := ctrl.LoggerFrom(ctx) - meta := signer.K8sMetadata{} + meta := command.K8sMetadata{} // Get the CertificateRequest var certificateRequest cmapi.CertificateRequest @@ -73,7 +71,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R if err := client.IgnoreNotFound(err); err != nil { return ctrl.Result{}, fmt.Errorf("unexpected get error: %v", err) } - log.Info("Not found. Ignoring.") + log.Info("CertificateRequest not found. ignoring.") return ctrl.Result{}, nil } @@ -113,20 +111,11 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R // We now have a CertificateRequest that belongs to us so we are responsible // for updating its Ready condition. - setReadyCondition := func(status cmmeta.ConditionStatus, reason, message string) { - cmutil.SetCertificateRequestCondition( - &certificateRequest, - cmapi.CertificateRequestConditionReady, - status, - reason, - message, - ) - } // Always attempt to update the Ready condition defer func() { if err != nil { - setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, err.Error()) + setCertificateRequestReadyCondition(&certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, err.Error()) } if updateErr := r.Status().Update(ctx, &certificateRequest); updateErr != nil { err = utilerrors.NewAggregate([]error{err, updateErr}) @@ -145,7 +134,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R } message := "The CertificateRequest was denied by an approval controller" - setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonDenied, message) + setCertificateRequestReadyCondition(&certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonDenied, message) return ctrl.Result{}, nil } @@ -160,7 +149,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R // Add a Ready condition if one does not already exist if ready := cmutil.GetCertificateRequestCondition(&certificateRequest, cmapi.CertificateRequestConditionReady); ready == nil { log.Info("Initializing Ready condition") - setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Initializing") + setCertificateRequestReadyCondition(&certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "Initializing") return ctrl.Result{}, nil } @@ -170,30 +159,33 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R if err != nil { err = fmt.Errorf("%w: %v", errIssuerRef, err) log.Error(err, "Unrecognized kind. Ignoring.") - setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error()) + setCertificateRequestReadyCondition(&certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error()) return ctrl.Result{}, nil } - issuer := issuerRO.(client.Object) - // Create a Namespaced name for Issuer and a non-Namespaced name for ClusterIssuer - issuerName := types.NamespacedName{ - Name: certificateRequest.Spec.IssuerRef.Name, + issuer, ok := issuerRO.(commandissuer.IssuerLike) + if !ok { + err := fmt.Errorf("unexpected type for issuer object: %T", issuerRO) + log.Error(err, "Failed to cast to commandissuer.IssuerLike") + setCertificateRequestReadyCondition(&certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error()) + return ctrl.Result{}, nil } + var secretNamespace string - switch t := issuer.(type) { - case *commandissuer.Issuer: - issuerName.Namespace = certificateRequest.Namespace - secretNamespace = certificateRequest.Namespace - log = log.WithValues("issuer", issuerName) - meta.ControllerKind = "issuer" - case *commandissuer.ClusterIssuer: + var issuerNamespace string + + // Create a Namespaced name for Issuer and a non-Namespaced name for ClusterIssuer + switch { + case issuer.IsClusterScoped(): + issuerNamespace = "" secretNamespace = r.ClusterResourceNamespace - log = log.WithValues("clusterissuer", issuerName) + log = log.WithValues("clusterissuer", issuerNamespace) meta.ControllerKind = "clusterissuer" - default: - err := fmt.Errorf("unexpected issuer type: %v", t) - log.Error(err, "The issuerRef referred to a registered Kind which is not yet handled. Ignoring.") - setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error()) - return ctrl.Result{}, nil + + case !issuer.IsClusterScoped(): + issuerNamespace = certificateRequest.Namespace + secretNamespace = certificateRequest.Namespace + log = log.WithValues("issuer", issuerNamespace) + meta.ControllerKind = "issuer" } // If SecretAccessGrantedAtClusterLevel is false, we always look for the Secret in the same namespace as the Issuer @@ -202,50 +194,24 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R } // Get the Issuer or ClusterIssuer - if err := r.Get(ctx, issuerName, issuer); err != nil { - return ctrl.Result{}, fmt.Errorf("%w: %v", errGetIssuer, err) - } - - issuerSpec, issuerStatus, err := issuerutil.GetSpecAndStatus(issuer) + err = r.Get(ctx, types.NamespacedName{ + Name: certificateRequest.Spec.IssuerRef.Name, + Namespace: issuerNamespace, + }, issuer) if err != nil { - log.Error(err, "Unable to get the IssuerStatus. Ignoring.") - setReadyCondition(cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error()) - return ctrl.Result{}, nil + return ctrl.Result{}, fmt.Errorf("%w: %w", errGetIssuer, err) } - if !issuerutil.IsReady(issuerStatus) { + if !issuer.GetStatus().HasCondition(commandissuer.IssuerConditionReady, commandissuer.ConditionTrue) { return ctrl.Result{}, errIssuerNotReady } - // Set the context on the config client - r.ConfigClient.SetContext(ctx) - - authSecretName := types.NamespacedName{ - Name: issuerSpec.SecretName, - Namespace: secretNamespace, - } - - var authSecret corev1.Secret - if err = r.ConfigClient.GetSecret(authSecretName, &authSecret); err != nil { - return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetAuthSecret, authSecretName, err) - } - - // Retrieve the CA certificate secret - caSecretName := types.NamespacedName{ - Name: issuerSpec.CaSecretName, - Namespace: authSecretName.Namespace, - } - - var caSecret corev1.Secret - if issuerSpec.CaSecretName != "" { - // If the CA secret name is not specified, we will not attempt to retrieve it - err = r.ConfigClient.GetSecret(caSecretName, &caSecret) - if err != nil { - return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetCaSecret, caSecretName, err) - } + config, err := commandConfigFromIssuer(ctx, r.Client, issuer, secretNamespace) + if err != nil { + return ctrl.Result{}, err } - commandSigner, err := r.SignerBuilder(ctx, issuerSpec, certificateRequest.GetAnnotations(), authSecret.Data, caSecret.Data) + commandSigner, err := r.SignerBuilder(ctx, config) if err != nil { return ctrl.Result{}, fmt.Errorf("%w: %v", errSignerBuilder, err) } @@ -259,17 +225,42 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R meta.ControllerReconcileId = string(controller.ReconcileIDFromContext(ctx)) meta.CertificateSigningRequestNamespace = certificateRequest.Namespace - leaf, chain, err := commandSigner.Sign(ctx, certificateRequest.Spec.Request, meta) + if value, exists := certificateRequest.Annotations["cert-manager.io/certificate-name"]; exists { + meta.CertManagerCertificateName = value + } + + signConfig := &command.SignConfig{ + CertificateTemplate: issuer.GetSpec().CertificateTemplate, + CertificateAuthorityLogicalName: issuer.GetSpec().CertificateAuthorityLogicalName, + CertificateAuthorityHostname: issuer.GetSpec().CertificateAuthorityHostname, + Annotations: certificateRequest.GetAnnotations(), + } + + if issuer.GetStatus().HasCondition(commandissuer.IssuerConditionSupportsMetadata, commandissuer.ConditionTrue) { + signConfig.Meta = &meta + } + + leaf, chain, err := commandSigner.Sign(ctx, certificateRequest.Spec.Request, signConfig) if err != nil { - return ctrl.Result{}, fmt.Errorf("%w: %v", errSignerSign, err) + return ctrl.Result{}, fmt.Errorf("%w: %w", errSignerSign, err) } certificateRequest.Status.Certificate = leaf certificateRequest.Status.CA = chain - setReadyCondition(cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "Signed") + setCertificateRequestReadyCondition(&certificateRequest, cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "Signed") return ctrl.Result{}, nil } +func setCertificateRequestReadyCondition(cr *cmapi.CertificateRequest, status cmmeta.ConditionStatus, reason, message string) { + cmutil.SetCertificateRequestCondition( + cr, + cmapi.CertificateRequestConditionReady, + status, + reason, + message, + ) +} + // SetupWithManager registers the CertificateRequestReconciler with the controller manager. // It configures controller-runtime to reconcile cert-manager CertificateRequests in the cluster. func (r *CertificateRequestReconciler) SetupWithManager(mgr ctrl.Manager) error { diff --git a/internal/controllers/certificaterequest_controller_test.go b/internal/controller/certificaterequest_controller_test.go similarity index 56% rename from internal/controllers/certificaterequest_controller_test.go rename to internal/controller/certificaterequest_controller_test.go index 46cc9ba..e01d633 100644 --- a/internal/controllers/certificaterequest_controller_test.go +++ b/internal/controller/certificaterequest_controller_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Keyfactor +Copyright © 2024 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,20 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" "errors" + "math/big" + "testing" + "time" + + commandissuerv1alpha1 "github.com/Keyfactor/command-cert-manager-issuer/api/v1alpha1" + "github.com/Keyfactor/command-cert-manager-issuer/internal/command" cmutil "github.com/cert-manager/cert-manager/pkg/api/util" cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" cmgen "github.com/cert-manager/cert-manager/test/unit/gen" - logrtesting "github.com/go-logr/logr/testr" + logrtesting "github.com/go-logr/logr/testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" - apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -37,10 +47,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "testing" - - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" - "github.com/Keyfactor/command-issuer/internal/issuer/signer" ) var ( @@ -51,27 +57,36 @@ type fakeSigner struct { errSign error } -func (o *fakeSigner) Sign(context.Context, []byte, signer.K8sMetadata) ([]byte, []byte, error) { - return []byte("fake signed certificate"), []byte("fake ca chain"), o.errSign +func (o *fakeSigner) Sign(context.Context, []byte, *command.SignConfig) ([]byte, []byte, error) { + return []byte("fake signed certificate"), []byte("fake chain"), o.errSign } -func TestCertificateRequestReconcile(t *testing.T) { - //nowMetaTime := metav1.NewTime(fixedClockStart) +var newFakeSignerBuilder = func(builderErr error, signerErr error) func(context.Context, *command.Config) (command.Signer, error) { + return func(context.Context, *command.Config) (command.Signer, error) { + return &fakeSigner{ + errSign: signerErr, + }, builderErr + } +} +func TestCertificateRequestReconcile(t *testing.T) { type testCase struct { - name types.NamespacedName - objects []client.Object - Builder signer.CommandSignerBuilder - clusterResourceNamespace string + name types.NamespacedName + signerBuilder command.SignerBuilder + + // Configuration + objects []client.Object + clusterResourceNamespace string + + // Expected expectedResult ctrl.Result expectedError error expectedReadyConditionStatus cmmeta.ConditionStatus expectedReadyConditionReason string - expectedFailureTime *metav1.Time expectedCertificate []byte } tests := map[string]testCase{ - "success-issuer": { + "success-issuer-basicauth": { name: types.NamespacedName{Namespace: "ns1", Name: "cr1"}, objects: []client.Object{ cmgen.CertificateRequest( @@ -79,7 +94,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -91,39 +106,41 @@ func TestCertificateRequestReconcile(t *testing.T) { Status: cmmeta.ConditionUnknown, }), ), - &commandissuer.Issuer{ + &commandissuerv1alpha1.Issuer{ ObjectMeta: metav1.ObjectMeta{ Name: "issuer1", Namespace: "ns1", }, - Spec: commandissuer.IssuerSpec{ + Spec: commandissuerv1alpha1.IssuerSpec{ SecretName: "issuer1-credentials", }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionTrue, + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, }, }, }, }, &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, ObjectMeta: metav1.ObjectMeta{ Name: "issuer1-credentials", Namespace: "ns1", }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, }, }, - Builder: func(context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) { - return &fakeSigner{}, nil - }, + signerBuilder: newFakeSignerBuilder(nil, nil), expectedReadyConditionStatus: cmmeta.ConditionTrue, expectedReadyConditionReason: cmapi.CertificateRequestReasonIssued, - expectedFailureTime: nil, expectedCertificate: []byte("fake signed certificate"), }, - "success-cluster-issuer": { + "success-clusterissuer-basicauth": { name: types.NamespacedName{Namespace: "ns1", Name: "cr1"}, objects: []client.Object{ cmgen.CertificateRequest( @@ -131,7 +148,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "clusterissuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "ClusterIssuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -143,36 +160,152 @@ func TestCertificateRequestReconcile(t *testing.T) { Status: cmmeta.ConditionUnknown, }), ), - &commandissuer.ClusterIssuer{ + &commandissuerv1alpha1.ClusterIssuer{ ObjectMeta: metav1.ObjectMeta{ Name: "clusterissuer1", }, - Spec: commandissuer.IssuerSpec{ + Spec: commandissuerv1alpha1.IssuerSpec{ SecretName: "clusterissuer1-credentials", }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionTrue, + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, }, }, }, }, &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, ObjectMeta: metav1.ObjectMeta{ Name: "clusterissuer1-credentials", Namespace: "kube-system", }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, }, }, - Builder: func(context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) { - return &fakeSigner{}, nil + signerBuilder: newFakeSignerBuilder(nil, nil), + clusterResourceNamespace: "kube-system", + expectedReadyConditionStatus: cmmeta.ConditionTrue, + expectedReadyConditionReason: cmapi.CertificateRequestReasonIssued, + expectedCertificate: []byte("fake signed certificate"), + }, + "success-issuer-oauth": { + name: types.NamespacedName{Namespace: "ns1", Name: "cr1"}, + objects: []client.Object{ + cmgen.CertificateRequest( + "cr1", + cmgen.SetCertificateRequestNamespace("ns1"), + cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ + Name: "issuer1", + Group: commandissuerv1alpha1.GroupVersion.Group, + Kind: "Issuer", + }), + cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionApproved, + Status: cmmeta.ConditionTrue, + }), + cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionReady, + Status: cmmeta.ConditionUnknown, + }), + ), + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + commandissuerv1alpha1.OAuthTokenURLKey: []byte("https://dev.idp.com/oauth/token"), + commandissuerv1alpha1.OAuthClientIDKey: []byte("fi3ElQUVoBBHyRNt4mpUxG9WY65AOCcJ"), + commandissuerv1alpha1.OAuthClientSecretKey: []byte("1EXHdD7Ikmmv0OkBoJZZtzOG5iAzvwdqBVuvquf-QEvL6fLrEG_heJHphtEXVj9H"), + commandissuerv1alpha1.OAuthScopesKey: []byte("read:certificates,write:certificates"), + commandissuerv1alpha1.OAuthAudienceKey: []byte("https://command.example.com"), + }, + }, + }, + signerBuilder: newFakeSignerBuilder(nil, nil), + expectedReadyConditionStatus: cmmeta.ConditionTrue, + expectedReadyConditionReason: cmapi.CertificateRequestReasonIssued, + expectedCertificate: []byte("fake signed certificate"), + }, + "success-cluster-issuer-oauth": { + name: types.NamespacedName{Namespace: "ns1", Name: "cr1"}, + objects: []client.Object{ + cmgen.CertificateRequest( + "cr1", + cmgen.SetCertificateRequestNamespace("ns1"), + cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ + Name: "clusterissuer1", + Group: commandissuerv1alpha1.GroupVersion.Group, + Kind: "ClusterIssuer", + }), + cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionApproved, + Status: cmmeta.ConditionTrue, + }), + cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionReady, + Status: cmmeta.ConditionUnknown, + }), + ), + &commandissuerv1alpha1.ClusterIssuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterissuer1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "clusterissuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterissuer1-credentials", + Namespace: "kube-system", + }, + Data: map[string][]byte{ + commandissuerv1alpha1.OAuthTokenURLKey: []byte("https://dev.idp.com/oauth/token"), + commandissuerv1alpha1.OAuthClientIDKey: []byte("fi3ElQUVoBBHyRNt4mpUxG9WY65AOCcJ"), + commandissuerv1alpha1.OAuthClientSecretKey: []byte("1EXHdD7Ikmmv0OkBoJZZtzOG5iAzvwdqBVuvquf-QEvL6fLrEG_heJHphtEXVj9H"), + commandissuerv1alpha1.OAuthScopesKey: []byte("read:certificates,write:certificates"), + commandissuerv1alpha1.OAuthAudienceKey: []byte("https://command.example.com"), + }, + }, }, + signerBuilder: newFakeSignerBuilder(nil, nil), clusterResourceNamespace: "kube-system", expectedReadyConditionStatus: cmmeta.ConditionTrue, expectedReadyConditionReason: cmapi.CertificateRequestReasonIssued, - expectedFailureTime: nil, expectedCertificate: []byte("fake signed certificate"), }, "certificaterequest-not-found": { @@ -199,7 +332,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -221,7 +354,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -241,7 +374,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "ForeignKind", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -265,7 +398,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -290,7 +423,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "clusterissuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "ClusterIssuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -315,7 +448,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -327,16 +460,16 @@ func TestCertificateRequestReconcile(t *testing.T) { Status: cmmeta.ConditionUnknown, }), ), - &commandissuer.Issuer{ + &commandissuerv1alpha1.Issuer{ ObjectMeta: metav1.ObjectMeta{ Name: "issuer1", Namespace: "ns1", }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionFalse, + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionFalse, }, }, }, @@ -354,7 +487,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -366,19 +499,19 @@ func TestCertificateRequestReconcile(t *testing.T) { Status: cmmeta.ConditionUnknown, }), ), - &commandissuer.Issuer{ + &commandissuerv1alpha1.Issuer{ ObjectMeta: metav1.ObjectMeta{ Name: "issuer1", Namespace: "ns1", }, - Spec: commandissuer.IssuerSpec{ + Spec: commandissuerv1alpha1.IssuerSpec{ SecretName: "issuer1-credentials", }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionTrue, + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, }, }, }, @@ -396,7 +529,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -408,33 +541,36 @@ func TestCertificateRequestReconcile(t *testing.T) { Status: cmmeta.ConditionUnknown, }), ), - &commandissuer.Issuer{ + &commandissuerv1alpha1.Issuer{ ObjectMeta: metav1.ObjectMeta{ Name: "issuer1", Namespace: "ns1", }, - Spec: commandissuer.IssuerSpec{ + Spec: commandissuerv1alpha1.IssuerSpec{ SecretName: "issuer1-credentials", }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionTrue, + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, }, }, }, }, &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, ObjectMeta: metav1.ObjectMeta{ Name: "issuer1-credentials", Namespace: "ns1", }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, }, }, - Builder: func(context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) { - return nil, errors.New("simulated signer builder error") - }, + signerBuilder: newFakeSignerBuilder(errors.New("simulated signer builder error"), nil), expectedError: errSignerBuilder, expectedReadyConditionStatus: cmmeta.ConditionFalse, expectedReadyConditionReason: cmapi.CertificateRequestReasonPending, @@ -447,7 +583,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -459,33 +595,36 @@ func TestCertificateRequestReconcile(t *testing.T) { Status: cmmeta.ConditionUnknown, }), ), - &commandissuer.Issuer{ + &commandissuerv1alpha1.Issuer{ ObjectMeta: metav1.ObjectMeta{ Name: "issuer1", Namespace: "ns1", }, - Spec: commandissuer.IssuerSpec{ + Spec: commandissuerv1alpha1.IssuerSpec{ SecretName: "issuer1-credentials", }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionTrue, + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, }, }, }, }, &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, ObjectMeta: metav1.ObjectMeta{ Name: "issuer1-credentials", Namespace: "ns1", }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, }, }, - Builder: func(context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) { - return &fakeSigner{errSign: errors.New("simulated sign error")}, nil - }, + signerBuilder: newFakeSignerBuilder(nil, errors.New("simulated sign error")), expectedError: errSignerSign, expectedReadyConditionStatus: cmmeta.ConditionFalse, expectedReadyConditionReason: cmapi.CertificateRequestReasonPending, @@ -498,7 +637,7 @@ func TestCertificateRequestReconcile(t *testing.T) { cmgen.SetCertificateRequestNamespace("ns1"), cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ Name: "issuer1", - Group: commandissuer.GroupVersion.Group, + Group: commandissuerv1alpha1.GroupVersion.Group, Kind: "Issuer", }), cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ @@ -506,92 +645,96 @@ func TestCertificateRequestReconcile(t *testing.T) { Status: cmmeta.ConditionUnknown, }), ), - &commandissuer.Issuer{ + &commandissuerv1alpha1.Issuer{ ObjectMeta: metav1.ObjectMeta{ Name: "issuer1", Namespace: "ns1", }, - Spec: commandissuer.IssuerSpec{ + Spec: commandissuerv1alpha1.IssuerSpec{ SecretName: "issuer1-credentials", }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionTrue, + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, }, }, }, }, &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, ObjectMeta: metav1.ObjectMeta{ Name: "issuer1-credentials", Namespace: "ns1", }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, }, }, - Builder: func(context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (signer.Signer, error) { - return &fakeSigner{}, nil - }, - expectedFailureTime: nil, + signerBuilder: newFakeSignerBuilder(nil, nil), expectedCertificate: nil, }, - //"request-denied": { - // name: types.NamespacedName{Namespace: "ns1", Name: "cr1"}, - // objects: []client.Object{ - // cmgen.CertificateRequest( - // "cr1", - // cmgen.SetCertificateRequestNamespace("ns1"), - // cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ - // Name: "issuer1", - // Group: commandissuer.GroupVersion.Group, - // Kind: "Issuer", - // }), - // cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ - // Type: cmapi.CertificateRequestConditionDenied, - // Status: cmmeta.ConditionTrue, - // }), - // cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ - // Type: cmapi.CertificateRequestConditionReady, - // Status: cmmeta.ConditionUnknown, - // }), - // ), - // &commandissuer.Issuer{ - // ObjectMeta: metav1.ObjectMeta{ - // Name: "issuer1", - // Namespace: "ns1", - // }, - // Spec: commandissuer.IssuerSpec{ - // SecretName: "issuer1-credentials", - // }, - // Status: commandissuer.IssuerStatus{ - // Conditions: []commandissuer.IssuerCondition{ - // { - // Type: commandissuer.IssuerConditionReady, - // Status: commandissuer.ConditionTrue, - // }, - // }, - // }, - // }, - // &corev1.Secret{ - // ObjectMeta: metav1.ObjectMeta{ - // Name: "issuer1-credentials", - // Namespace: "ns1", - // }, - // }, - // }, - // Builder: func(*commandissuer.IssuerSpec, map[string][]byte) (signer.Signer, error) { - // return &fakeSigner{}, nil - // }, - // expectedCertificate: nil, - // expectedFailureTime: &nowMetaTime, - // expectedReadyConditionStatus: cmmeta.ConditionFalse, - // expectedReadyConditionReason: cmapi.CertificateRequestReasonDenied, - //}, + "request-denied": { + name: types.NamespacedName{Namespace: "ns1", Name: "cr1"}, + objects: []client.Object{ + cmgen.CertificateRequest( + "cr1", + cmgen.SetCertificateRequestNamespace("ns1"), + cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{ + Name: "issuer1", + Group: commandissuerv1alpha1.GroupVersion.Group, + Kind: "Issuer", + }), + cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionDenied, + Status: cmmeta.ConditionTrue, + }), + cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{ + Type: cmapi.CertificateRequestConditionReady, + Status: cmmeta.ConditionUnknown, + }), + ), + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionTrue, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, + }, + }, + signerBuilder: newFakeSignerBuilder(nil, nil), + expectedCertificate: nil, + expectedReadyConditionStatus: cmmeta.ConditionFalse, + expectedReadyConditionReason: cmapi.CertificateRequestReasonDenied, + }, } scheme := runtime.NewScheme() - require.NoError(t, commandissuer.AddToScheme(scheme)) + require.NoError(t, commandissuerv1alpha1.AddToScheme(scheme)) require.NoError(t, cmapi.AddToScheme(scheme)) require.NoError(t, corev1.AddToScheme(scheme)) @@ -600,19 +743,24 @@ func TestCertificateRequestReconcile(t *testing.T) { fakeClient := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(tc.objects...). + WithStatusSubresource(tc.objects...). Build() controller := CertificateRequestReconciler{ Client: fakeClient, - ConfigClient: NewFakeConfigClient(fakeClient), Scheme: scheme, ClusterResourceNamespace: tc.clusterResourceNamespace, - SignerBuilder: tc.Builder, + SignerBuilder: tc.signerBuilder, CheckApprovedCondition: true, Clock: fixedClock, SecretAccessGrantedAtClusterLevel: true, } + if tc.expectedError != nil { + t.Logf("test %s - expected error: %s", name, tc.expectedError) + } else { + t.Logf("test %s - expected error: nil", name) + } result, err := controller.Reconcile( - ctrl.LoggerInto(context.TODO(), logrtesting.New(t)), + ctrl.LoggerInto(context.TODO(), logrtesting.NewTestLogger(t)), reconcile.Request{NamespacedName: tc.name}, ) if tc.expectedError != nil { @@ -631,10 +779,6 @@ func TestCertificateRequestReconcile(t *testing.T) { assertCertificateRequestHasReadyCondition(t, tc.expectedReadyConditionStatus, tc.expectedReadyConditionReason, &cr) } assert.Equal(t, tc.expectedCertificate, cr.Status.Certificate) - - if !apiequality.Semantic.DeepEqual(tc.expectedFailureTime, cr.Status.FailureTime) { - assert.Equal(t, tc.expectedFailureTime, cr.Status.FailureTime) - } } }) } @@ -662,3 +806,39 @@ func assertCertificateRequestHasReadyCondition(t *testing.T, status cmmeta.Condi assert.Contains(t, validReasons, reason, "unexpected condition reason") assert.Equal(t, reason, condition.Reason, "unexpected condition reason") } + +func issueTestCertificate(t *testing.T, cn string, parent *x509.Certificate, signingKey any) (*x509.Certificate, *ecdsa.PrivateKey) { + var err error + var key *ecdsa.PrivateKey + now := time.Now() + + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + publicKey := &key.PublicKey + signerPrivateKey := key + if signingKey != nil { + signerPrivateKey = signingKey.(*ecdsa.PrivateKey) + } + + serial, _ := rand.Int(rand.Reader, big.NewInt(1337)) + certTemplate := &x509.Certificate{ + Subject: pkix.Name{CommonName: cn}, + SerialNumber: serial, + BasicConstraintsValid: true, + IsCA: true, + NotBefore: now, + NotAfter: now.Add(time.Hour * 24), + } + + if parent == nil { + parent = certTemplate + } + + certData, err := x509.CreateCertificate(rand.Reader, certTemplate, parent, publicKey, signerPrivateKey) + require.NoError(t, err) + + cert, err := x509.ParseCertificate(certData) + require.NoError(t, err) + + return cert, key +} diff --git a/internal/controller/issuer_controller.go b/internal/controller/issuer_controller.go new file mode 100644 index 0000000..573c8fc --- /dev/null +++ b/internal/controller/issuer_controller.go @@ -0,0 +1,255 @@ +/* +Copyright © 2024 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + commandissuer "github.com/Keyfactor/command-cert-manager-issuer/api/v1alpha1" + "github.com/Keyfactor/command-cert-manager-issuer/internal/command" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + issuerReadyConditionReason = "command-issuer.IssuerController.Reconcile" + defaultHealthCheckInterval = time.Minute +) + +var ( + errGetAuthSecret = errors.New("failed to get Secret containing Issuer credentials") + errGetCaSecret = errors.New("caSecretName specified a name, but failed to get Secret containing CA certificate") + errHealthCheckerBuilder = errors.New("failed to build the healthchecker") + errHealthCheckerCheck = errors.New("healthcheck failed") +) + +// IssuerReconciler reconciles a Issuer object +type IssuerReconciler struct { + client.Client + Kind string + ClusterResourceNamespace string + SecretAccessGrantedAtClusterLevel bool + Scheme *runtime.Scheme + HealthCheckerBuilder command.HealthCheckerBuilder +} + +//+kubebuilder:rbac:groups=command-issuer.keyfactor.com,resources=issuers;clusterissuers,verbs=get;list;watch +//+kubebuilder:rbac:groups=command-issuer.keyfactor.com,resources=issuers/status;clusterissuers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=command-issuer.keyfactor.com,resources=issuers/finalizers,verbs=update + +// newIssuer returns a new Issuer or ClusterIssuer object +func (r *IssuerReconciler) newIssuer() (commandissuer.IssuerLike, error) { + issuerGVK := commandissuer.GroupVersion.WithKind(r.Kind) + ro, err := r.Scheme.New(issuerGVK) + if err != nil { + return nil, err + } + return ro.(commandissuer.IssuerLike), nil +} + +// Reconcile reconciles and updates the status of an Issuer or ClusterIssuer object +func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + log := ctrl.LoggerFrom(ctx) + + issuer, err := r.newIssuer() + if err != nil { + log.Error(err, "unrecognized issuer type") + return ctrl.Result{}, nil + } + if err := r.Get(ctx, req.NamespacedName, issuer); err != nil { + if err := client.IgnoreNotFound(err); err != nil { + return ctrl.Result{}, fmt.Errorf("unexpected get error: %v", err) + } + log.Info("Issuer not found. ignoring.") + return ctrl.Result{}, nil + } + + // Always attempt to update the Ready condition + defer func() { + if err != nil { + issuer.GetStatus().SetCondition(ctx, commandissuer.IssuerConditionReady, commandissuer.ConditionFalse, issuerReadyConditionReason, err.Error()) + } + if updateErr := r.Status().Update(ctx, issuer); updateErr != nil { + err = utilerrors.NewAggregate([]error{err, updateErr}) + result = ctrl.Result{} + } + }() + + var secretNamespace string + + switch { + case issuer.IsClusterScoped(): + secretNamespace = r.ClusterResourceNamespace + + case !issuer.IsClusterScoped(): + secretNamespace = req.Namespace + } + + // If SecretAccessGrantedAtClusterLevel is false, we always look for the Secret in the same namespace as the Issuer + if !r.SecretAccessGrantedAtClusterLevel { + secretNamespace = r.ClusterResourceNamespace + } + + config, err := commandConfigFromIssuer(ctx, r.Client, issuer, secretNamespace) + if err != nil { + return ctrl.Result{}, err + } + + checker, err := r.HealthCheckerBuilder(ctx, config) + if err != nil { + return ctrl.Result{}, fmt.Errorf("%w: %w", errHealthCheckerBuilder, err) + } + + err = checker.Check(ctx) + if err != nil { + return ctrl.Result{}, fmt.Errorf("%w: %w", errHealthCheckerCheck, err) + } + issuer.GetStatus().SetCondition(ctx, commandissuer.IssuerConditionReady, commandissuer.ConditionTrue, "Success", "Health check succeeded") + + metadataSupported, err := checker.CommandSupportsMetadata() + if err != nil { + return ctrl.Result{}, fmt.Errorf("%w: %w", errHealthCheckerCheck, err) + } + + switch { + case metadataSupported: + issuer.GetStatus().SetCondition(ctx, commandissuer.IssuerConditionSupportsMetadata, commandissuer.ConditionTrue, "Metadata fields are defined", "Connected Command platform has the Command Issuer metadata fields defined.") + default: + issuer.GetStatus().SetCondition(ctx, commandissuer.IssuerConditionSupportsMetadata, commandissuer.ConditionFalse, "Metadata fields are not defined", "Connected Command platform doesn't have the Command Issuer metadata fields defined.") + } + + return ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, nil +} + +func commandConfigFromIssuer(ctx context.Context, c client.Client, issuer commandissuer.IssuerLike, secretNamespace string) (*command.Config, error) { + log := ctrl.LoggerFrom(ctx) + + var basicAuth *command.BasicAuth + var oauth *command.OAuth + + // The SecretName is optional since the user may elect to use ambient credentials for scenarios like Workload Identity. + if issuer.GetSpec().SecretName != "" { + var authSecret corev1.Secret + err := c.Get(ctx, types.NamespacedName{ + Name: issuer.GetSpec().SecretName, + Namespace: secretNamespace, + }, &authSecret) + if err != nil { + return nil, fmt.Errorf("%w, secret name: %s, reason: %w", errGetAuthSecret, issuer.GetSpec().SecretName, err) + } + + switch { + case authSecret.Type == corev1.SecretTypeOpaque: + // We expect auth credentials for a client credential OAuth2.0 flow if the secret type is opaque + tokenURL, ok := authSecret.Data[commandissuer.OAuthTokenURLKey] + if !ok { + return nil, fmt.Errorf("%w: %s", errGetAuthSecret, "found secret with no tokenUrl") + } + clientID, ok := authSecret.Data[commandissuer.OAuthClientIDKey] + if !ok { + return nil, fmt.Errorf("%w: %s", errGetAuthSecret, "found secret with no clientId") + } + clientSecret, ok := authSecret.Data[commandissuer.OAuthClientSecretKey] + if !ok { + return nil, fmt.Errorf("%w: %s", errGetAuthSecret, "found secret with no clientSecret") + } + oauth = &command.OAuth{ + TokenURL: string(tokenURL), + ClientID: string(clientID), + ClientSecret: string(clientSecret), + } + scopes, ok := authSecret.Data[commandissuer.OAuthScopesKey] + if ok { + oauth.Scopes = strings.Split(string(scopes), ",") + } + audience, ok := authSecret.Data[commandissuer.OAuthAudienceKey] + if ok { + oauth.Audience = string(audience) + } + log.Info("found oauth client credentials in secret", "commandSecretName", issuer.GetSpec().SecretName, "type", authSecret.Type) + + case authSecret.Type == corev1.SecretTypeBasicAuth: + username, ok := authSecret.Data[corev1.BasicAuthUsernameKey] + if !ok { + return nil, fmt.Errorf("%w: %s", errGetAuthSecret, "found basic auth secret with no username") + } + password, ok := authSecret.Data[corev1.BasicAuthPasswordKey] + if !ok { + return nil, fmt.Errorf("%w: %s", errGetAuthSecret, "found basic auth secret with no password") + } + + basicAuth = &command.BasicAuth{ + Username: string(username), + Password: string(password), + } + log.Info("found basic auth credentials in secret", "commandSecretName", issuer.GetSpec().SecretName, "type", authSecret.Type) + + default: + return nil, fmt.Errorf("%w: %s", errGetAuthSecret, "found secret with unsupported type") + } + + } + + var caSecret corev1.Secret + // If the CA secret name is not specified, we will not attempt to retrieve it + if issuer.GetSpec().CaSecretName != "" { + err := c.Get(ctx, types.NamespacedName{ + Name: issuer.GetSpec().CaSecretName, + Namespace: secretNamespace, + }, &caSecret) + if err != nil { + return nil, fmt.Errorf("%w, secret name: %s, reason: %w", errGetCaSecret, issuer.GetSpec().CaSecretName, err) + } + } + + var caCertBytes []byte + // There is no requirement that the CA certificate is stored under a specific + // key in the secret, so we can just iterate over the map and effectively select + // the last value in the map + for _, bytes := range caSecret.Data { + caCertBytes = bytes + } + + return &command.Config{ + Hostname: issuer.GetSpec().Hostname, + APIPath: issuer.GetSpec().APIPath, + CaCertsBytes: caCertBytes, + BasicAuth: basicAuth, + OAuth: oauth, + AmbientCredentialScopes: strings.Split(issuer.GetSpec().Scopes, ","), + }, nil +} + +// SetupWithManager registers the IssuerReconciler with the controller manager. +// It configures controller-runtime to reconcile Keyfactor Command Issuers/ClusterIssuers in the cluster. +func (r *IssuerReconciler) SetupWithManager(mgr ctrl.Manager) error { + issuerType, err := r.newIssuer() + if err != nil { + return err + } + return ctrl.NewControllerManagedBy(mgr). + For(issuerType). + Complete(r) +} diff --git a/internal/controller/issuer_controller_test.go b/internal/controller/issuer_controller_test.go new file mode 100644 index 0000000..fe754c5 --- /dev/null +++ b/internal/controller/issuer_controller_test.go @@ -0,0 +1,621 @@ +/* +Copyright © 2024 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "errors" + "testing" + + commandissuerv1alpha1 "github.com/Keyfactor/command-cert-manager-issuer/api/v1alpha1" + "github.com/Keyfactor/command-cert-manager-issuer/internal/command" + logrtesting "github.com/go-logr/logr/testing" + "github.com/stretchr/testify/assert" + "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" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type fakeHealthChecker struct { + supportsMetadata bool + errCheck error +} + +func (f *fakeHealthChecker) Check(context.Context) error { + return f.errCheck +} + +func (f *fakeHealthChecker) CommandSupportsMetadata() (bool, error) { + return f.supportsMetadata, nil +} + +var newFakeHealthCheckerBuilder = func(builderErr error, checkerErr error, supportsMetadata bool) func(context.Context, *command.Config) (command.HealthChecker, error) { + return func(context.Context, *command.Config) (command.HealthChecker, error) { + return &fakeHealthChecker{ + errCheck: checkerErr, + }, builderErr + } +} + +func TestIssuerReconcile(t *testing.T) { + // caCert, rootKey := issueTestCertificate(t, "Root-CA", nil, nil) + // caCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}) + + // serverCert, _ := issueTestCertificate(t, "Server", caCert, rootKey) + // serverCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw}) + // caChain := append(serverCertPem, caCertPem...) + + type testCase struct { + kind string + name types.NamespacedName + objects []client.Object + healthCheckerBuilder command.HealthCheckerBuilder + clusterResourceNamespace string + expectedResult ctrl.Result + expectedError error + expectedReadyConditionStatus commandissuerv1alpha1.ConditionStatus + expectedMetadataSupportedConditionStatus commandissuerv1alpha1.ConditionStatus + } + + tests := map[string]testCase{ + "success-issuer-basicauth": { + kind: "Issuer", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, + }, + }, + healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, false), + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionTrue, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, + }, + "issuer-basicauth-no-username": { + kind: "Issuer", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + corev1.BasicAuthPasswordKey: []byte("password"), + }, + }, + }, + expectedError: errGetAuthSecret, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + "issuer-basicauth-no-password": { + kind: "Issuer", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + }, + }, + }, + expectedError: errGetAuthSecret, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + "success-clusterissuer-basicauth": { + kind: "ClusterIssuer", + name: types.NamespacedName{Name: "clusterissuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.ClusterIssuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterissuer1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "clusterissuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterissuer1-credentials", + Namespace: "kube-system", + }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, + }, + }, + healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, false), + clusterResourceNamespace: "kube-system", + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionTrue, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, + }, + "success-issuer-oauth": { + kind: "Issuer", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + commandissuerv1alpha1.OAuthTokenURLKey: []byte("https://dev.idp.com/oauth/token"), + commandissuerv1alpha1.OAuthClientIDKey: []byte("fi3ElQUVoBBHyRNt4mpUxG9WY65AOCcJ"), + commandissuerv1alpha1.OAuthClientSecretKey: []byte("1EXHdD7Ikmmv0OkBoJZZtzOG5iAzvwdqBVuvquf-QEvL6fLrEG_heJHphtEXVj9H"), + commandissuerv1alpha1.OAuthScopesKey: []byte("read:certificates,write:certificates"), + commandissuerv1alpha1.OAuthAudienceKey: []byte("https://command.example.com"), + }, + }, + }, + healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, false), + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionTrue, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, + }, + "issuer-oauth-no-tokenurl": { + kind: "Issuer", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + commandissuerv1alpha1.OAuthClientIDKey: []byte("fi3ElQUVoBBHyRNt4mpUxG9WY65AOCcJ"), + commandissuerv1alpha1.OAuthClientSecretKey: []byte("1EXHdD7Ikmmv0OkBoJZZtzOG5iAzvwdqBVuvquf-QEvL6fLrEG_heJHphtEXVj9H"), + commandissuerv1alpha1.OAuthScopesKey: []byte("read:certificates,write:certificates"), + commandissuerv1alpha1.OAuthAudienceKey: []byte("https://command.example.com"), + }, + }, + }, + expectedError: errGetAuthSecret, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + "issuer-oauth-no-clientid": { + kind: "Issuer", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + commandissuerv1alpha1.OAuthTokenURLKey: []byte("https://dev.idp.com/oauth/token"), + commandissuerv1alpha1.OAuthClientSecretKey: []byte("1EXHdD7Ikmmv0OkBoJZZtzOG5iAzvwdqBVuvquf-QEvL6fLrEG_heJHphtEXVj9H"), + commandissuerv1alpha1.OAuthScopesKey: []byte("read:certificates,write:certificates"), + commandissuerv1alpha1.OAuthAudienceKey: []byte("https://command.example.com"), + }, + }, + }, + expectedError: errGetAuthSecret, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + "issuer-oauth-no-clientsecret": { + kind: "Issuer", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + commandissuerv1alpha1.OAuthTokenURLKey: []byte("https://dev.idp.com/oauth/token"), + commandissuerv1alpha1.OAuthClientIDKey: []byte("fi3ElQUVoBBHyRNt4mpUxG9WY65AOCcJ"), + commandissuerv1alpha1.OAuthScopesKey: []byte("read:certificates,write:certificates"), + commandissuerv1alpha1.OAuthAudienceKey: []byte("https://command.example.com"), + }, + }, + }, + expectedError: errGetAuthSecret, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + "success-clusterissuer-oauth": { + kind: "ClusterIssuer", + name: types.NamespacedName{Name: "clusterissuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.ClusterIssuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterissuer1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "clusterissuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterissuer1-credentials", + Namespace: "kube-system", + }, + Data: map[string][]byte{ + commandissuerv1alpha1.OAuthTokenURLKey: []byte("https://dev.idp.com/oauth/token"), + commandissuerv1alpha1.OAuthClientIDKey: []byte("fi3ElQUVoBBHyRNt4mpUxG9WY65AOCcJ"), + commandissuerv1alpha1.OAuthClientSecretKey: []byte("1EXHdD7Ikmmv0OkBoJZZtzOG5iAzvwdqBVuvquf-QEvL6fLrEG_heJHphtEXVj9H"), + commandissuerv1alpha1.OAuthScopesKey: []byte("read:certificates,write:certificates"), + commandissuerv1alpha1.OAuthAudienceKey: []byte("https://command.example.com"), + }, + }, + }, + healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, false), + clusterResourceNamespace: "kube-system", + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionTrue, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedResult: ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, + }, + "issuer-kind-Unrecognized": { + kind: "UnrecognizedType", + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + }, + "issuer-not-found": { + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + }, + "issuer-missing-secret": { + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + }, + expectedError: errGetAuthSecret, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + "issuer-failing-healthchecker-builder": { + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, + }, + }, + healthCheckerBuilder: newFakeHealthCheckerBuilder(errors.New("simulated health checker builder error"), nil, false), + + expectedError: errHealthCheckerBuilder, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + "issuer-failing-healthchecker-check": { + name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, + objects: []client.Object{ + &commandissuerv1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1", + Namespace: "ns1", + }, + Spec: commandissuerv1alpha1.IssuerSpec{ + SecretName: "issuer1-credentials", + }, + Status: commandissuerv1alpha1.IssuerStatus{ + Conditions: []commandissuerv1alpha1.IssuerCondition{ + { + Type: commandissuerv1alpha1.IssuerConditionReady, + Status: commandissuerv1alpha1.ConditionUnknown, + }, + { + Type: commandissuerv1alpha1.IssuerConditionSupportsMetadata, + Status: commandissuerv1alpha1.ConditionFalse, + }, + }, + }, + }, + &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{ + Name: "issuer1-credentials", + Namespace: "ns1", + }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("username"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, + }, + }, + healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, errors.New("simulated health check error"), false), + expectedError: errHealthCheckerCheck, + expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse, + expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse, + }, + } + + scheme := runtime.NewScheme() + require.NoError(t, commandissuerv1alpha1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(tc.objects...). + WithStatusSubresource(tc.objects...). + Build() + if tc.kind == "" { + tc.kind = "Issuer" + } + controller := IssuerReconciler{ + Kind: tc.kind, + Client: fakeClient, + Scheme: scheme, + HealthCheckerBuilder: tc.healthCheckerBuilder, + ClusterResourceNamespace: tc.clusterResourceNamespace, + SecretAccessGrantedAtClusterLevel: true, + } + result, err := controller.Reconcile( + ctrl.LoggerInto(context.TODO(), logrtesting.NewTestLogger(t)), + reconcile.Request{NamespacedName: tc.name}, + ) + if tc.expectedError != nil { + assertErrorIs(t, tc.expectedError, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tc.expectedResult, result, "Unexpected result") + + if tc.expectedReadyConditionStatus != "" { + issuer, err := controller.newIssuer() + require.NoError(t, err) + require.NoError(t, fakeClient.Get(context.TODO(), tc.name, issuer)) + require.NoError(t, err) + assert.True(t, issuer.GetStatus().HasCondition(commandissuerv1alpha1.IssuerConditionReady, tc.expectedReadyConditionStatus)) + assert.True(t, issuer.GetStatus().HasCondition(commandissuerv1alpha1.IssuerConditionSupportsMetadata, tc.expectedMetadataSupportedConditionStatus)) + } + }) + } +} diff --git a/internal/controllers/fake_configclient_test.go b/internal/controllers/fake_configclient_test.go deleted file mode 100644 index a654654..0000000 --- a/internal/controllers/fake_configclient_test.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "github.com/Keyfactor/command-issuer/internal/issuer/util" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// FakeConfigClient is a fake implementation of the util.ConfigClient interface -// It forwards requests destined for the Kubernetes API server implemented by -// the util.ConfigClient interface to a fake Kubernetes API server implemented -// by the client.Client interface. - -// Force the compiler to check that FakeConfigClient implements the util.ConfigClient interface -var _ util.ConfigClient = &FakeConfigClient{} - -type FakeConfigClient struct { - client client.Client - ctx context.Context -} - -// NewFakeConfigClient uses the -func NewFakeConfigClient(fakeControllerRuntimeClient client.Client) util.ConfigClient { - return &FakeConfigClient{ - client: fakeControllerRuntimeClient, - } -} - -func (f *FakeConfigClient) SetContext(ctx context.Context) { - f.ctx = ctx -} - -func (f *FakeConfigClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error { - return f.client.Get(f.ctx, name, out) -} - -func (f *FakeConfigClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error { - return f.client.Get(f.ctx, name, out) -} diff --git a/internal/controllers/issuer_controller.go b/internal/controllers/issuer_controller.go deleted file mode 100644 index 9f96af0..0000000 --- a/internal/controllers/issuer_controller.go +++ /dev/null @@ -1,177 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "errors" - "fmt" - "github.com/Keyfactor/command-issuer/internal/issuer/signer" - issuerutil "github.com/Keyfactor/command-issuer/internal/issuer/util" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - "time" - - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - issuerReadyConditionReason = "command-issuer.IssuerController.Reconcile" - defaultHealthCheckInterval = time.Minute -) - -var ( - errGetAuthSecret = errors.New("failed to get Secret containing Issuer credentials") - errGetCaSecret = errors.New("caSecretName specified a name, but failed to get Secret containing CA certificate") - errHealthCheckerBuilder = errors.New("failed to build the healthchecker") - errHealthCheckerCheck = errors.New("healthcheck failed") -) - -// IssuerReconciler reconciles a Issuer object -type IssuerReconciler struct { - client.Client - ConfigClient issuerutil.ConfigClient - Kind string - ClusterResourceNamespace string - SecretAccessGrantedAtClusterLevel bool - Scheme *runtime.Scheme - HealthCheckerBuilder signer.HealthCheckerBuilder -} - -//+kubebuilder:rbac:groups=command-issuer.keyfactor.com,resources=issuers;clusterissuers,verbs=get;list;watch -//+kubebuilder:rbac:groups=command-issuer.keyfactor.com,resources=issuers/status;clusterissuers/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=command-issuer.keyfactor.com,resources=issuers/finalizers,verbs=update - -// newIssuer returns a new Issuer or ClusterIssuer object -func (r *IssuerReconciler) newIssuer() (client.Object, error) { - issuerGVK := commandissuer.GroupVersion.WithKind(r.Kind) - ro, err := r.Scheme.New(issuerGVK) - if err != nil { - return nil, err - } - return ro.(client.Object), nil -} - -// Reconcile reconciles and updates the status of an Issuer or ClusterIssuer object -func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { - log := ctrl.LoggerFrom(ctx) - - issuer, err := r.newIssuer() - if err != nil { - log.Error(err, "Unrecognized issuer type") - return ctrl.Result{}, nil - } - if err := r.Get(ctx, req.NamespacedName, issuer); err != nil { - if err := client.IgnoreNotFound(err); err != nil { - return ctrl.Result{}, fmt.Errorf("unexpected get error: %v", err) - } - log.Info("Not found. Ignoring.") - return ctrl.Result{}, nil - } - - issuerSpec, issuerStatus, err := issuerutil.GetSpecAndStatus(issuer) - if err != nil { - log.Error(err, "Unexpected error while getting issuer spec and status. Not retrying.") - return ctrl.Result{}, nil - } - - // Always attempt to update the Ready condition - defer func() { - if err != nil { - issuerutil.SetReadyCondition(issuerStatus, commandissuer.ConditionFalse, issuerReadyConditionReason, err.Error()) - } - if updateErr := r.Status().Update(ctx, issuer); updateErr != nil { - err = utilerrors.NewAggregate([]error{err, updateErr}) - result = ctrl.Result{} - } - }() - - if ready := issuerutil.GetReadyCondition(issuerStatus); ready == nil { - issuerutil.SetReadyCondition(issuerStatus, commandissuer.ConditionUnknown, issuerReadyConditionReason, "First seen") - return ctrl.Result{}, nil - } - - authSecretName := types.NamespacedName{ - Name: issuerSpec.SecretName, - } - - switch issuer.(type) { - case *commandissuer.Issuer: - authSecretName.Namespace = req.Namespace - case *commandissuer.ClusterIssuer: - authSecretName.Namespace = r.ClusterResourceNamespace - default: - log.Error(fmt.Errorf("unexpected issuer type: %t", issuer), "Not retrying.") - return ctrl.Result{}, nil - } - - // If SecretAccessGrantedAtClusterLevel is false, we always look for the Secret in the same namespace as the Issuer - if !r.SecretAccessGrantedAtClusterLevel { - authSecretName.Namespace = r.ClusterResourceNamespace - } - - // Set the context on the config client - r.ConfigClient.SetContext(ctx) - - var authSecret corev1.Secret - if err := r.ConfigClient.GetSecret(authSecretName, &authSecret); err != nil { - return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetAuthSecret, authSecretName, err) - } - - // Retrieve the CA certificate secret - caSecretName := types.NamespacedName{ - Name: issuerSpec.CaSecretName, - Namespace: authSecretName.Namespace, - } - - var caSecret corev1.Secret - if issuerSpec.CaSecretName != "" { - // If the CA secret name is not specified, we will not attempt to retrieve it - err = r.ConfigClient.GetSecret(caSecretName, &caSecret) - if err != nil { - return ctrl.Result{}, fmt.Errorf("%w, secret name: %s, reason: %v", errGetCaSecret, caSecretName, err) - } - } - - checker, err := r.HealthCheckerBuilder(ctx, issuerSpec, authSecret.Data, caSecret.Data) - if err != nil { - return ctrl.Result{}, fmt.Errorf("%w: %v", errHealthCheckerBuilder, err) - } - - if err := checker.Check(); err != nil { - return ctrl.Result{}, fmt.Errorf("%w: %v", errHealthCheckerCheck, err) - } - - issuerutil.SetReadyCondition(issuerStatus, commandissuer.ConditionTrue, issuerReadyConditionReason, "Success") - return ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, nil -} - -// SetupWithManager registers the IssuerReconciler with the controller manager. -// It configures controller-runtime to reconcile Keyfactor Command Issuers/ClusterIssuers in the cluster. -func (r *IssuerReconciler) SetupWithManager(mgr ctrl.Manager) error { - issuerType, err := r.newIssuer() - if err != nil { - return err - } - return ctrl.NewControllerManagedBy(mgr). - For(issuerType). - Complete(r) -} diff --git a/internal/controllers/issuer_controller_test.go b/internal/controllers/issuer_controller_test.go deleted file mode 100644 index 074b024..0000000 --- a/internal/controllers/issuer_controller_test.go +++ /dev/null @@ -1,293 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "errors" - "github.com/Keyfactor/command-issuer/internal/issuer/signer" - issuerutil "github.com/Keyfactor/command-issuer/internal/issuer/util" - logrtesting "github.com/go-logr/logr/testr" - "github.com/stretchr/testify/assert" - "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" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "testing" - - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" -) - -type fakeHealthChecker struct { - errCheck error -} - -func (o *fakeHealthChecker) Check() error { - return o.errCheck -} - -func TestIssuerReconcile(t *testing.T) { - type testCase struct { - kind string - name types.NamespacedName - objects []client.Object - healthCheckerBuilder signer.HealthCheckerBuilder - clusterResourceNamespace string - expectedResult ctrl.Result - expectedError error - expectedReadyConditionStatus commandissuer.ConditionStatus - } - - tests := map[string]testCase{ - "success-issuer": { - kind: "Issuer", - name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, - objects: []client.Object{ - &commandissuer.Issuer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1", - Namespace: "ns1", - }, - Spec: commandissuer.IssuerSpec{ - SecretName: "issuer1-credentials", - }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ - { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionUnknown, - }, - }, - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1-credentials", - Namespace: "ns1", - }, - }, - }, - healthCheckerBuilder: func(context.Context, *commandissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.HealthChecker, error) { - return &fakeHealthChecker{}, nil - }, - expectedReadyConditionStatus: commandissuer.ConditionTrue, - expectedResult: ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, - }, - "success-clusterissuer": { - kind: "ClusterIssuer", - name: types.NamespacedName{Name: "clusterissuer1"}, - objects: []client.Object{ - &commandissuer.ClusterIssuer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "clusterissuer1", - }, - Spec: commandissuer.IssuerSpec{ - SecretName: "clusterissuer1-credentials", - }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ - { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionUnknown, - }, - }, - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "clusterissuer1-credentials", - Namespace: "kube-system", - }, - }, - }, - healthCheckerBuilder: func(context.Context, *commandissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.HealthChecker, error) { - return &fakeHealthChecker{}, nil - }, - clusterResourceNamespace: "kube-system", - expectedReadyConditionStatus: commandissuer.ConditionTrue, - expectedResult: ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, - }, - "issuer-kind-Unrecognized": { - kind: "UnrecognizedType", - name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, - }, - "issuer-not-found": { - name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, - }, - "issuer-missing-ready-condition": { - name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, - objects: []client.Object{ - &commandissuer.Issuer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1", - Namespace: "ns1", - }, - }, - }, - expectedReadyConditionStatus: commandissuer.ConditionUnknown, - }, - "issuer-missing-secret": { - name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, - objects: []client.Object{ - &commandissuer.Issuer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1", - Namespace: "ns1", - }, - Spec: commandissuer.IssuerSpec{ - SecretName: "issuer1-credentials", - }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ - { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionUnknown, - }, - }, - }, - }, - }, - expectedError: errGetAuthSecret, - expectedReadyConditionStatus: commandissuer.ConditionFalse, - }, - "issuer-failing-healthchecker-builder": { - name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, - objects: []client.Object{ - &commandissuer.Issuer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1", - Namespace: "ns1", - }, - Spec: commandissuer.IssuerSpec{ - SecretName: "issuer1-credentials", - }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ - { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionUnknown, - }, - }, - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1-credentials", - Namespace: "ns1", - }, - }, - }, - healthCheckerBuilder: func(context.Context, *commandissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.HealthChecker, error) { - return nil, errors.New("simulated health checker builder error") - }, - expectedError: errHealthCheckerBuilder, - expectedReadyConditionStatus: commandissuer.ConditionFalse, - }, - "issuer-failing-healthchecker-check": { - name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"}, - objects: []client.Object{ - &commandissuer.Issuer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1", - Namespace: "ns1", - }, - Spec: commandissuer.IssuerSpec{ - SecretName: "issuer1-credentials", - }, - Status: commandissuer.IssuerStatus{ - Conditions: []commandissuer.IssuerCondition{ - { - Type: commandissuer.IssuerConditionReady, - Status: commandissuer.ConditionUnknown, - }, - }, - }, - }, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "issuer1-credentials", - Namespace: "ns1", - }, - }, - }, - healthCheckerBuilder: func(context.Context, *commandissuer.IssuerSpec, map[string][]byte, map[string][]byte) (signer.HealthChecker, error) { - return &fakeHealthChecker{errCheck: errors.New("simulated health check error")}, nil - }, - expectedError: errHealthCheckerCheck, - expectedReadyConditionStatus: commandissuer.ConditionFalse, - }, - } - - scheme := runtime.NewScheme() - require.NoError(t, commandissuer.AddToScheme(scheme)) - require.NoError(t, corev1.AddToScheme(scheme)) - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - fakeClient := fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects(tc.objects...). - Build() - if tc.kind == "" { - tc.kind = "Issuer" - } - controller := IssuerReconciler{ - Kind: tc.kind, - Client: fakeClient, - ConfigClient: NewFakeConfigClient(fakeClient), - Scheme: scheme, - HealthCheckerBuilder: tc.healthCheckerBuilder, - ClusterResourceNamespace: tc.clusterResourceNamespace, - SecretAccessGrantedAtClusterLevel: true, - } - result, err := controller.Reconcile( - ctrl.LoggerInto(context.TODO(), logrtesting.New(t)), - reconcile.Request{NamespacedName: tc.name}, - ) - if tc.expectedError != nil { - assertErrorIs(t, tc.expectedError, err) - } else { - assert.NoError(t, err) - } - - assert.Equal(t, tc.expectedResult, result, "Unexpected result") - - if tc.expectedReadyConditionStatus != "" { - issuer, err := controller.newIssuer() - require.NoError(t, err) - require.NoError(t, fakeClient.Get(context.TODO(), tc.name, issuer)) - _, issuerStatus, err := issuerutil.GetSpecAndStatus(issuer) - require.NoError(t, err) - assertIssuerHasReadyCondition(t, tc.expectedReadyConditionStatus, issuerStatus) - } - }) - } -} - -func assertIssuerHasReadyCondition(t *testing.T, status commandissuer.ConditionStatus, issuerStatus *commandissuer.IssuerStatus) { - condition := issuerutil.GetReadyCondition(issuerStatus) - if !assert.NotNil(t, condition, "Ready condition not found") { - return - } - assert.Equal(t, issuerReadyConditionReason, condition.Reason, "unexpected condition reason") - assert.Equal(t, status, condition.Status, "unexpected condition status") -} diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go deleted file mode 100644 index f01a64c..0000000 --- a/internal/controllers/suite_test.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "path/filepath" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" - //+kubebuilder:scaffold:imports -) - -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecs(t, "Controller Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - } - - var err error - // cfg is defined in this file globally. - cfg, err = testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - err = commandissuer.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:scheme - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) - -}) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) -}) diff --git a/internal/issuer/signer/signer.go b/internal/issuer/signer/signer.go deleted file mode 100644 index 2e41281..0000000 --- a/internal/issuer/signer/signer.go +++ /dev/null @@ -1,438 +0,0 @@ -/* -Copyright 2023 Keyfactor. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package signer - -import ( - "context" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" - "github.com/Keyfactor/keyfactor-go-client-sdk/api/keyfactor" - "math/rand" - "sigs.k8s.io/controller-runtime/pkg/log" - "strings" - "time" -) - -const ( - // Keyfactor enrollment PEM format - enrollmentPEMFormat = "PEM" - commandMetadataAnnotationPrefix = "metadata.command-issuer.keyfactor.com/" -) - -type K8sMetadata struct { - ControllerNamespace string - ControllerKind string - ControllerResourceGroupName string - IssuerName string - IssuerNamespace string - ControllerReconcileId string - CertificateSigningRequestNamespace string -} - -type commandSigner struct { - client *keyfactor.APIClient - certificateTemplate string - certificateAuthorityLogicalName string - certificateAuthorityHostname string - certManagerCertificateName string - customMetadata map[string]interface{} -} - -type HealthChecker interface { - Check() error -} - -type HealthCheckerBuilder func(context.Context, *commandissuer.IssuerSpec, map[string][]byte, map[string][]byte) (HealthChecker, error) -type CommandSignerBuilder func(context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (Signer, error) - -type Signer interface { - Sign(context.Context, []byte, K8sMetadata) ([]byte, []byte, error) -} - -// CommandHealthCheckerFromIssuerAndSecretData creates a new HealthChecker instance using the provided issuer spec and secret data -func CommandHealthCheckerFromIssuerAndSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, authSecretData map[string][]byte, caSecretData map[string][]byte) (HealthChecker, error) { - signer := commandSigner{} - - client, err := createCommandClientFromSecretData(ctx, spec, authSecretData, caSecretData) - if err != nil { - return nil, err - } - - signer.client = client - - return &signer, nil -} - -// CommandSignerFromIssuerAndSecretData is a wrapper for commandSignerFromIssuerAndSecretData that returns a Signer interface -// given the provided issuer spec and secret data -func CommandSignerFromIssuerAndSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, annotations map[string]string, authSecretData map[string][]byte, caSecretData map[string][]byte) (Signer, error) { - return commandSignerFromIssuerAndSecretData(ctx, spec, annotations, authSecretData, caSecretData) -} - -// commandSignerFromIssuerAndSecretData creates a new Signer instance using the provided issuer spec and secret data -func commandSignerFromIssuerAndSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, annotations map[string]string, authSecretData map[string][]byte, caSecretData map[string][]byte) (*commandSigner, error) { - k8sLog := log.FromContext(ctx) - - signer := commandSigner{} - - client, err := createCommandClientFromSecretData(ctx, spec, authSecretData, caSecretData) - if err != nil { - return nil, err - } - - signer.client = client - - if spec.CertificateTemplate == "" { - k8sLog.Error(errors.New("missing certificate template"), "missing certificate template") - return nil, errors.New("missing certificate template") - } - signer.certificateTemplate = spec.CertificateTemplate - - if spec.CertificateAuthorityLogicalName == "" { - k8sLog.Error(errors.New("missing certificate authority logical name"), "missing certificate authority logical name") - return nil, errors.New("missing certificate authority logical name") - } - signer.certificateAuthorityLogicalName = spec.CertificateAuthorityLogicalName - - // CA Hostname is optional - signer.certificateAuthorityHostname = spec.CertificateAuthorityHostname - - // Override defaults from annotations - if value, exists := annotations["command-issuer.keyfactor.com/certificateTemplate"]; exists { - signer.certificateTemplate = value - } - if value, exists := annotations["command-issuer.keyfactor.com/certificateAuthorityLogicalName"]; exists { - signer.certificateAuthorityLogicalName = value - } - if value, exists := annotations["command-issuer.keyfactor.com/certificateAuthorityHostname"]; exists { - signer.certificateAuthorityHostname = value - } - - if value, exists := annotations["command-manager.io/certificate-name"]; exists { - signer.certManagerCertificateName = value - } - - k8sLog.Info(fmt.Sprintf("Using certificate template %q and certificate authority %q (%s)", signer.certificateTemplate, signer.certificateAuthorityLogicalName, signer.certificateAuthorityHostname)) - - signer.customMetadata = extractMetadataFromAnnotations(annotations) - - return &signer, nil -} - -// extractMetadataFromAnnotations extracts metadata from the provided annotations -func extractMetadataFromAnnotations(annotations map[string]string) map[string]interface{} { - metadata := make(map[string]interface{}) - - for key, value := range annotations { - if strings.HasPrefix(key, commandMetadataAnnotationPrefix) { - metadata[strings.TrimPrefix(key, commandMetadataAnnotationPrefix)] = value - } - } - - return metadata -} - -// Check checks the health of the signer by verifying that the "POST /Enrollment/CSR" endpoint exists -func (s *commandSigner) Check() error { - endpoints, _, err := s.client.StatusApi.StatusGetEndpoints(context.Background()).Execute() - if err != nil { - detail := "failed to get endpoints from Keyfactor Command" - - var bodyError *keyfactor.GenericOpenAPIError - ok := errors.As(err, &bodyError) - if ok { - detail += fmt.Sprintf(" - %s", string(bodyError.Body())) - } - - detail += fmt.Sprintf(" (%s)", err.Error()) - - return errors.New(detail) - } - - for _, endpoint := range endpoints { - if strings.Contains(endpoint, "POST /Enrollment/CSR") { - return nil - } - } - - return errors.New("missing \"POST /Enrollment/CSR\" endpoint") -} - -// Sign signs the provided CSR using the Keyfactor Command API -func (s *commandSigner) Sign(ctx context.Context, csrBytes []byte, k8sMeta K8sMetadata) ([]byte, []byte, error) { - k8sLog := log.FromContext(ctx) - - csr, err := parseCSR(csrBytes) - if err != nil { - k8sLog.Error(err, "failed to parse CSR") - return nil, nil, err - } - - // Log the common metadata of the CSR - k8sLog.Info(fmt.Sprintf("Found CSR wtih Common Name %q and %d DNS SANs, %d IP SANs, and %d URI SANs", csr.Subject.CommonName, len(csr.DNSNames), len(csr.IPAddresses), len(csr.URIs))) - - // Print the SANs - for _, dnsName := range csr.DNSNames { - k8sLog.Info(fmt.Sprintf("DNS SAN: %s", dnsName)) - } - - for _, ipAddress := range csr.IPAddresses { - k8sLog.Info(fmt.Sprintf("IP SAN: %s", ipAddress.String())) - } - - for _, uri := range csr.URIs { - k8sLog.Info(fmt.Sprintf("URI SAN: %s", uri.String())) - } - - modelRequest := keyfactor.ModelsEnrollmentCSREnrollmentRequest{ - CSR: string(csrBytes), - IncludeChain: ptr(true), - Metadata: map[string]interface{}{ - CommandMetaControllerNamespace: k8sMeta.ControllerNamespace, - CommandMetaControllerKind: k8sMeta.ControllerKind, - CommandMetaControllerResourceGroupName: k8sMeta.ControllerResourceGroupName, - CommandMetaIssuerName: k8sMeta.IssuerName, - CommandMetaIssuerNamespace: k8sMeta.IssuerNamespace, - CommandMetaControllerReconcileId: k8sMeta.ControllerReconcileId, - CommandMetaCertificateSigningRequestNamespace: k8sMeta.CertificateSigningRequestNamespace, - }, - Template: &s.certificateTemplate, - SANs: nil, - } - - for metaName, value := range s.customMetadata { - k8sLog.Info(fmt.Sprintf("Adding metadata %q with value %q", metaName, value)) - modelRequest.Metadata[metaName] = value - } - - var caBuilder strings.Builder - if s.certificateAuthorityHostname != "" { - caBuilder.WriteString(s.certificateAuthorityHostname) - caBuilder.WriteString("\\") - } - caBuilder.WriteString(s.certificateAuthorityLogicalName) - - modelRequest.SetCertificateAuthority(caBuilder.String()) - modelRequest.SetTimestamp(time.Now()) - - commandCsrResponseObject, _, err := s.client.EnrollmentApi.EnrollmentPostCSREnroll(context.Background()).Request(modelRequest).XCertificateformat(enrollmentPEMFormat).Execute() - if err != nil { - detail := fmt.Sprintf("error enrolling certificate with Command. Verify that the certificate template %q exists and that the certificate authority %q (%s) is configured correctly.", s.certificateTemplate, s.certificateAuthorityLogicalName, s.certificateAuthorityHostname) - - if len(s.customMetadata) > 0 { - detail += " Also verify that the metadata fields provided exist in Command." - } - - var bodyError *keyfactor.GenericOpenAPIError - ok := errors.As(err, &bodyError) - if ok { - detail += fmt.Sprintf(" - %s", string(bodyError.Body())) - } - - k8sLog.Error(err, detail) - - return nil, nil, fmt.Errorf(detail) - } - - certAndChain, err := getCertificatesFromCertificateInformation(commandCsrResponseObject.CertificateInformation) - if err != nil { - return nil, nil, err - } - - k8sLog.Info(fmt.Sprintf("Successfully enrolled certificate with Command with subject %q. Certificate has %d SANs", certAndChain[0].Subject, len(certAndChain[0].DNSNames)+len(certAndChain[0].IPAddresses)+len(certAndChain[0].URIs))) - - // Return the certificate and chain in PEM format - return compileCertificatesToPemBytes(certAndChain) -} - -// getCertificatesFromCertificateInformation takes a keyfactor.ModelsPkcs10CertificateResponse object and -// returns a slice of x509 certificates -func getCertificatesFromCertificateInformation(commandResp *keyfactor.ModelsPkcs10CertificateResponse) ([]*x509.Certificate, error) { - var certBytes []byte - - for _, cert := range commandResp.Certificates { - block, _ := pem.Decode([]byte(cert)) - if block == nil { - return nil, errors.New("failed to parse certificate PEM") - } - - certBytes = append(certBytes, block.Bytes...) - } - - certs, err := x509.ParseCertificates(certBytes) - if err != nil { - return nil, err - } - - return certs, nil -} - -// compileCertificatesToPemString takes a slice of x509 certificates and returns a string containing the certificates in PEM format -// If an error occurred, the function logs the error and continues to parse the remaining objects. -func compileCertificatesToPemBytes(certificates []*x509.Certificate) ([]byte, []byte, error) { - var leaf strings.Builder - var chain strings.Builder - - for i, certificate := range certificates { - if i == 0 { - err := pem.Encode(&leaf, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certificate.Raw, - }) - if err != nil { - return make([]byte, 0), make([]byte, 0), err - } - } else { - err := pem.Encode(&chain, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certificate.Raw, - }) - if err != nil { - return make([]byte, 0), make([]byte, 0), err - } - } - } - - return []byte(leaf.String()), []byte(chain.String()), nil -} - -const ( - CommandMetaControllerNamespace = "Controller-Namespace" - CommandMetaControllerKind = "Controller-Kind" - CommandMetaControllerResourceGroupName = "Controller-Resource-Group-Name" - CommandMetaIssuerName = "Issuer-Name" - CommandMetaIssuerNamespace = "Issuer-Namespace" - CommandMetaControllerReconcileId = "Controller-Reconcile-Id" - CommandMetaCertificateSigningRequestNamespace = "Certificate-Signing-Request-Namespace" -) - -// createCommandClientFromSecretData creates a new Keyfactor Command client using the provided issuer spec and secret data -func createCommandClientFromSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, authSecretData map[string][]byte, caSecretData map[string][]byte) (*keyfactor.APIClient, error) { - k8sLogger := log.FromContext(ctx) - - // Get username and password from secretData which contains key value pairs of a kubernetes.io/basic-auth secret - username := string(authSecretData["username"]) - if username == "" { - k8sLogger.Error(errors.New("missing username"), "missing username") - return nil, errors.New("missing username") - } - password := string(authSecretData["password"]) - if password == "" { - k8sLogger.Error(errors.New("missing password"), "missing password") - return nil, errors.New("missing password") - } - - keyfactorConfig := make(map[string]string) - - // Set username and password for the Keyfactor client - for key, value := range authSecretData { - keyfactorConfig[key] = string(value) - } - // Set the hostname for the Keyfactor client - keyfactorConfig["host"] = spec.Hostname - - config := keyfactor.NewConfiguration(keyfactorConfig) - if config == nil { - k8sLogger.Error(errors.New("failed to create Keyfactor configuration"), "failed to create Keyfactor configuration") - return nil, errors.New("failed to create Keyfactor configuration") - } - - // Set the user agent for the Keyfactor client - config.UserAgent = "command-issuer" - - // If the CA certificate is provided, add it to the EJBCA configuration - if len(caSecretData) > 0 { - // There is no requirement that the CA certificate is stored under a specific key in the secret, so we can just iterate over the map - var caCertBytes []byte - for _, caCertBytes = range caSecretData { - } - - // Try to decode caCertBytes as a PEM formatted block - caChainBlocks, _ := decodePEMBytes(caCertBytes) - if caChainBlocks != nil { - var caChain []*x509.Certificate - for _, block := range caChainBlocks { - // Parse the PEM block into an x509 certificate - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, err - } - - caChain = append(caChain, cert) - } - - config.SetCaCertificates(caChain) - } - } - - client := keyfactor.NewAPIClient(config) - if client == nil { - k8sLogger.Error(errors.New("failed to create Keyfactor client"), "failed to create Keyfactor client") - return nil, errors.New("failed to create Keyfactor client") - } - - k8sLogger.Info("Created Keyfactor Command client") - - return client, nil -} - -// decodePEMBytes takes a byte array containing PEM encoded data and returns a slice of PEM blocks and a private key PEM block -func decodePEMBytes(buf []byte) ([]*pem.Block, *pem.Block) { - var privKey *pem.Block - var certificates []*pem.Block - var block *pem.Block - for { - block, buf = pem.Decode(buf) - if block == nil { - break - } else if strings.Contains(block.Type, "PRIVATE KEY") { - privKey = block - } else { - certificates = append(certificates, block) - } - } - return certificates, privKey -} - -// parseCSR takes a byte array containing a PEM encoded CSR and returns a x509.CertificateRequest object -func parseCSR(pemBytes []byte) (*x509.CertificateRequest, error) { - // extract PEM from request object - block, _ := pem.Decode(pemBytes) - if block == nil || block.Type != "CERTIFICATE REQUEST" { - return nil, errors.New("PEM block type must be CERTIFICATE REQUEST") - } - return x509.ParseCertificateRequest(block.Bytes) -} - -// generateRandomString generates a random string of the specified length -func generateRandomString(length int) string { - rand.Seed(time.Now().UnixNano()) - letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, length) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) -} - -// ptr returns a pointer to the provided value -func ptr[T any](v T) *T { - return &v -} diff --git a/internal/issuer/signer/signer_test.go b/internal/issuer/signer/signer_test.go deleted file mode 100644 index 4ff4869..0000000 --- a/internal/issuer/signer/signer_test.go +++ /dev/null @@ -1,559 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package signer - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/pem" - "fmt" - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" - "github.com/Keyfactor/keyfactor-go-client-sdk/api/keyfactor" - "github.com/stretchr/testify/assert" - "math/big" - "os" - "reflect" - "strings" - "testing" - "time" -) - -type testSigner struct { - SignerBuilder CommandSignerBuilder - HealthCheckerBuilder HealthCheckerBuilder -} - -func TestCommandHealthCheckerFromIssuerAndSecretData(t *testing.T) { - obj := testSigner{ - HealthCheckerBuilder: CommandHealthCheckerFromIssuerAndSecretData, - } - - builder, err := obj.HealthCheckerBuilder(getTestHealthCheckerConfigItems(t)) - if err != nil { - t.Fatal(err) - } - - err = builder.Check() - if err != nil { - t.Fatal(err) - } -} - -func TestCommandSignerFromIssuerAndSecretData(t *testing.T) { - t.Run("ValidSigning", func(t *testing.T) { - obj := testSigner{ - SignerBuilder: CommandSignerFromIssuerAndSecretData, - } - - // Generate a test CSR to sign - csr, err := generateCSR("C=US,ST=California,L=San Francisco,O=Keyfactor,OU=Engineering,CN=example.com") - if err != nil { - t.Fatal(err) - } - - meta := K8sMetadata{ - ControllerNamespace: "test-namespace", - ControllerKind: "Issuer", - ControllerResourceGroupName: "test-issuer.example.com", - IssuerName: "test-issuer", - IssuerNamespace: "test-namespace", - ControllerReconcileId: "GUID", - CertificateSigningRequestNamespace: "test-namespace", - } - - start := time.Now() - signer, err := obj.SignerBuilder(getTestSignerConfigItems(t)) - if err != nil { - t.Fatal(err) - } - - leaf, chain, err := signer.Sign(context.Background(), csr, meta) - if err != nil { - t.Fatal(err) - } - t.Logf("Signing took %s", time.Since(start)) - - t.Logf("Signed certificate: %s", string(leaf)) - t.Logf("Chain: %s", string(chain)) - }) - - // Set up test data - - spec := commandissuer.IssuerSpec{ - Hostname: "example-hostname.com", - CertificateTemplate: "example-template", - CertificateAuthorityLogicalName: "example-logical-name", - CertificateAuthorityHostname: "ca-hostname.com", - SecretName: "example-secret-name", - CaSecretName: "example-ca-secret-name", - } - - authSecretData := map[string][]byte{ - "username": []byte("username"), - "password": []byte("password"), - } - - caSecretData := map[string][]byte{ - "tls.crt": []byte("ca-cert"), - } - - t.Run("MissingCertTemplate", func(t *testing.T) { - templateCopy := spec.CertificateTemplate - spec.CertificateTemplate = "" - // Create the signer - _, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, make(map[string]string), authSecretData, caSecretData) - if err == nil { - t.Errorf("expected error, got nil") - } - - spec.CertificateTemplate = templateCopy - }) - - t.Run("MissingCaLogicalName", func(t *testing.T) { - logicalNameCopy := spec.CertificateAuthorityLogicalName - spec.CertificateAuthorityLogicalName = "" - // Create the signer - _, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, make(map[string]string), authSecretData, caSecretData) - if err == nil { - t.Errorf("expected error, got nil") - } - - spec.CertificateAuthorityLogicalName = logicalNameCopy - }) - - t.Run("NoAnnotations", func(t *testing.T) { - // Create the signer - signer, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, make(map[string]string), authSecretData, caSecretData) - if err != nil { - t.Fatal(err) - } - - // If there are no annotations, the customMetadata map should be empty - if len(signer.customMetadata) != 0 { - t.Errorf("expected customMetadata to be empty, got %v", signer.customMetadata) - } - }) - - t.Run("MetadataAnnotations", func(t *testing.T) { - annotations := map[string]string{ - commandMetadataAnnotationPrefix + "key1": "value1", - commandMetadataAnnotationPrefix + "key2": "value2", - } - - // Create the signer - signer, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, annotations, authSecretData, caSecretData) - if err != nil { - t.Fatal(err) - } - - // If there are no annotations, the customMetadata map should be empty - if len(signer.customMetadata) != 2 { - t.Errorf("expected customMetadata to have 2 entries, got %v", signer.customMetadata) - } - - if value, ok := signer.customMetadata["key1"].(string); ok && value == "value1" { - // They are equal - } else { - t.Errorf("expected customMetadata key1 to be value1, got %v", signer.customMetadata["key1"]) - } - - if value, ok := signer.customMetadata["key2"].(string); ok && value == "value2" { - // They are equal - } else { - t.Errorf("expected customMetadata key1 to be value1, got %v", signer.customMetadata["key1"]) - } - }) - - t.Run("AnnotationDefaultOverrides", func(t *testing.T) { - annotations := map[string]string{ - "command-issuer.keyfactor.com/certificateTemplate": "TestCertificateTemplate", - "command-issuer.keyfactor.com/certificateAuthorityLogicalName": "TestCertificateAuthorityLogicalName", - "command-issuer.keyfactor.com/certificateAuthorityHostname": "TestCertificateAuthorityHostname", - "command-manager.io/certificate-name": "TestCertificateName", - } - - // Create the signer - signer, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, annotations, authSecretData, caSecretData) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, "TestCertificateTemplate", signer.certificateTemplate) - assert.Equal(t, "TestCertificateAuthorityLogicalName", signer.certificateAuthorityLogicalName) - assert.Equal(t, "TestCertificateAuthorityHostname", signer.certificateAuthorityHostname) - assert.Equal(t, "TestCertificateName", signer.certManagerCertificateName) - }) -} - -func TestCompileCertificatesToPemBytes(t *testing.T) { - // Generate two certificates for testing - cert1, err := generateSelfSignedCertificate() - if err != nil { - t.Fatalf("failed to generate mock certificate: %v", err) - } - cert2, err := generateSelfSignedCertificate() - if err != nil { - t.Fatalf("failed to generate mock certificate: %v", err) - } - - tests := []struct { - name string - certificates []*x509.Certificate - expectedError bool - }{ - { - name: "No certificates", - certificates: []*x509.Certificate{}, - expectedError: false, - }, - { - name: "Single certificate", - certificates: []*x509.Certificate{cert1}, - expectedError: false, - }, - { - name: "Multiple certificates", - certificates: []*x509.Certificate{cert1, cert2}, - expectedError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, _, err = compileCertificatesToPemBytes(tt.certificates) - if (err != nil) != tt.expectedError { - t.Errorf("expected error = %v, got %v", tt.expectedError, err) - } - }) - } -} - -func Test_extractMetadataFromAnnotations(t *testing.T) { - tests := []struct { - name string - annotations map[string]string - expected map[string]interface{} - }{ - { - name: "empty annotations", - annotations: map[string]string{}, - expected: map[string]interface{}{}, - }, - { - name: "annotations without metadata prefix", - annotations: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - expected: map[string]interface{}{}, - }, - { - name: "annotations with metadata prefix", - annotations: map[string]string{ - commandMetadataAnnotationPrefix + "key1": "value1", - "key2": "value2", - }, - expected: map[string]interface{}{ - "key1": "value1", - }, - }, - { - name: "mixed annotations", - annotations: map[string]string{ - commandMetadataAnnotationPrefix + "key1": "value1", - commandMetadataAnnotationPrefix + "key2": "value2", - "key3": "value3", - }, - expected: map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := extractMetadataFromAnnotations(tt.annotations) - if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("expected %v, got %v", tt.expected, result) - } - }) - } -} - -func Test_createCommandClientFromSecretData(t *testing.T) { - cert1, err := generateSelfSignedCertificate() - if err != nil { - t.Fatalf("failed to generate self-signed certificate: %v", err) - } - - leafBytes, _, err := compileCertificatesToPemBytes([]*x509.Certificate{cert1}) - if err != nil { - return - } - - tests := []struct { - name string - spec commandissuer.IssuerSpec - authSecretData map[string][]byte - caSecretData map[string][]byte - verify func(*testing.T, *keyfactor.APIClient) error - expectedErr bool - }{ - { - name: "EmptySecretData", - authSecretData: map[string][]byte{ - "username": []byte(""), - "password": []byte(""), - }, - verify: func(t *testing.T, client *keyfactor.APIClient) error { - if client != nil { - return fmt.Errorf("expected client to be nil") - } - return nil - }, - expectedErr: true, - }, - { - name: "ValidAuthData", - spec: commandissuer.IssuerSpec{ - Hostname: "hostname", - }, - authSecretData: map[string][]byte{ - "username": []byte("username"), - "password": []byte("password"), - }, - verify: func(t *testing.T, client *keyfactor.APIClient) error { - if client == nil { - return fmt.Errorf("expected client to be non-nil") - } - - if client.GetConfig().Host != "hostname" { - return fmt.Errorf("expected hostname to be hostname, got %s", client.GetConfig().Host) - } - - if client.GetConfig().BasicAuth.UserName != "username" { - return fmt.Errorf("expected username to be username, got %s", client.GetConfig().BasicAuth.UserName) - } - - if client.GetConfig().BasicAuth.Password != "password" { - return fmt.Errorf("expected password to be password, got %s", client.GetConfig().BasicAuth.Password) - } - - return nil - }, - expectedErr: false, - }, - { - name: "InvalidCaData", - spec: commandissuer.IssuerSpec{ - Hostname: "hostname", - }, - authSecretData: map[string][]byte{ - "username": []byte("username"), - "password": []byte("password"), - }, - caSecretData: map[string][]byte{ - "tls.crt": leafBytes, - }, - verify: func(t *testing.T, client *keyfactor.APIClient) error { - if client == nil { - return fmt.Errorf("expected client to be non-nil") - } - - return nil - }, - expectedErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := createCommandClientFromSecretData(context.Background(), &tt.spec, tt.authSecretData, tt.caSecretData) - if (err != nil) != tt.expectedErr { - t.Errorf("expected error = %v, got %v", tt.expectedErr, err) - } - if err = tt.verify(t, result); err != nil { - t.Error(err) - } - }) - } -} - -func getTestHealthCheckerConfigItems(t *testing.T) (context.Context, *commandissuer.IssuerSpec, map[string][]byte, map[string][]byte) { - ctx, spec, _, secret, configmap := getTestSignerConfigItems(t) - return ctx, spec, secret, configmap -} - -func getTestSignerConfigItems(t *testing.T) (context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) { - // Get the username and password from the environment - secretData := make(map[string][]byte) - username := os.Getenv("COMMAND_USERNAME") - if username == "" { - t.Fatal("COMMAND_USERNAME must be set to run this test") - } - secretData["username"] = []byte(username) - - password := os.Getenv("COMMAND_PASSWORD") - if password == "" { - t.Fatal("COMMAND_PASSWORD must be set to run this test") - } - secretData["password"] = []byte(password) - - // Get the hostname, certificate template, and certificate authority from the environment - spec := commandissuer.IssuerSpec{} - hostname := os.Getenv("COMMAND_HOSTNAME") - if hostname == "" { - t.Fatal("COMMAND_HOSTNAME must be set to run this test") - } - spec.Hostname = hostname - - certificateTemplate := os.Getenv("COMMAND_CERTIFICATE_TEMPLATE") - if certificateTemplate == "" { - t.Fatal("COMMAND_CERTIFICATE_TEMPLATE must be set to run this test") - } - spec.CertificateTemplate = certificateTemplate - - certificateAuthorityLogicalName := os.Getenv("COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME") - if certificateAuthorityLogicalName == "" { - t.Fatal("COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME must be set to run this test") - } - spec.CertificateAuthorityLogicalName = certificateAuthorityLogicalName - - certificateAuthorityHostname := os.Getenv("COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME") - if certificateAuthorityHostname == "" { - t.Fatal("COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME must be set to run this test") - } - spec.CertificateAuthorityHostname = certificateAuthorityHostname - - // Get the certificate authority path from the environment - pathToCaCert := os.Getenv("COMMAND_CA_CERT_PATH") - - // Read the CA cert from the file system. - caCertBytes, err := os.ReadFile(pathToCaCert) - if err != nil { - t.Log("CA cert not found, assuming that Command is using a trusted CA") - } - - caSecretData := map[string][]byte{} - if len(caCertBytes) != 0 { - caSecretData["tls.crt"] = caCertBytes - } - - return context.Background(), &spec, make(map[string]string), secretData, caSecretData -} - -func generateCSR(subject string) ([]byte, error) { - keyBytes, _ := rsa.GenerateKey(rand.Reader, 2048) - - subj, err := parseSubjectDN(subject, false) - if err != nil { - return make([]byte, 0), err - } - - template := x509.CertificateRequest{ - Subject: subj, - SignatureAlgorithm: x509.SHA256WithRSA, - DNSNames: []string{subj.CommonName}, - } - var csrBuf bytes.Buffer - csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &template, keyBytes) - err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) - if err != nil { - return make([]byte, 0), err - } - - return csrBuf.Bytes(), nil -} - -// Function that turns subject string into pkix.Name -// EG "C=US,ST=California,L=San Francisco,O=HashiCorp,OU=Engineering,CN=example.com" -func parseSubjectDN(subject string, randomizeCn bool) (pkix.Name, error) { - var name pkix.Name - - // Split the subject into its individual parts - parts := strings.Split(subject, ",") - - for _, part := range parts { - // Split the part into key and value - keyValue := strings.SplitN(part, "=", 2) - - if len(keyValue) != 2 { - return pkix.Name{}, asn1.SyntaxError{Msg: "malformed subject DN"} - } - - key := strings.TrimSpace(keyValue[0]) - value := strings.TrimSpace(keyValue[1]) - - // Map the key to the appropriate field in the pkix.Name struct - switch key { - case "C": - name.Country = []string{value} - case "ST": - name.Province = []string{value} - case "L": - name.Locality = []string{value} - case "O": - name.Organization = []string{value} - case "OU": - name.OrganizationalUnit = []string{value} - case "CN": - if randomizeCn { - name.CommonName = fmt.Sprintf("%s-%s", value, generateRandomString(5)) - } else { - name.CommonName = value - } - default: - // Ignore any unknown keys - } - } - - return name, nil -} - -func generateSelfSignedCertificate() (*x509.Certificate, error) { - priv, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{CommonName: "test"}, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - } - - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) - if err != nil { - return nil, err - } - - cert, err := x509.ParseCertificate(certDER) - if err != nil { - return nil, err - } - - return cert, nil -} diff --git a/internal/issuer/util/configclient.go b/internal/issuer/util/configclient.go deleted file mode 100644 index db86470..0000000 --- a/internal/issuer/util/configclient.go +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "context" - "fmt" - authv1 "k8s.io/api/authorization/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" - "k8s.io/klog/v2" - ctrl "sigs.k8s.io/controller-runtime" -) - -// ConfigClient is an interface for a K8s REST client. -type ConfigClient interface { - SetContext(ctx context.Context) - GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error - GetSecret(name types.NamespacedName, out *corev1.Secret) error -} - -type configClient struct { - ctx context.Context - logger klog.Logger - client kubernetes.Interface - accessCache map[string]bool - - verifyAccessFunc func(apiResource string, resource types.NamespacedName) error -} - -// NewConfigClient creates a new K8s REST client using the configuration from the controller-runtime. -func NewConfigClient(ctx context.Context) (ConfigClient, error) { - config := ctrl.GetConfigOrDie() - - // Create the clientset - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create clientset: %w", err) - } - - client := &configClient{ - client: clientset, - accessCache: make(map[string]bool), - ctx: ctx, - logger: klog.NewKlogr(), - } - - client.verifyAccessFunc = client.verifyAccessToResource - - return client, nil -} - -// SetContext sets the context for the client. -func (c *configClient) SetContext(ctx context.Context) { - c.ctx = ctx - c.logger = klog.FromContext(ctx) -} - -// verifyAccessToResource verifies that the client has access to a given resource in a given namespace -// by creating a SelfSubjectAccessReview. This is done to avoid errors when the client does not have -// access to the resource. -func (c *configClient) verifyAccessToResource(apiResource string, resource types.NamespacedName) error { - verbs := []string{"get", "list", "watch"} - - for _, verb := range verbs { - ssar := &authv1.SelfSubjectAccessReview{ - Spec: authv1.SelfSubjectAccessReviewSpec{ - ResourceAttributes: &authv1.ResourceAttributes{ - Name: resource.Name, - Namespace: resource.Namespace, - - Group: "", - Resource: apiResource, - Verb: verb, - }, - }, - } - - ssar, err := c.client.AuthorizationV1().SelfSubjectAccessReviews().Create(c.ctx, ssar, metav1.CreateOptions{}) - if err != nil { - return fmt.Errorf("failed to create SelfSubjectAccessReview to check access to %s for verb %q: %w", apiResource, verb, err) - } - - if !ssar.Status.Allowed { - return fmt.Errorf("client does not have access to %s called %q for verb %q, reason: %v", apiResource, resource.String(), verb, ssar.Status.String()) - } - } - - c.logger.Info(fmt.Sprintf("Client has access to %s called %q", apiResource, resource.String())) - - return nil -} - -// GetConfigMap gets the configmap with the given name and namespace and copies it into the out parameter. -func (c *configClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error { - if c == nil { - return fmt.Errorf("config client is nil") - } - - // Check if the client has access to the configmap resource - if _, ok := c.accessCache[name.String()]; !ok { - // If this is the first time the client is accessing the resource and it does have - // permission, add it to the access cache so that it does not need to be checked again. - err := c.verifyAccessFunc("configmaps", name) - if err != nil { - return err - } - c.accessCache[name.String()] = true - } - - // Get the configmap - configmap, err := c.client.CoreV1().ConfigMaps(name.Namespace).Get(c.ctx, name.Name, metav1.GetOptions{}) - if err != nil { - return err - } - - // Copy the configmap into the out parameter - configmap.DeepCopyInto(out) - return nil -} - -// GetSecret gets the secret with the given name and namespace and copies it into the out parameter. -func (c *configClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error { - if c == nil { - return fmt.Errorf("config client is nil") - } - - // Check if the client has access to the secret resource - if _, ok := c.accessCache[name.String()]; !ok { - // If this is the first time the client is accessing the resource and it does have - // permission, add it to the access cache so that it does not need to be checked again. - err := c.verifyAccessFunc("secrets", name) - if err != nil { - return err - } - c.accessCache[name.String()] = true - } - - // Get the secret - secret, err := c.client.CoreV1().Secrets(name.Namespace).Get(c.ctx, name.Name, metav1.GetOptions{}) - if err != nil { - return err - } - - // Copy the secret into the out parameter - secret.DeepCopyInto(out) - return nil -} diff --git a/internal/issuer/util/configclient_test.go b/internal/issuer/util/configclient_test.go deleted file mode 100644 index 5c30aad..0000000 --- a/internal/issuer/util/configclient_test.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "context" - logrtesting "github.com/go-logr/logr/testr" - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/fake" - ctrl "sigs.k8s.io/controller-runtime" - "testing" -) - -func TestConfigClient(t *testing.T) { - var err error - - // Define namespaced names for test objects - configMapName := types.NamespacedName{Name: "test-configmap", Namespace: "default"} - secretName := types.NamespacedName{Name: "test-secret", Namespace: "default"} - - // Create and inject fake ConfigMap - testConfigMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: configMapName.Name, Namespace: configMapName.Namespace}, - } - - // Create and inject fake Secret - testSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: secretName.Name, Namespace: secretName.Namespace}, - } - - // Create a fake clientset with the test objects - clientset := fake.NewSimpleClientset([]runtime.Object{ - testConfigMap, - testSecret, - }...) - - // We can't test NewConfigClient unless we can mock ctrl.GetConfigOrDie() and kubernetes.NewForConfig() - // So we'll just test the methods that use the clientset - - // Create a ConfigClient - client := &configClient{ - client: clientset, - accessCache: make(map[string]bool), - } - - // The fake client doesn't implement authorization.k8s.io/v1 SelfSubjectAccessReview - // So we'll mock the verifyAccessFunc - client.verifyAccessFunc = func(apiResource string, resource types.NamespacedName) error { - return nil - } - - // Setup logging for test environment by setting the context - client.SetContext(ctrl.LoggerInto(context.TODO(), logrtesting.New(t))) - - t.Run("GetConfigMap", func(t *testing.T) { - // Test GetConfigMap - var out corev1.ConfigMap - err = client.GetConfigMap(configMapName, &out) - assert.NoError(t, err) - assert.Equal(t, testConfigMap, &out) - }) - - t.Run("GetSecret", func(t *testing.T) { - // Test GetSecret - var out corev1.Secret - err = client.GetSecret(secretName, &out) - assert.NoError(t, err) - assert.Equal(t, testSecret, &out) - }) -} diff --git a/internal/issuer/util/util.go b/internal/issuer/util/util.go deleted file mode 100644 index 4d513c2..0000000 --- a/internal/issuer/util/util.go +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright © 2023 Keyfactor - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "errors" - "fmt" - "os" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" -) - -const inClusterNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" - -// GetSpecAndStatus is a helper function that returns the Spec and Status of an Issuer object. -func GetSpecAndStatus(issuer client.Object) (*commandissuer.IssuerSpec, *commandissuer.IssuerStatus, error) { - switch t := issuer.(type) { - case *commandissuer.Issuer: - return &t.Spec, &t.Status, nil - case *commandissuer.ClusterIssuer: - return &t.Spec, &t.Status, nil - default: - return nil, nil, fmt.Errorf("not an issuer type: %t", t) - } -} - -// SetReadyCondition is a helper function that sets the Ready condition on an IssuerStatus. -func SetReadyCondition(status *commandissuer.IssuerStatus, conditionStatus commandissuer.ConditionStatus, reason, message string) { - ready := GetReadyCondition(status) - if ready == nil { - ready = &commandissuer.IssuerCondition{ - Type: commandissuer.IssuerConditionReady, - } - status.Conditions = append(status.Conditions, *ready) - } - if ready.Status != conditionStatus { - ready.Status = conditionStatus - now := metav1.Now() - ready.LastTransitionTime = &now - } - ready.Reason = reason - ready.Message = message - - for i, c := range status.Conditions { - if c.Type == commandissuer.IssuerConditionReady { - status.Conditions[i] = *ready - return - } - } -} - -// GetReadyCondition is a helper function that returns the Ready condition from an IssuerStatus. -func GetReadyCondition(status *commandissuer.IssuerStatus) *commandissuer.IssuerCondition { - for _, c := range status.Conditions { - if c.Type == commandissuer.IssuerConditionReady { - return &c - } - } - return nil -} - -// IsReady is a helper function that returns true if the Ready condition is set to True. -func IsReady(status *commandissuer.IssuerStatus) bool { - if c := GetReadyCondition(status); c != nil { - return c.Status == commandissuer.ConditionTrue - } - return false -} - -var ErrNotInCluster = errors.New("not running in-cluster") - -// Copied from controller-runtime/pkg/leaderelection -func GetInClusterNamespace() (string, error) { - // Check whether the namespace file exists. - // If not, we are not running in cluster so can't guess the namespace. - _, err := os.Stat(inClusterNamespacePath) - if os.IsNotExist(err) { - return "", ErrNotInCluster - } else if err != nil { - return "", fmt.Errorf("error checking namespace file: %w", err) - } - - // Load the namespace file and return its content - namespace, err := os.ReadFile(inClusterNamespacePath) - if err != nil { - return "", fmt.Errorf("error reading namespace file: %w", err) - } - return string(namespace), nil -}