From 6047d932e90b11b5e567ff183f57865e4512cb8a Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 12:13:56 -0700 Subject: [PATCH 01/18] 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 -} From c5c4544e9f3c7e337902b59855310816eb3435d1 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 12:17:41 -0700 Subject: [PATCH 02/18] chore(ci): Test code before running bootstrap workflow Signed-off-by: Hayden Roszell --- .../keyfactor-bootstrap-workflow.yml | 36 ++++++++++++++++ .github/workflows/test.yml | 42 ------------------- 2 files changed, 36 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index 573db7e..f990d96 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -10,8 +10,44 @@ on: - 'release-*.*' jobs: + + build: + name: Build and Lint + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod download + - run: go build -v . + - name: Run linters + uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0 + with: + version: latest + + test: + name: Go Test + needs: build + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - name: Set up Go 1.x + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod download + - name: Run go test + run: go test -v ./... + call-starter-workflow: uses: keyfactor/actions/.github/workflows/starter.yml@v3 + needs: test secrets: token: ${{ secrets.V2BUILDTOKEN}} APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 988a2a7..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: test -on: [workflow_dispatch, push, pull_request] -jobs: - build: - name: Build and Lint - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version-file: 'go.mod' - cache: true - - run: go mod download - - run: go build -v . - - name: Run linters - uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0 - with: - version: latest - test: - name: Go Test - needs: build - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - - name: Set up Go 1.x - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version-file: 'go.mod' - cache: true - - run: go mod download - - env: - COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME: ${{ vars.COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME }} - COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME: ${{ vars.COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME }} - COMMAND_CERTIFICATE_TEMPLATE: ${{ vars.COMMAND_CERTIFICATE_TEMPLATE }} - COMMAND_HOSTNAME: ${{ vars.COMMAND_HOSTNAME }} - COMMAND_USERNAME: ${{ secrets.COMMAND_USERNAME }} - COMMAND_PASSWORD: ${{ secrets.COMMAND_PASSWORD }} - name: Run go test - run: go test -v ./... \ No newline at end of file From 648cdcc41c5373f0bb900a71036e5deaad4b7b45 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 12:18:18 -0700 Subject: [PATCH 03/18] chore(ci): Test code before running bootstrap workflow 2 Signed-off-by: Hayden Roszell --- .github/workflows/keyfactor-bootstrap-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index f990d96..7598f51 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -22,7 +22,7 @@ jobs: go-version-file: 'go.mod' cache: true - run: go mod download - - run: go build -v . + - run: go build -v ./cmd/main.go - name: Run linters uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0 with: From 78b6b888e3ff0b1d129e301112a9cd0f1a06792c Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 12:40:16 -0700 Subject: [PATCH 04/18] chore(ci): Test code before running bootstrap workflow 3 Signed-off-by: Hayden Roszell --- .../keyfactor-bootstrap-workflow.yml | 8 +-- .golangci.yml | 54 ++++++++----------- Makefile | 2 +- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index 7598f51..c24a986 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -23,10 +23,10 @@ jobs: cache: true - run: go mod download - run: go build -v ./cmd/main.go - - name: Run linters - uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0 - with: - version: latest + # - name: Run linters + # uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0 + # with: + # version: latest test: name: Go Test diff --git a/.golangci.yml b/.golangci.yml index aed8644..318dc6d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,40 +1,32 @@ run: - deadline: 5m - allow-parallel-runners: true + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 12m + + skip-dirs: + - testdata$ + - test/mock + - go/pkg/mod + + skip-files: + - ".*\\.pb\\.go" -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 + - bodyclose + - durationcheck + - errorlint - goimports - - gosimple - - govet - - ineffassign - - lll + - revive + - gosec - misspell - nakedret - - prealloc - - staticcheck - - typecheck - unconvert - unparam - - unused + - whitespace + - gocritic + - nolintlint + +linters-settings: + revive: + # minimal confidence for issues, default is 0.8 + confidence: 0.0 diff --git a/Makefile b/Makefile index ab65716..e59af68 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) KUSTOMIZE_VERSION ?= v5.3.0 CONTROLLER_TOOLS_VERSION ?= v0.14.0 ENVTEST_VERSION ?= latest -GOLANGCI_LINT_VERSION ?= v1.54.2 +GOLANGCI_LINT_VERSION ?= v1.60.1 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. From 2a26e1f7c576f3d7e0f8c95f2e6e447c0518cac8 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 12:46:15 -0700 Subject: [PATCH 05/18] chore(doctool): rename primary doc Signed-off-by: Hayden Roszell --- docsource/{overview.md => content.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docsource/{overview.md => content.md} (100%) diff --git a/docsource/overview.md b/docsource/content.md similarity index 100% rename from docsource/overview.md rename to docsource/content.md From cbcab455ef94f32c91c4c7a869d266afc093ff21 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Tue, 24 Dec 2024 19:47:53 +0000 Subject: [PATCH 06/18] Update generated docs --- README.md | 437 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 365 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index f63ae52..235983c 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,407 @@ -# command-cert-manager-issuer -// TODO(user): Add simple overview of use/purpose +

+ command-cert-manager-issuer +

-## Description -// TODO(user): An in-depth paragraph about your project and overview of use +

+ +Integration Status: production +Release +Issues +GitHub Downloads (all assets, all releases) +

-## Getting Started +

+ + + Support + + · + + License + + · + + Related Integrations + +

-### Prerequisites -- go version v1.21.0+ -- docker version 17.03+. -- kubectl version v1.11.3+. -- Access to a Kubernetes v1.11.3+ cluster. +## Support +The command-cert-manager-issuer is open source and community supported, meaning that there is **no SLA** applicable. -### To Deploy on the cluster -**Build and push your image to the location specified by `IMG`:** +> 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, use the **[Pull requests](../../pulls)** tab. -```sh -make docker-build docker-push IMG=/command-cert-manager-issuer:tag -``` -**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. +# Overview -**Install the CRDs into the cluster:** +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/). -```sh -make install -``` +# Requirements -**Deploy the Manager to the cluster with the image specified by `IMG`:** +Before starting, ensure that the following requirements are met: -```sh -make deploy IMG=/command-cert-manager-issuer:tag -``` +- [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 + ``` -> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin -privileges or be logged in as admin. +2. Add the Helm repository: -**Create instances of your solution** -You can apply the samples (examples) from the config/sample: + ```shell + helm repo add command-issuer https://keyfactor.github.io/command-cert-manager-issuer + helm repo update + ``` -```sh -kubectl apply -k config/samples/ +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 ``` ->**NOTE**: Ensure that the samples has default values to test it out. +## OAuth -### To Uninstall -**Delete the instances (CRs) from the cluster:** +Create an Opaque secret containing the client ID and client secret to authenticate with Command: -```sh -kubectl delete -k config/samples/ +```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" ``` -**Delete the APIs(CRDs) from the cluster:** +> 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. -```sh -make uninstall +```shell +kubectl -n command-issuer-system create secret generic command-ca-secret --from-file=ca.crt ``` -**UnDeploy the controller from the cluster:** +# 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 +``` -```sh -make undeploy +> 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: ``` -## Project Distribution +> 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) -Following are the steps to build the installer and distribute this project to users. +## Approving Certificate Requests -1. Build the installer for the image built and published in the registry: +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 +``` -```sh -make build-installer IMG=/command-cert-manager-issuer:tag +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 ``` -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. +> To learn more about certificate approval and RBAC configuration, see the [cert-manager documentation](https://cert-manager.io/docs/concepts/certificaterequest/#approval). -2. Using the installer +## Overriding the Issuer/ClusterIssuer `spec` using Kubernetes Annotations on CertificateRequest Resources -Users can just run kubectl apply -f to install the project, i.e.: +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. -```sh -kubectl apply -f https://raw.githubusercontent.com//command-cert-manager-issuer//dist/install.yaml -``` +- `command-issuer.keyfactor.com/certificateAuthorityHostname` overrides `certificateAuthorityHostname` +- `command-issuer.keyfactor.com/certificateAuthorityLogicalName` overrides `certificateAuthorityLogicalName` +- `command-issuer.keyfactor.com/certificateTemplate` overrides `certificateTemplate` -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project +> cert-manager copies Annotations set on Certificate resources to the corresponding CertificateRequest. -**NOTE:** Run `make help` for more information on all potential `make` targets +> **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 +> ``` +>
-More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) +# Certificate Metadata -## License +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. | -Copyright 2024. + > 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. -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 +- **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/: + ``` + + + +## License - http://www.apache.org/licenses/LICENSE-2.0 +Apache License 2.0, see [LICENSE](LICENSE). -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. +## Related Integrations +See all [Keyfactor integrations](https://github.com/topics/keyfactor-integration). \ No newline at end of file From 5678b07cdeeaef5ccc03d1a1e028d4707deae360 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 12:57:50 -0700 Subject: [PATCH 07/18] chore(ci): Update integration manifest Signed-off-by: Hayden Roszell --- docsource/content.md | 18 +----------------- integration-manifest.json | 3 ++- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/docsource/content.md b/docsource/content.md index cd65b4e..69bd235 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -229,22 +229,6 @@ For example, ClusterIssuer resources can be used to issue certificates for resou 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. @@ -341,7 +325,7 @@ Keyfactor Command allows users to [attach custom metadata to certificates](https - **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. + 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 if you would like Command Issuer to populate these fields on certificates upon enrollment. | Field Name | Description | |-------------------------------------|-----------------------------------------------------------------------------------------------------------------| diff --git a/integration-manifest.json b/integration-manifest.json index 53b7775..be2d6b6 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,10 +1,11 @@ { "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json", "integration_type": "api-client", - "name": "command-cert-manager-issuer", + "name": "Command Issuer", "status": "production", "link_github": false, "update_catalog": false, "support_level": "community", + "platform_matrix": "linux/arm64,linux/amd64,linux/s390x,linux/ppc64le", "description": "cert-manager external issuer for the Keyfactor Command platform" } From e3c25231f457d009131cb24bdc40bcb5b1c033c5 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Tue, 24 Dec 2024 19:59:34 +0000 Subject: [PATCH 08/18] Update generated docs --- README.md | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 235983c..ad4e69d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- command-cert-manager-issuer + Command Issuer

@@ -26,7 +26,7 @@

## Support -The command-cert-manager-issuer is open source and community supported, meaning that there is **no SLA** applicable. +The Command Issuer is open source and community supported, meaning that there is **no SLA** applicable. > 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, use the **[Pull requests](../../pulls)** tab. @@ -262,22 +262,6 @@ For example, ClusterIssuer resources can be used to issue certificates for resou 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. @@ -374,7 +358,7 @@ Keyfactor Command allows users to [attach custom metadata to certificates](https - **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. + 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 if you would like Command Issuer to populate these fields on certificates upon enrollment. | Field Name | Description | |-------------------------------------|-----------------------------------------------------------------------------------------------------------------| From 1e05c5ea073807d9bdf288482f05cac9dca01f9c Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 24 Dec 2024 13:32:13 -0700 Subject: [PATCH 09/18] chore(crd): Update chart crds Signed-off-by: Hayden Roszell --- .../templates/crds/clusterissuers.yaml | 197 +++++++++++------- .../templates/crds/issuers.yaml | 197 +++++++++++------- .../command-cert-manager-issuer/values.yaml | 6 +- internal/command/command.go | 3 - internal/command/command_test.go | 5 - 5 files changed, 247 insertions(+), 161 deletions(-) diff --git a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml index f845fda..ffcf0e4 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml @@ -16,78 +16,125 @@ spec: singular: clusterissuer scope: Cluster versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - 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' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IssuerSpec defines the desired state of Issuer - properties: - 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. - type: string - certificateAuthorityHostname: - 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" - type: string - certificateTemplate: - 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). - type: string - hostname: - description: Hostname is the hostname of a Keyfactor Command instance. - 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`. - 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. - format: date-time - type: string - message: - 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. - type: string - status: - description: Status of the condition, one of ('True', 'False', 'Unknown'). - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: Type of the condition, known values are ('Ready'). - type: string - required: - - status - - type - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} -{{- end }} \ No newline at end of file + - name: v1alpha1 + schema: + openAPIV3Schema: + 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 + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: 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. + type: string + certificateAuthorityHostname: + 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" + type: string + certificateTemplate: + 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). + 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`. + 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. + format: date-time + type: string + message: + 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. + type: string + status: + description: Status of the condition, one of ('True', 'False', + 'Unknown'). + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of the condition, known values are ('Ready'). + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} diff --git a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml index de8de0b..f13ffc9 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml @@ -16,78 +16,125 @@ spec: singular: issuer scope: Namespaced versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - 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' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IssuerSpec defines the desired state of Issuer - properties: - 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. - type: string - certificateAuthorityHostname: - 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" - type: string - certificateTemplate: - 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). - type: string - hostname: - description: Hostname is the hostname of a Keyfactor Command instance. - 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`. - 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. - format: date-time - type: string - message: - 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. - type: string - status: - description: Status of the condition, one of ('True', 'False', 'Unknown'). - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: Type of the condition, known values are ('Ready'). - type: string - required: - - status - - type - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} -{{- end }} \ No newline at end of file + - name: v1alpha1 + schema: + openAPIV3Schema: + 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 + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: 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. + type: string + certificateAuthorityHostname: + 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" + type: string + certificateTemplate: + 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). + 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`. + 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. + format: date-time + type: string + message: + 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. + type: string + status: + description: Status of the condition, one of ('True', 'False', + 'Unknown'). + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of the condition, known values are ('Ready'). + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} diff --git a/deploy/charts/command-cert-manager-issuer/values.yaml b/deploy/charts/command-cert-manager-issuer/values.yaml index 4806cc0..49169d7 100644 --- a/deploy/charts/command-cert-manager-issuer/values.yaml +++ b/deploy/charts/command-cert-manager-issuer/values.yaml @@ -1,10 +1,10 @@ -# Default values for ejbca-cert-manager-issuer chart. +# Default values for command-cert-manager-issuer chart. -# The number of replica ejbca-cert-manager-issuers to run +# The number of replica command-cert-manager-issuers to run replicaCount: 1 image: - repository: "" + repository: "keyfactor/command-cert-manager-issuer" pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" diff --git a/internal/command/command.go b/internal/command/command.go index cee0bc4..53a3f13 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -256,9 +256,6 @@ func (s *SignConfig) validate() error { if s.CertificateAuthorityLogicalName == "" { return errors.New("certificateAuthorityLogicalName is required") } - if s.CertificateAuthorityHostname == "" { - return errors.New("certificateAuthorityHostname is required") - } return nil } diff --git a/internal/command/command_test.go b/internal/command/command_test.go index c429e99..27f8e64 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -209,11 +209,6 @@ func TestSignConfigValidate(t *testing.T) { 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"}, From d229b82450c94044b9ff5193224427dab36698b0 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Sat, 28 Dec 2024 13:49:18 -0700 Subject: [PATCH 10/18] chore(informer): Configure controller-runtime to not use shared list+watch informer for secrets Signed-off-by: Hayden Roszell --- cmd/main.go | 16 +++++++-- .../command-cert-manager-issuer/README.md | 1 - .../templates/clusterrole.yaml | 34 ------------------- .../templates/clusterrolebinding.yaml | 17 ---------- .../templates/deployment.yaml | 28 ++------------- .../templates/role.yaml | 18 +++++++++- .../templates/rolebinding.yaml | 17 +++++++++- .../templates/secretrole.yaml | 30 ---------------- .../templates/service.yaml | 16 --------- .../templates/serviceaccount.yaml | 3 ++ .../command-cert-manager-issuer/values.yaml | 9 +++-- internal/command/command.go | 4 +-- .../certificaterequest_controller.go | 4 ++- internal/controller/issuer_controller.go | 14 ++++---- 14 files changed, 70 insertions(+), 141 deletions(-) delete mode 100644 deploy/charts/command-cert-manager-issuer/templates/secretrole.yaml delete mode 100644 deploy/charts/command-cert-manager-issuer/templates/service.yaml diff --git a/cmd/main.go b/cmd/main.go index 49d5314..51ca91e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,10 +28,13 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/utils/clock" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -122,10 +125,18 @@ func main() { } } + var cacheOpts cache.Options if secretAccessGrantedAtClusterLevel { - setupLog.Info("expecting secret access at cluster level") + setupLog.Info("expecting SA to have Get+List+Watch permissions for corev1 Secret resources at cluster level") } else { - setupLog.Info(fmt.Sprintf("expecting secret access at namespace level (%s)", clusterResourceNamespace)) + setupLog.Info(fmt.Sprintf("expecting SA to have Get+List+Watch permissions for corev1 Secret resources in the %q namespace", clusterResourceNamespace)) + cacheOpts = cache.Options{ + ByObject: map[client.Object]cache.ByObject{ + &corev1.Secret{}: { + Namespaces: map[string]cache.Config{clusterResourceNamespace: cache.Config{}}, + }, + }, + } } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ @@ -135,6 +146,7 @@ func main() { SecureServing: secureMetrics, TLSOpts: tlsOpts, }, + Cache: cacheOpts, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, diff --git a/deploy/charts/command-cert-manager-issuer/README.md b/deploy/charts/command-cert-manager-issuer/README.md index 6f625cb..61248c0 100644 --- a/deploy/charts/command-cert-manager-issuer/README.md +++ b/deploy/charts/command-cert-manager-issuer/README.md @@ -85,5 +85,4 @@ The following table lists the configurable parameters of the `command-cert-manag | `resources` | CPU/Memory resource requests/limits | `{}` (with commented out options) | | `nodeSelector` | Node labels for pod assignment | `{}` | | `tolerations` | Tolerations for pod assignment | `[]` | -| `secureMetrics.enabled` | Whether to enable and configure the kube-rbac-proxy sidecar for authorized and authenticated use of the /metrics endpoint by Prometheus. | `false` | | `secretConfig.useClusterRoleForSecretAccess` | Specifies if the ServiceAccount should be granted access to the Secret resource using a ClusterRole | `false` | diff --git a/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml b/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml index 489c4c1..13fd364 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml @@ -45,37 +45,3 @@ rules: - issuers/finalizers verbs: - update -{{- if .Values.secureMetrics.enabled }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} - name: {{ include "command-cert-manager-issuer.name" . }}-proxy-role -rules: - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} - name: {{ include "command-cert-manager-issuer.name" . }}-metrics-reader -rules: - - nonResourceURLs: - - /metrics - verbs: - - get -{{- end }} \ No newline at end of file diff --git a/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml b/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml index 8a9c2b6..c913b98 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml @@ -12,20 +12,3 @@ subjects: - kind: ServiceAccount name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} namespace: {{ .Release.Namespace }} -{{- if .Values.secureMetrics.enabled }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} - name: {{ include "command-cert-manager-issuer.name" . }}-proxy-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "command-cert-manager-issuer.name" . }}-proxy-role -subjects: - - kind: ServiceAccount - name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} -{{- end }} \ No newline at end of file diff --git a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml index a42935f..24e11fc 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml @@ -17,6 +17,9 @@ spec: {{- end }} labels: {{- include "command-cert-manager-issuer.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -26,31 +29,6 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - {{- if .Values.secureMetrics.enabled }} - - args: - - --secure-listen-address=0.0.0.0:8443 - - --upstream=http://127.0.0.1:8080/ - - --logtostderr=true - - --v=0 - image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.1 - name: kube-rbac-proxy - ports: - - containerPort: 8443 - name: https - protocol: TCP - resources: - limits: - cpu: 500m - memory: 128Mi - requests: - cpu: 5m - memory: 64Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - {{- end }} - args: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 diff --git a/deploy/charts/command-cert-manager-issuer/templates/role.yaml b/deploy/charts/command-cert-manager-issuer/templates/role.yaml index 78b9b28..9c8617d 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/role.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/role.yaml @@ -23,4 +23,20 @@ rules: - events verbs: - create - - patch \ No newline at end of file + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRole{{ else }}Role{{ end }} +metadata: + labels: + {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "command-cert-manager-issuer.name" . }}-secret-reader-role +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch diff --git a/deploy/charts/command-cert-manager-issuer/templates/rolebinding.yaml b/deploy/charts/command-cert-manager-issuer/templates/rolebinding.yaml index 1900997..631df66 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/rolebinding.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/rolebinding.yaml @@ -11,4 +11,19 @@ roleRef: subjects: - kind: ServiceAccount name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} \ No newline at end of file + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRoleBinding{{ else }}RoleBinding{{ end }} +metadata: + labels: + {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} + name: {{ include "command-cert-manager-issuer.name" . }}-secret-reader-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRole{{ else }}Role{{ end }} + name: {{ include "command-cert-manager-issuer.name" . }}-secret-reader-role +subjects: + - kind: ServiceAccount + name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/charts/command-cert-manager-issuer/templates/secretrole.yaml b/deploy/charts/command-cert-manager-issuer/templates/secretrole.yaml deleted file mode 100644 index 2ef0a3c..0000000 --- a/deploy/charts/command-cert-manager-issuer/templates/secretrole.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRole{{ else }}Role{{ end }} -metadata: - labels: - {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} - name: {{ include "command-cert-manager-issuer.name" . }}-secret-reader-role -rules: - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRoleBinding{{ else }}RoleBinding{{ end }} -metadata: - labels: - {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} - name: {{ include "command-cert-manager-issuer.name" . }}-secret-reader-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: {{ if .Values.secretConfig.useClusterRoleForSecretAccess }}ClusterRole{{ else }}Role{{ end }} - name: {{ include "command-cert-manager-issuer.name" . }}-secret-reader-role -subjects: - - kind: ServiceAccount - name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/deploy/charts/command-cert-manager-issuer/templates/service.yaml b/deploy/charts/command-cert-manager-issuer/templates/service.yaml deleted file mode 100644 index c07551c..0000000 --- a/deploy/charts/command-cert-manager-issuer/templates/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -{{- if .Values.secureMetrics.enabled }} -apiVersion: v1 -kind: Service -metadata: - labels: - {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} - name: {{ include "command-cert-manager-issuer.name" . }}-metrics-service -spec: - ports: - - name: https - port: 8443 - protocol: TCP - targetPort: https - selector: - {{- include "command-cert-manager-issuer.selectorLabels" . | nindent 4 }} -{{- end}} \ No newline at end of file diff --git a/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml b/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml index e7a7604..ccefb2e 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/serviceaccount.yaml @@ -5,6 +5,9 @@ metadata: name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} labels: {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} + {{- if .Values.serviceAccount.labels }} + {{- toYaml .Values.serviceAccount.labels | nindent 4 }} + {{- end }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/deploy/charts/command-cert-manager-issuer/values.yaml b/deploy/charts/command-cert-manager-issuer/values.yaml index 49169d7..fbe4ee6 100644 --- a/deploy/charts/command-cert-manager-issuer/values.yaml +++ b/deploy/charts/command-cert-manager-issuer/values.yaml @@ -13,11 +13,6 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" -# Whether to enable and configure the kube-rbac-proxy sidecar for authorized and authenticated -# use of the /metrics endpoint by Prometheus. -secureMetrics: - enabled: false - secretConfig: # If true, when using Issuer resources, the credential secret must be created in the same namespace as the # Issuer resource. This access is facilitated by granting the ServiceAccount [get, list, watch] for the secret @@ -37,12 +32,16 @@ crd: serviceAccount: # Specifies whether a service account should be created create: true + # Labels to add to the service account + labels: {} # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" +podLabels: {} + podAnnotations: {} podSecurityContext: diff --git a/internal/command/command.go b/internal/command/command.go index 53a3f13..50322a5 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -237,7 +237,7 @@ func newServerConfig(ctx context.Context, config *Config) (*auth_providers.Serve } } - log.Info("configuration was valid - successfully generated server config", "authMethod", server.AuthType, "hostname", server.Host, "apiPath", server.APIPath) + log.Info("Configuration was valid - Successfully generated server config", "authMethod", server.AuthType, "hostname", server.Host, "apiPath", server.APIPath) return server, nil } @@ -287,7 +287,7 @@ func newInternalSigner(ctx context.Context, config *Config, newClientFunc newCom testConnection: client.AuthClient.Authenticate, } - log.Info("successfully generated Command client") + log.Info("Successfully generated Command client") s.client = adapter return s, nil diff --git a/internal/controller/certificaterequest_controller.go b/internal/controller/certificaterequest_controller.go index 053d0b0..2f2ba57 100644 --- a/internal/controller/certificaterequest_controller.go +++ b/internal/controller/certificaterequest_controller.go @@ -109,6 +109,8 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, nil } + log.Info("Starting CertificateRequest reconciliation run") + // We now have a CertificateRequest that belongs to us so we are responsible // for updating its Ready condition. @@ -164,7 +166,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R } issuer, ok := issuerRO.(commandissuer.IssuerLike) if !ok { - err := fmt.Errorf("unexpected type for issuer object: %T", issuerRO) + err := fmt.Errorf("%w: unexpected type for issuer object: %T", errIssuerRef, issuerRO) log.Error(err, "Failed to cast to commandissuer.IssuerLike") setCertificateRequestReadyCondition(&certificateRequest, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, err.Error()) return ctrl.Result{}, nil diff --git a/internal/controller/issuer_controller.go b/internal/controller/issuer_controller.go index 573c8fc..6ecd616 100644 --- a/internal/controller/issuer_controller.go +++ b/internal/controller/issuer_controller.go @@ -75,17 +75,19 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res issuer, err := r.newIssuer() if err != nil { - log.Error(err, "unrecognized issuer type") + 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.") + log.Info(fmt.Sprintf("%s not found. Ignoring.", issuer.GetObjectKind().GroupVersionKind().Kind)) return ctrl.Result{}, nil } + log.Info(fmt.Sprintf("Starting %s reconciliation run", issuer.GetObjectKind().GroupVersionKind().Kind)) + // Always attempt to update the Ready condition defer func() { if err != nil { @@ -103,7 +105,7 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res case issuer.IsClusterScoped(): secretNamespace = r.ClusterResourceNamespace - case !issuer.IsClusterScoped(): + default: secretNamespace = req.Namespace } @@ -152,6 +154,7 @@ func commandConfigFromIssuer(ctx context.Context, c client.Client, issuer comman // 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 + log.Info("Fetching commandSecret from ns", "name", issuer.GetSpec().SecretName, "namespace", secretNamespace) err := c.Get(ctx, types.NamespacedName{ Name: issuer.GetSpec().SecretName, Namespace: secretNamespace, @@ -188,7 +191,7 @@ func commandConfigFromIssuer(ctx context.Context, c client.Client, issuer comman if ok { oauth.Audience = string(audience) } - log.Info("found oauth client credentials in secret", "commandSecretName", issuer.GetSpec().SecretName, "type", authSecret.Type) + 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] @@ -204,12 +207,11 @@ func commandConfigFromIssuer(ctx context.Context, c client.Client, issuer comman Username: string(username), Password: string(password), } - log.Info("found basic auth credentials in secret", "commandSecretName", issuer.GetSpec().SecretName, "type", authSecret.Type) + 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 From c00253a3dfe047bc696edf0c7cb933e27bb277ba Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Sat, 28 Dec 2024 14:30:35 -0700 Subject: [PATCH 11/18] chore(docs): Document Azure Workload Identity Signed-off-by: Hayden Roszell --- docsource/content.md | 93 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/docsource/content.md b/docsource/content.md index 69bd235..e824996 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -18,12 +18,6 @@ Before starting, ensure that the following requirements are met: - [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 @@ -142,6 +136,93 @@ kubectl -n command-issuer-system create secret generic command-secret \ > Audience and Scopes are optional +## Managed Identity Using Azure Entra ID Workload Identity (AKS) + +Azure Entra ID workload identity in Azure Kubernetes Service (AKS) allows Command Issuer to exchange a Kubernetes ServiceAccount Token for an Azure Entra ID access token, which is then used to authenticate to Command. + +1. Reconfigure the AKS cluster to enable workload identity federation. + + ```shell + az aks update \ + --name ${CLUSTER} \ + --enable-oidc-issuer \ + --enable-workload-identity + ``` + + > The [Azure Workload Identity extension can be installed on non-AKS or self-managed clusters](https://azure.github.io/azure-workload-identity/docs/installation.html) if you're not using AKS. + > + > Refer to the [AKS documentation](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster) for more information on the `--enable-workload-identity` feature. + +2. Reconfigure or deploy Command Issuer with extra labels for the Azure Workload Identity webhook, which will result in the Command Issuer controller Pod having an extra volume containing a Kubernetes ServiceAccount token which it will exchange for a token from Azure. + + ```shell + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace command-issuer-system \ + --create-namespace \ + --set "fullnameOverride=$chart_name" \ + --set-string "podLabels.azure\.workload\.identity/use=true" \ + --set-string "serviceAccount.labels.azure\.workload\.identity/use=true" + # --set-string "serviceAccount.annotations.azure\.workload\.identity/client-id=" # May be necessary, but is usually not. + ``` + + If successful, the Command Issuer Pod will have new environment variables and the Azure WI ServiceAccount token as a projected volume: + + ```shell + kubectl -n command-issuer-system describe pod + ``` + + ```shell + Containers: + command-cert-manager-issuer: + ... + Environment: + AZURE_CLIENT_ID: + AZURE_TENANT_ID: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + AZURE_AUTHORITY_HOST: https://login.microsoftonline.com/ + Mounts: + /var/run/secrets/azure/tokens from azure-identity-token (ro) + /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-6rmzz (ro) + ... + Volumes: + ... + azure-identity-token: + Type: Projected (a volume that contains injected data from multiple sources) + TokenExpirationSeconds: 3600 + ``` + + > Refer to [Azure Workload Identity docs](https://azure.github.io/azure-workload-identity/docs/installation/mutating-admission-webhook.html) more information on the role of the Mutating Admission Webhook. + +3. Create a User Assigned Managed Identity in Azure. + + ```shell + export IDENTITY_NAME=command-issuer + az identity create --name "${IDENTITY_NAME}" + ``` + + > Read more about [the `az identity` command](https://learn.microsoft.com/en-us/cli/azure/identity?view=azure-cli-latest). + +4. Associate a Federated Identity Credential (FIC) with the User Assigned Managed Identity. The FIC allows Command Issuer to act on behalf of the Managed Identity by telling Azure to expect: + - The `iss` claim of the ServiceAccount token to match the cluster's OIDC Issuer. Azure will also use the Issuer URL to download the JWT signing certificate. + - The `sub` claim of the ServiceAccount token to match the ServiceAccount's name and namespace. + + ```shell + export SERVICE_ACCOUNT_NAME=command-cert-manager-issuer # This is the default Kubernetes ServiceAccount used by the Command Issuer controller. + export SERVICE_ACCOUNT_NAMESPACE=command-issuer-system # This is the default namespace for Command Issuer used in this doc. + export SERVICE_ACCOUNT_ISSUER=$(az aks show --resource-group $AZURE_DEFAULTS_GROUP --name $CLUSTER --query "oidcIssuerProfile.issuerUrl" -o tsv) + az identity federated-credential create \ + --name "command-issuer" \ + --identity-name "${IDENTITY_NAME}" \ + --issuer "${SERVICE_ACCOUNT_ISSUER}" \ + --subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}" + ``` + + > Read more about [Workload Identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation) in the Entra ID documentation. + > + > Read more about [the `az identity federated-credential` command](https://learn.microsoft.com/en-us/cli/azure/identity/federated-credential?view=azure-cli-latest). + +5. Add Microsoft Entra ID as an [Identity Provider in Command](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/IdentityProviders.htm?Highlight=identity%20provider), and [add the Managed Identity's Client ID as an `oid` claim to the Security Role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) created/identified earlier. + # 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. From 15886a9e9abec846b81b5889faa44f5137574d7e Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Sat, 28 Dec 2024 21:32:18 +0000 Subject: [PATCH 12/18] Update generated docs --- README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ad4e69d..18c0668 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,6 @@ Before starting, ensure that the following requirements are met: - [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 @@ -175,6 +169,93 @@ kubectl -n command-issuer-system create secret generic command-secret \ > Audience and Scopes are optional +## Managed Identity Using Azure Entra ID Workload Identity (AKS) + +Azure Entra ID workload identity in Azure Kubernetes Service (AKS) allows Command Issuer to exchange a Kubernetes ServiceAccount Token for an Azure Entra ID access token, which is then used to authenticate to Command. + +1. Reconfigure the AKS cluster to enable workload identity federation. + + ```shell + az aks update \ + --name ${CLUSTER} \ + --enable-oidc-issuer \ + --enable-workload-identity + ``` + + > The [Azure Workload Identity extension can be installed on non-AKS or self-managed clusters](https://azure.github.io/azure-workload-identity/docs/installation.html) if you're not using AKS. + > + > Refer to the [AKS documentation](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster) for more information on the `--enable-workload-identity` feature. + +2. Reconfigure or deploy Command Issuer with extra labels for the Azure Workload Identity webhook, which will result in the Command Issuer controller Pod having an extra volume containing a Kubernetes ServiceAccount token which it will exchange for a token from Azure. + + ```shell + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace command-issuer-system \ + --create-namespace \ + --set "fullnameOverride=$chart_name" \ + --set-string "podLabels.azure\.workload\.identity/use=true" \ + --set-string "serviceAccount.labels.azure\.workload\.identity/use=true" + # --set-string "serviceAccount.annotations.azure\.workload\.identity/client-id=" # May be necessary, but is usually not. + ``` + + If successful, the Command Issuer Pod will have new environment variables and the Azure WI ServiceAccount token as a projected volume: + + ```shell + kubectl -n command-issuer-system describe pod + ``` + + ```shell + Containers: + command-cert-manager-issuer: + ... + Environment: + AZURE_CLIENT_ID: + AZURE_TENANT_ID: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + AZURE_AUTHORITY_HOST: https://login.microsoftonline.com/ + Mounts: + /var/run/secrets/azure/tokens from azure-identity-token (ro) + /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-6rmzz (ro) + ... + Volumes: + ... + azure-identity-token: + Type: Projected (a volume that contains injected data from multiple sources) + TokenExpirationSeconds: 3600 + ``` + + > Refer to [Azure Workload Identity docs](https://azure.github.io/azure-workload-identity/docs/installation/mutating-admission-webhook.html) more information on the role of the Mutating Admission Webhook. + +3. Create a User Assigned Managed Identity in Azure. + + ```shell + export IDENTITY_NAME=command-issuer + az identity create --name "${IDENTITY_NAME}" + ``` + + > Read more about [the `az identity` command](https://learn.microsoft.com/en-us/cli/azure/identity?view=azure-cli-latest). + +4. Associate a Federated Identity Credential (FIC) with the User Assigned Managed Identity. The FIC allows Command Issuer to act on behalf of the Managed Identity by telling Azure to expect: + - The `iss` claim of the ServiceAccount token to match the cluster's OIDC Issuer. Azure will also use the Issuer URL to download the JWT signing certificate. + - The `sub` claim of the ServiceAccount token to match the ServiceAccount's name and namespace. + + ```shell + export SERVICE_ACCOUNT_NAME=command-cert-manager-issuer # This is the default Kubernetes ServiceAccount used by the Command Issuer controller. + export SERVICE_ACCOUNT_NAMESPACE=command-issuer-system # This is the default namespace for Command Issuer used in this doc. + export SERVICE_ACCOUNT_ISSUER=$(az aks show --resource-group $AZURE_DEFAULTS_GROUP --name $CLUSTER --query "oidcIssuerProfile.issuerUrl" -o tsv) + az identity federated-credential create \ + --name "command-issuer" \ + --identity-name "${IDENTITY_NAME}" \ + --issuer "${SERVICE_ACCOUNT_ISSUER}" \ + --subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}" + ``` + + > Read more about [Workload Identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation) in the Entra ID documentation. + > + > Read more about [the `az identity federated-credential` command](https://learn.microsoft.com/en-us/cli/azure/identity/federated-credential?view=azure-cli-latest). + +5. Add Microsoft Entra ID as an [Identity Provider in Command](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/IdentityProviders.htm?Highlight=identity%20provider), and [add the Managed Identity's Client ID as an `oid` claim to the Security Role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) created/identified earlier. + # 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. From a7b92ba85f528782aee700def17b53a9ba78a8c0 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Sat, 28 Dec 2024 14:34:54 -0700 Subject: [PATCH 13/18] chore(changelog): Update changelog Signed-off-by: Hayden Roszell --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7817fc..2b44d3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,3 +18,18 @@ ## Fixes * fix(helm): Add configuration flag to configure chart to either grant cluster-scoped or namespace-scoped access to Secret and ConfigMap API * fix(controller): Add logic to read secret from reconciler namespace or Issuer namespace depending on Helm configuration. + +# v2.0.0 + +## Features +- Implement OAuth 2.0 Client Credentials grant as an authentication mechanism. +- Implement Azure Workload Identity as an authentication mechanism. + +## Chores +- Refactor Command signer module to remove tight dependency on Issuer/ClusterIssuer types. +- Migrate Kubebuilder from go/v3 to go/v4: + - Upgrade kustomize version to v5.3.0. + - Upgrade controller-gen to v0.14.0. +- Refactor unit tests to use fake Command API instead of requiring live Command server. +- Write e2e integration test. + From a507c62012eef79abae9933e39a608418fa3549b Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Mon, 6 Jan 2025 12:11:10 -0700 Subject: [PATCH 14/18] chore(docs): Typos Signed-off-by: Hayden Roszell --- docsource/content.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docsource/content.md b/docsource/content.md index e824996..8983f7d 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -4,7 +4,7 @@ The Command Issuer for [cert-manager](https://cert-manager.io/) is a [Certificat # Requirements -Before starting, ensure that the following requirements are met: +Before continuing, 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). @@ -22,7 +22,7 @@ Before starting, ensure that the following requirements are met: ## 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. +Command Issuer enrolls certificates by submitting a POST request to the Command 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** @@ -42,11 +42,11 @@ Command Issuer enrolls certificates by submitting a POST request to the CSR Enro 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. + The same goes for **Enrollment RegExes** and **Policies** defined on 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. + 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 define 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. @@ -61,7 +61,7 @@ Command Issuer enrolls certificates by submitting a POST request to the CSR Enro 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 +1. Verify that at least one Kubernetes node is running: ```shell kubectl get nodes @@ -95,9 +95,9 @@ These credentials must be configured using a Kubernetes Secret. By default, the > 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. +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. The following methods are supported: -- Azure Workload Identity (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) +- Managed Identity Using Azure Entra ID Workload Identity (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) ## Basic Auth From 846b0b7f5721a50f6af2de2bffd435cb12d21027 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Mon, 6 Jan 2025 19:16:10 +0000 Subject: [PATCH 15/18] Update generated docs --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 18c0668..140ecff 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The Command Issuer for [cert-manager](https://cert-manager.io/) is a [Certificat # Requirements -Before starting, ensure that the following requirements are met: +Before continuing, 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). @@ -55,7 +55,7 @@ Before starting, ensure that the following requirements are met: ## 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. +Command Issuer enrolls certificates by submitting a POST request to the Command 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** @@ -75,11 +75,11 @@ Command Issuer enrolls certificates by submitting a POST request to the CSR Enro 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. + The same goes for **Enrollment RegExes** and **Policies** defined on 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. + 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 define 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. @@ -94,7 +94,7 @@ Command Issuer enrolls certificates by submitting a POST request to the CSR Enro 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 +1. Verify that at least one Kubernetes node is running: ```shell kubectl get nodes @@ -128,9 +128,9 @@ These credentials must be configured using a Kubernetes Secret. By default, the > 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. +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. The following methods are supported: -- Azure Workload Identity (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) +- Managed Identity Using Azure Entra ID Workload Identity (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) ## Basic Auth From a2682716d8ceabae56660c2952b9aa59cf049c11 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Mon, 13 Jan 2025 11:07:41 -0700 Subject: [PATCH 16/18] chore(ci): Trigger helm release on release-* Signed-off-by: Hayden Roszell --- .github/workflows/helm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index e1cd857..d2862a1 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -5,7 +5,7 @@ on: - '*' pull_request: branches: - - 'v*' + - 'release-*' types: # action should run when the pull request is closed # (regardless of whether it was merged or just closed) From e8eb9493c713f13f0529679267b4bd2649e656b7 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Mon, 13 Jan 2025 20:14:17 -0700 Subject: [PATCH 17/18] chore(ci): Trigger helm release on v-* Signed-off-by: Hayden Roszell --- .github/workflows/helm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index d2862a1..e1cd857 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -5,7 +5,7 @@ on: - '*' pull_request: branches: - - 'release-*' + - 'v*' types: # action should run when the pull request is closed # (regardless of whether it was merged or just closed) From 66d023e781178a53ae52e85b752b8613f420bb45 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Mon, 13 Jan 2025 20:14:56 -0700 Subject: [PATCH 18/18] chore(ci): Trigger helm release on release-* Signed-off-by: Hayden Roszell --- .github/workflows/helm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index e1cd857..d2862a1 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -5,7 +5,7 @@ on: - '*' pull_request: branches: - - 'v*' + - 'release-*' types: # action should run when the pull request is closed # (regardless of whether it was merged or just closed)