├── config ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── network-policy │ ├── kustomization.yaml │ └── allow-metrics-traffic.yaml ├── samples │ ├── secret_issuer.yaml │ ├── secret_clusterissuer.yaml │ ├── certificate_issuer.yaml │ ├── sample-issuer_v1alpha1_sampleissuer.yaml │ ├── certificate_clusterissuer.yaml │ ├── sample-issuer_v1alpha1_sampleclusterissuer.yaml │ ├── kustomization.yaml │ ├── certificaterequest_issuer.yaml │ └── certificaterequest_clusterissuer.yaml ├── default │ ├── manager_metrics_patch.yaml │ ├── metrics_service.yaml │ ├── cert_metrics_manager_patch.yaml │ └── kustomization.yaml ├── rbac │ ├── metrics_reader_role.yaml │ ├── service_account.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── cert_manager_controller_approver_clusterrole.yaml │ ├── cert_manager_controller_approver_clusterrolebinding.yaml │ ├── leader_election_role.yaml │ ├── sampleissuer_admin_role.yaml │ ├── sampleclusterissuer_admin_role.yaml │ ├── sampleissuer_viewer_role.yaml │ ├── sampleclusterissuer_viewer_role.yaml │ ├── sampleissuer_editor_role.yaml │ ├── sampleclusterissuer_editor_role.yaml │ ├── role.yaml │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ ├── monitor_tls_patch.yaml │ └── monitor.yaml └── crd │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ ├── sample-issuer.example.com_sampleissuers.yaml │ └── sample-issuer.example.com_sampleclusterissuers.yaml ├── OWNERS ├── .dockerignore ├── .github ├── renovate.json └── workflows │ ├── test.yml │ ├── lint.yml │ └── test-e2e.yml ├── OWNERS_ALIASES ├── .gitignore ├── hack └── boilerplate.go.txt ├── .devcontainer ├── devcontainer.json └── post-install.sh ├── internal ├── version │ └── version.go ├── signer │ ├── x509util.go │ ├── authority.go │ ├── example.go │ └── policies.go └── controllers │ └── signer.go ├── PROJECT ├── .golangci.yml ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── sampleclusterissuer_types.go │ ├── sampleissuer_types.go │ └── zz_generated.deepcopy.go ├── Dockerfile ├── test ├── e2e │ ├── e2e_suite_test.go │ └── e2e_test.go └── utils │ └── utils.go ├── go.mod ├── Makefile ├── LICENSE ├── cmd └── main.go ├── README.md └── go.sum /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - cm-maintainers 3 | reviewers: 4 | - cm-maintainers 5 | -------------------------------------------------------------------------------- /config/network-policy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - allow-metrics-traffic.yaml 3 | -------------------------------------------------------------------------------- /config/samples/secret_issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: sampleissuer-sample-credentials 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>cert-manager/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /config/samples/secret_clusterissuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: sampleclusterissuer-sample-credentials 5 | namespace: sample-external-issuer-system 6 | -------------------------------------------------------------------------------- /OWNERS_ALIASES: -------------------------------------------------------------------------------- 1 | aliases: 2 | cm-maintainers: 3 | - munnerz 4 | - joshvanl 5 | - wallrj 6 | - jakexks 7 | - maelvls 8 | - sgtcodfish 9 | - inteon 10 | - thatsmrtalbot 11 | - erikgb 12 | -------------------------------------------------------------------------------- /config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/0 4 | value: --metrics-bind-address=:8443 5 | -------------------------------------------------------------------------------- /config/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: sample-external-issuer 6 | app.kubernetes.io/managed-by: kustomize 7 | name: controller-manager 8 | namespace: system 9 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/certificate_issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: certificate-by-sampleissuer 5 | spec: 6 | commonName: certificate-by-sampleissuer.example.com 7 | secretName: certificate-by-sampleissuer 8 | issuerRef: 9 | name: sampleissuer-sample 10 | group: sample-issuer.example.com 11 | kind: SampleIssuer 12 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/samples/sample-issuer_v1alpha1_sampleissuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sample-issuer.example.com/v1alpha1 2 | kind: SampleIssuer 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: sample-external-issuer 6 | app.kubernetes.io/managed-by: kustomize 7 | name: sampleissuer-sample 8 | spec: 9 | authSecretName: "sampleissuer-sample-credentials" 10 | url: "http://sample-issuer.example.com/api/v1" 11 | -------------------------------------------------------------------------------- /config/samples/certificate_clusterissuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: certificate-by-sampleclusterissuer 5 | spec: 6 | commonName: certificate-by-sampleclusterissuer.example.com 7 | secretName: certificate-by-sampleclusterissuer 8 | issuerRef: 9 | name: sampleclusterissuer-sample 10 | group: sample-issuer.example.com 11 | kind: SampleClusterIssuer 12 | -------------------------------------------------------------------------------- /config/samples/sample-issuer_v1alpha1_sampleclusterissuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sample-issuer.example.com/v1alpha1 2 | kind: SampleClusterIssuer 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: sample-external-issuer 6 | app.kubernetes.io/managed-by: kustomize 7 | name: sampleclusterissuer-sample 8 | spec: 9 | authSecretName: "sampleclusterissuer-sample-credentials" 10 | url: "http://sample-issuer.example.com/api/v1" 11 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - sample-issuer_v1alpha1_sampleissuer.yaml 4 | - sample-issuer_v1alpha1_sampleclusterissuer.yaml 5 | # +kubebuilder:scaffold:manifestskustomizesamples 6 | 7 | - certificate_clusterissuer.yaml 8 | - certificate_issuer.yaml 9 | - certificaterequest_clusterissuer.yaml 10 | - certificaterequest_issuer.yaml 11 | - secret_clusterissuer.yaml 12 | - secret_issuer.yaml 13 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: sample-external-issuer 6 | app.kubernetes.io/managed-by: kustomize 7 | name: manager-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: sample-external-issuer 6 | app.kubernetes.io/managed-by: kustomize 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/cert_manager_controller_approver_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | # permissions to approve all sample-issuer.example.com requests 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: cert-manager-controller-approve:sample-issuer-example-com 6 | rules: 7 | - apiGroups: 8 | - cert-manager.io 9 | resources: 10 | - signers 11 | verbs: 12 | - approve 13 | resourceNames: 14 | - sampleissuers.sample-issuer.example.com/* 15 | - sampleclusterissuers.sample-issuer.example.com/* 16 | -------------------------------------------------------------------------------- /config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: sample-external-issuer 7 | app.kubernetes.io/managed-by: kustomize 8 | name: controller-manager-metrics-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: https 13 | port: 8443 14 | protocol: TCP 15 | targetPort: 8443 16 | selector: 17 | control-plane: controller-manager 18 | app.kubernetes.io/name: sample-external-issuer 19 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | 4 | # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus 5 | # to securely reference certificates created and managed by cert-manager. 6 | # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml 7 | # to mount the "metrics-server-cert" secret in the Manager Deployment. 8 | #patches: 9 | # - path: monitor_tls_patch.yaml 10 | # target: 11 | # kind: ServiceMonitor 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/* 8 | Dockerfile.cross 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Go workspace file 17 | go.work 18 | 19 | # Kubernetes Generated files - skip generated files, except for vendored files 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | .vscode 25 | *.iml 26 | *.swp 27 | *.swo 28 | *~ 29 | -------------------------------------------------------------------------------- /config/rbac/cert_manager_controller_approver_clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | # bind the cert-manager internal approver to approve 2 | # sample-issuer.example.com CertificateRequests 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: cert-manager-controller-approve:sample-issuer-example-com 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: cert-manager-controller-approve:sample-issuer-example-com 11 | subjects: 12 | - kind: ServiceAccount 13 | name: cert-manager 14 | namespace: cert-manager 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | name: Run on Ubuntu 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Clone the code 13 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 17 | with: 18 | go-version-file: go.mod 19 | 20 | - name: Running Tests 21 | run: | 22 | go mod tidy 23 | make test 24 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | name: Run on Ubuntu 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Clone the code 13 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 17 | with: 18 | go-version-file: go.mod 19 | 20 | - name: Run linter 21 | uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 22 | with: 23 | version: v2.4.0 24 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kubebuilder DevContainer", 3 | "image": "docker.io/golang:1.25", 4 | "features": { 5 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 6 | "ghcr.io/devcontainers/features/git:1": {} 7 | }, 8 | 9 | "runArgs": ["--network=host"], 10 | 11 | "customizations": { 12 | "vscode": { 13 | "settings": { 14 | "terminal.integrated.shell.linux": "/bin/bash" 15 | }, 16 | "extensions": [ 17 | "ms-kubernetes-tools.vscode-kubernetes-tools", 18 | "ms-azuretools.vscode-docker" 19 | ] 20 | } 21 | }, 22 | 23 | "onCreateCommand": "bash .devcontainer/post-install.sh" 24 | } 25 | 26 | -------------------------------------------------------------------------------- /config/prometheus/monitor_tls_patch.yaml: -------------------------------------------------------------------------------- 1 | # Patch for Prometheus ServiceMonitor to enable secure TLS configuration 2 | # using certificates managed by cert-manager 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: controller-manager-metrics-monitor 7 | namespace: system 8 | spec: 9 | endpoints: 10 | - tlsConfig: 11 | insecureSkipVerify: false 12 | ca: 13 | secret: 14 | name: metrics-server-cert 15 | key: ca.crt 16 | cert: 17 | secret: 18 | name: metrics-server-cert 19 | key: tls.crt 20 | keySecret: 21 | name: metrics-server-cert 22 | key: tls.key 23 | -------------------------------------------------------------------------------- /.devcontainer/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 5 | chmod +x ./kind 6 | mv ./kind /usr/local/bin/kind 7 | 8 | curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64 9 | chmod +x kubebuilder 10 | mv kubebuilder /usr/local/bin/ 11 | 12 | KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) 13 | curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" 14 | chmod +x kubectl 15 | mv kubectl /usr/local/bin/kubectl 16 | 17 | docker network create -d=bridge --subnet=172.19.0.0/24 kind 18 | 19 | kind version 20 | kubebuilder version 21 | docker --version 22 | go version 23 | kubectl version --client 24 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The cert-manager Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | var ( 20 | Version = "development" 21 | ) 22 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: sample-external-issuer 7 | app.kubernetes.io/managed-by: kustomize 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/sample-issuer.example.com_sampleissuers.yaml 6 | - bases/sample-issuer.example.com_sampleclusterissuers.yaml 7 | # +kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patches: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [WEBHOOK] To enable webhook, uncomment the following section 15 | # the following config is for teaching kustomize how to do kustomization for CRDs. 16 | #configurations: 17 | #- kustomizeconfig.yaml 18 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: example.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: sample-external-issuer 9 | repo: github.com/cert-manager/sample-external-issuer 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | domain: example.com 15 | group: sample-issuer 16 | kind: SampleIssuer 17 | path: github.com/cert-manager/sample-external-issuer/api/v1alpha1 18 | version: v1alpha1 19 | - api: 20 | crdVersion: v1 21 | domain: example.com 22 | group: sample-issuer 23 | kind: SampleClusterIssuer 24 | path: github.com/cert-manager/sample-external-issuer/api/v1alpha1 25 | version: v1alpha1 26 | version: "3" 27 | -------------------------------------------------------------------------------- /config/rbac/sampleissuer_admin_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project sample-external-issuer itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants full permissions ('*') over sample-issuer.example.com. 5 | # This role is intended for users authorized to modify roles and bindings within the cluster, 6 | # enabling them to delegate specific permissions to other users or groups as needed. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: sample-external-issuer 13 | app.kubernetes.io/managed-by: kustomize 14 | name: sampleissuer-admin-role 15 | rules: 16 | - apiGroups: 17 | - sample-issuer.example.com 18 | resources: 19 | - sampleissuers 20 | verbs: 21 | - '*' 22 | - apiGroups: 23 | - sample-issuer.example.com 24 | resources: 25 | - sampleissuers/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/sampleclusterissuer_admin_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project sample-external-issuer itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants full permissions ('*') over sample-issuer.example.com. 5 | # This role is intended for users authorized to modify roles and bindings within the cluster, 6 | # enabling them to delegate specific permissions to other users or groups as needed. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: sample-external-issuer 13 | app.kubernetes.io/managed-by: kustomize 14 | name: sampleclusterissuer-admin-role 15 | rules: 16 | - apiGroups: 17 | - sample-issuer.example.com 18 | resources: 19 | - sampleclusterissuers 20 | verbs: 21 | - '*' 22 | - apiGroups: 23 | - sample-issuer.example.com 24 | resources: 25 | - sampleclusterissuers/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/sampleissuer_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project sample-external-issuer itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants read-only access to sample-issuer.example.com resources. 5 | # This role is intended for users who need visibility into these resources 6 | # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: sample-external-issuer 13 | app.kubernetes.io/managed-by: kustomize 14 | name: sampleissuer-viewer-role 15 | rules: 16 | - apiGroups: 17 | - sample-issuer.example.com 18 | resources: 19 | - sampleissuers 20 | verbs: 21 | - get 22 | - list 23 | - watch 24 | - apiGroups: 25 | - sample-issuer.example.com 26 | resources: 27 | - sampleissuers/status 28 | verbs: 29 | - get 30 | -------------------------------------------------------------------------------- /.github/workflows/test-e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test-e2e: 9 | name: Run on Ubuntu 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Clone the code 13 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 17 | with: 18 | go-version-file: go.mod 19 | 20 | - name: Install the latest version of kind 21 | run: | 22 | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 23 | chmod +x ./kind 24 | sudo mv ./kind /usr/local/bin/kind 25 | 26 | - name: Verify kind installation 27 | run: kind version 28 | 29 | - name: Create kind cluster 30 | run: kind create cluster 31 | 32 | - name: Running Test e2e 33 | run: | 34 | go mod tidy 35 | make test-e2e 36 | -------------------------------------------------------------------------------- /config/rbac/sampleclusterissuer_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project sample-external-issuer itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants read-only access to sample-issuer.example.com resources. 5 | # This role is intended for users who need visibility into these resources 6 | # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: sample-external-issuer 13 | app.kubernetes.io/managed-by: kustomize 14 | name: sampleclusterissuer-viewer-role 15 | rules: 16 | - apiGroups: 17 | - sample-issuer.example.com 18 | resources: 19 | - sampleclusterissuers 20 | verbs: 21 | - get 22 | - list 23 | - watch 24 | - apiGroups: 25 | - sample-issuer.example.com 26 | resources: 27 | - sampleclusterissuers/status 28 | verbs: 29 | - get 30 | -------------------------------------------------------------------------------- /config/network-policy/allow-metrics-traffic.yaml: -------------------------------------------------------------------------------- 1 | # This NetworkPolicy allows ingress traffic 2 | # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those 3 | # namespaces are able to gather data from the metrics endpoint. 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: sample-external-issuer 9 | app.kubernetes.io/managed-by: kustomize 10 | name: allow-metrics-traffic 11 | namespace: system 12 | spec: 13 | podSelector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | app.kubernetes.io/name: sample-external-issuer 17 | policyTypes: 18 | - Ingress 19 | ingress: 20 | # This allows ingress traffic from any namespace with the label metrics: enabled 21 | - from: 22 | - namespaceSelector: 23 | matchLabels: 24 | metrics: enabled # Only from namespaces with this label 25 | ports: 26 | - port: 8443 27 | protocol: TCP 28 | -------------------------------------------------------------------------------- /config/samples/certificaterequest_issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: CertificateRequest 3 | metadata: 4 | name: sampleissuer-sample 5 | spec: 6 | request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQllUQ0NBUWdDQVFBd01ERXVNQ3dHQTFVRUF4TWxiWGt0Y0c5a0xtMTVMVzVoYldWemNHRmpaUzV3YjJRdQpZMngxYzNSbGNpNXNiMk5oYkRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkFCcVpNYXBxcDFFCkZqVGlMOG5GZkxwOFNTcmxQWFRzLzVLSG1PL3grYmtHZ1Q2bGJjL3FPampVSlhlV2xpZWxPS2o0NG9PYklKMVIKMXN1SmdLL3BZd1dnZGpCMEJna3Foa2lHOXcwQkNRNHhaekJsTUdNR0ExVWRFUVJjTUZxQ0pXMTVMWE4yWXk1dAplUzF1WVcxbGMzQmhZMlV1YzNaakxtTnNkWE4wWlhJdWJHOWpZV3lDSlcxNUxYQnZaQzV0ZVMxdVlXMWxjM0JoClkyVXVjRzlrTG1Oc2RYTjBaWEl1Ykc5allXeUhCTUFBQWhpSEJBb0FJZ0l3Q2dZSUtvWkl6ajBFQXdJRFJ3QXcKUkFJZ0Q2T2JROUNScXZ2MUYzc01PeGZRRWdrZEdyTlR3UDE5eGQ5RFVQZ24vUHNDSURIS0k0UlBxK3V2QW5KSwplanU0THcyUDh2QjVHY3ZRcCs5amhCMVdiVFZpCi0tLS0tRU5EIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLQo= 7 | issuerRef: 8 | name: sampleissuer-sample 9 | group: sample-issuer.example.com 10 | kind: SampleIssuer 11 | -------------------------------------------------------------------------------- /config/rbac/sampleissuer_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project sample-external-issuer itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants permissions to create, update, and delete resources within the sample-issuer.example.com. 5 | # This role is intended for users who need to manage these resources 6 | # but should not control RBAC or manage permissions for others. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: sample-external-issuer 13 | app.kubernetes.io/managed-by: kustomize 14 | name: sampleissuer-editor-role 15 | rules: 16 | - apiGroups: 17 | - sample-issuer.example.com 18 | resources: 19 | - sampleissuers 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - sample-issuer.example.com 30 | resources: 31 | - sampleissuers/status 32 | verbs: 33 | - get 34 | -------------------------------------------------------------------------------- /config/default/cert_metrics_manager_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. 2 | 3 | # Add the volumeMount for the metrics-server certs 4 | - op: add 5 | path: /spec/template/spec/containers/0/volumeMounts/- 6 | value: 7 | mountPath: /tmp/k8s-metrics-server/metrics-certs 8 | name: metrics-certs 9 | readOnly: true 10 | 11 | # Add the --metrics-cert-path argument for the metrics server 12 | - op: add 13 | path: /spec/template/spec/containers/0/args/- 14 | value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs 15 | 16 | # Add the metrics-server certs volume configuration 17 | - op: add 18 | path: /spec/template/spec/volumes/- 19 | value: 20 | name: metrics-certs 21 | secret: 22 | secretName: metrics-server-cert 23 | optional: false 24 | items: 25 | - key: ca.crt 26 | path: ca.crt 27 | - key: tls.crt 28 | path: tls.crt 29 | - key: tls.key 30 | path: tls.key 31 | -------------------------------------------------------------------------------- /config/samples/certificaterequest_clusterissuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: CertificateRequest 3 | metadata: 4 | name: sampleclusterissuer-sample 5 | spec: 6 | request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQllUQ0NBUWdDQVFBd01ERXVNQ3dHQTFVRUF4TWxiWGt0Y0c5a0xtMTVMVzVoYldWemNHRmpaUzV3YjJRdQpZMngxYzNSbGNpNXNiMk5oYkRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkFCcVpNYXBxcDFFCkZqVGlMOG5GZkxwOFNTcmxQWFRzLzVLSG1PL3grYmtHZ1Q2bGJjL3FPampVSlhlV2xpZWxPS2o0NG9PYklKMVIKMXN1SmdLL3BZd1dnZGpCMEJna3Foa2lHOXcwQkNRNHhaekJsTUdNR0ExVWRFUVJjTUZxQ0pXMTVMWE4yWXk1dAplUzF1WVcxbGMzQmhZMlV1YzNaakxtTnNkWE4wWlhJdWJHOWpZV3lDSlcxNUxYQnZaQzV0ZVMxdVlXMWxjM0JoClkyVXVjRzlrTG1Oc2RYTjBaWEl1Ykc5allXeUhCTUFBQWhpSEJBb0FJZ0l3Q2dZSUtvWkl6ajBFQXdJRFJ3QXcKUkFJZ0Q2T2JROUNScXZ2MUYzc01PeGZRRWdrZEdyTlR3UDE5eGQ5RFVQZ24vUHNDSURIS0k0UlBxK3V2QW5KSwplanU0THcyUDh2QjVHY3ZRcCs5amhCMVdiVFZpCi0tLS0tRU5EIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLQo= 7 | issuerRef: 8 | name: sampleclusterissuer-sample 9 | group: sample-issuer.example.com 10 | kind: SampleClusterIssuer 11 | -------------------------------------------------------------------------------- /config/rbac/sampleclusterissuer_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project sample-external-issuer itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants permissions to create, update, and delete resources within the sample-issuer.example.com. 5 | # This role is intended for users who need to manage these resources 6 | # but should not control RBAC or manage permissions for others. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: sample-external-issuer 13 | app.kubernetes.io/managed-by: kustomize 14 | name: sampleclusterissuer-editor-role 15 | rules: 16 | - apiGroups: 17 | - sample-issuer.example.com 18 | resources: 19 | - sampleclusterissuers 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - sample-issuer.example.com 30 | resources: 31 | - sampleclusterissuers/status 32 | verbs: 33 | - get 34 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | allow-parallel-runners: true 4 | linters: 5 | default: none 6 | enable: 7 | - copyloopvar 8 | - dupl 9 | - errcheck 10 | - ginkgolinter 11 | - goconst 12 | - gocyclo 13 | - govet 14 | - ineffassign 15 | - lll 16 | - misspell 17 | - nakedret 18 | - prealloc 19 | - revive 20 | - staticcheck 21 | - unconvert 22 | - unparam 23 | - unused 24 | settings: 25 | revive: 26 | rules: 27 | - name: comment-spacings 28 | staticcheck: 29 | dot-import-whitelist: 30 | - github.com/onsi/ginkgo/v2 31 | exclusions: 32 | generated: lax 33 | rules: 34 | - linters: 35 | - lll 36 | path: api/* 37 | - linters: 38 | - dupl 39 | - lll 40 | path: internal/* 41 | paths: 42 | - third_party$ 43 | - builtin$ 44 | - examples$ 45 | formatters: 46 | enable: 47 | - gofmt 48 | - goimports 49 | exclusions: 50 | generated: lax 51 | paths: 52 | - third_party$ 53 | - builtin$ 54 | - examples$ 55 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: sample-external-issuer 8 | app.kubernetes.io/managed-by: kustomize 9 | name: controller-manager-metrics-monitor 10 | namespace: system 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: https # Ensure this is the name of the port that exposes HTTPS metrics 15 | scheme: https 16 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 17 | tlsConfig: 18 | # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables 19 | # certificate verification, exposing the system to potential man-in-the-middle attacks. 20 | # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. 21 | # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, 22 | # which securely references the certificate from the 'metrics-server-cert' secret. 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | app.kubernetes.io/name: sample-external-issuer 28 | -------------------------------------------------------------------------------- /internal/signer/x509util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package signer 18 | 19 | import ( 20 | "crypto/rsa" 21 | "crypto/x509" 22 | "encoding/pem" 23 | "errors" 24 | ) 25 | 26 | func parseKey(pemBytes []byte) (*rsa.PrivateKey, error) { 27 | // extract PEM from request object 28 | block, _ := pem.Decode(pemBytes) 29 | if block == nil || block.Type != "RSA PRIVATE KEY" { 30 | return nil, errors.New("PEM block type must be RSA PRIVATE KEY") 31 | } 32 | return x509.ParsePKCS1PrivateKey(block.Bytes) 33 | } 34 | 35 | func parseCert(pemBytes []byte) (*x509.Certificate, error) { 36 | // extract PEM from request object 37 | block, _ := pem.Decode(pemBytes) 38 | if block == nil || block.Type != "CERTIFICATE" { 39 | return nil, errors.New("PEM block type must be CERTIFICATE") 40 | } 41 | return x509.ParseCertificate(block.Bytes) 42 | } 43 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the sample-issuer v1alpha1 API group. 18 | // +kubebuilder:object:generate=true 19 | // +groupName=sample-issuer.example.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects. 29 | GroupVersion = schema.GroupVersion{Group: "sample-issuer.example.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - create 13 | - patch 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - secrets 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - cert-manager.io 24 | resources: 25 | - certificaterequests 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - apiGroups: 31 | - cert-manager.io 32 | resources: 33 | - certificaterequests/status 34 | verbs: 35 | - patch 36 | - apiGroups: 37 | - certificates.k8s.io 38 | resources: 39 | - certificatesigningrequests 40 | verbs: 41 | - get 42 | - list 43 | - watch 44 | - apiGroups: 45 | - certificates.k8s.io 46 | resources: 47 | - certificatesigningrequests/status 48 | verbs: 49 | - patch 50 | - apiGroups: 51 | - certificates.k8s.io 52 | resourceNames: 53 | - sampleclusterissuers.sample-issuer.example.com/* 54 | - sampleissuers.sample-issuer.example.com/* 55 | resources: 56 | - signers 57 | verbs: 58 | - sign 59 | - apiGroups: 60 | - sample-issuer.example.com 61 | resources: 62 | - sampleclusterissuers 63 | - sampleissuers 64 | verbs: 65 | - get 66 | - list 67 | - watch 68 | - apiGroups: 69 | - sample-issuer.example.com 70 | resources: 71 | - sampleclusterissuers/status 72 | - sampleissuers/status 73 | verbs: 74 | - patch 75 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # The following RBAC configurations are used to protect 13 | # the metrics endpoint with authn/authz. These configurations 14 | # ensure that only authorized users and service accounts 15 | # can access the metrics endpoint. Comment the following 16 | # permissions if you want to disable this protection. 17 | # More info: https://book.kubebuilder.io/reference/metrics.html 18 | - metrics_auth_role.yaml 19 | - metrics_auth_role_binding.yaml 20 | - metrics_reader_role.yaml 21 | # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by 22 | # default, aiding admins in cluster management. Those roles are 23 | # not used by the sample-external-issuer itself. You can comment the following lines 24 | # if you do not want those helpers be installed with your Project. 25 | - sampleclusterissuer_admin_role.yaml 26 | - sampleclusterissuer_editor_role.yaml 27 | - sampleclusterissuer_viewer_role.yaml 28 | - sampleissuer_admin_role.yaml 29 | - sampleissuer_editor_role.yaml 30 | - sampleissuer_viewer_role.yaml 31 | # Comment the following 2 lines if you don't wish for the internal cert-manager 32 | # approver to approve all sample-issuer.example.com CertificateRequests by 33 | # default. 34 | - cert_manager_controller_approver_clusterrole.yaml 35 | - cert_manager_controller_approver_clusterrolebinding.yaml 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM docker.io/golang:1.25@sha256:36b4f45d2874905b9e8573b783292629bcb346d0a70d8d7150b6df545234818f AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/ internal/ 18 | 19 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 20 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 21 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 22 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 23 | ENV CGO_ENABLED=0 24 | ENV GOOS=${TARGETOS:-linux} 25 | ENV GOARCH=${TARGETARCH} 26 | ENV GO111MODULE=on 27 | 28 | # Do an initial compilation before setting the version so that there is less to 29 | # re-compile when the version changes 30 | RUN go build -mod=readonly ./... 31 | 32 | ARG VERSION 33 | 34 | # Build 35 | RUN go build \ 36 | -ldflags="-X=github.com/cert-manager/sample-external-issuer/internal/version.Version=${VERSION}" \ 37 | -mod=readonly \ 38 | -o manager cmd/main.go 39 | 40 | # Use distroless as minimal base image to package the manager binary 41 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 42 | FROM gcr.io/distroless/static:nonroot@sha256:2b7c93f6d6648c11f0e80a48558c8f77885eb0445213b8e69a6a0d7c89fc6ae4 43 | WORKDIR / 44 | COPY --from=builder /workspace/manager . 45 | USER 65532:65532 46 | 47 | ENTRYPOINT ["/manager"] 48 | -------------------------------------------------------------------------------- /internal/signer/authority.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package signer 18 | 19 | import ( 20 | "crypto" 21 | "crypto/rand" 22 | "crypto/x509" 23 | "fmt" 24 | "time" 25 | ) 26 | 27 | // CertificateAuthority implements a certificate authority that supports policy 28 | // based signing. It's used by the signing controller. 29 | type CertificateAuthority struct { 30 | // RawCert is an optional field to determine if signing cert/key pairs have changed 31 | RawCert []byte 32 | // RawKey is an optional field to determine if signing cert/key pairs have changed 33 | RawKey []byte 34 | 35 | Certificate *x509.Certificate 36 | PrivateKey crypto.Signer 37 | Backdate time.Duration 38 | Now func() time.Time 39 | } 40 | 41 | // Sign signs a certificate request, applying a SigningPolicy and returns a DER 42 | // encoded x509 certificate. 43 | func (ca *CertificateAuthority) Sign(certTemplate *x509.Certificate, policy SigningPolicy) ([]byte, error) { 44 | now := time.Now() 45 | if ca.Now != nil { 46 | now = ca.Now() 47 | } 48 | 49 | nbf := now.Add(-ca.Backdate) 50 | if !nbf.Before(ca.Certificate.NotAfter) { 51 | return nil, fmt.Errorf("the signer has expired: NotAfter=%v", ca.Certificate.NotAfter) 52 | } 53 | 54 | if err := policy.apply(certTemplate); err != nil { 55 | return nil, err 56 | } 57 | 58 | if !certTemplate.NotAfter.Before(ca.Certificate.NotAfter) { 59 | certTemplate.NotAfter = ca.Certificate.NotAfter 60 | } 61 | if !now.Before(ca.Certificate.NotAfter) { 62 | return nil, fmt.Errorf("refusing to sign a certificate that expired in the past") 63 | } 64 | 65 | der, err := x509.CreateCertificate(rand.Reader, certTemplate, ca.Certificate, certTemplate.PublicKey, ca.PrivateKey) 66 | if err != nil { 67 | return nil, fmt.Errorf("failed to sign certificate: %v", err) 68 | } 69 | 70 | return der, nil 71 | } 72 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: sample-external-issuer 7 | app.kubernetes.io/managed-by: kustomize 8 | name: system 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: controller-manager 14 | namespace: system 15 | labels: 16 | control-plane: controller-manager 17 | app.kubernetes.io/name: sample-external-issuer 18 | app.kubernetes.io/managed-by: kustomize 19 | spec: 20 | selector: 21 | matchLabels: 22 | control-plane: controller-manager 23 | app.kubernetes.io/name: sample-external-issuer 24 | replicas: 1 25 | template: 26 | metadata: 27 | annotations: 28 | kubectl.kubernetes.io/default-container: manager 29 | labels: 30 | control-plane: controller-manager 31 | app.kubernetes.io/name: sample-external-issuer 32 | spec: 33 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 34 | # according to the platforms which are supported by your solution. 35 | # It is considered best practice to support multiple architectures. You can 36 | # build your manager image using the makefile target docker-buildx. 37 | # affinity: 38 | # nodeAffinity: 39 | # requiredDuringSchedulingIgnoredDuringExecution: 40 | # nodeSelectorTerms: 41 | # - matchExpressions: 42 | # - key: kubernetes.io/arch 43 | # operator: In 44 | # values: 45 | # - amd64 46 | # - arm64 47 | # - ppc64le 48 | # - s390x 49 | # - key: kubernetes.io/os 50 | # operator: In 51 | # values: 52 | # - linux 53 | securityContext: 54 | # Projects are configured by default to adhere to the "restricted" Pod Security Standards. 55 | # This ensures that deployments meet the highest security requirements for Kubernetes. 56 | # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 57 | runAsNonRoot: true 58 | seccompProfile: 59 | type: RuntimeDefault 60 | containers: 61 | - command: 62 | - /manager 63 | args: 64 | - --leader-elect 65 | - --health-probe-bind-address=:8081 66 | image: controller:latest 67 | name: manager 68 | ports: [] 69 | securityContext: 70 | allowPrivilegeEscalation: false 71 | capabilities: 72 | drop: 73 | - "ALL" 74 | livenessProbe: 75 | httpGet: 76 | path: /healthz 77 | port: 8081 78 | initialDelaySeconds: 15 79 | periodSeconds: 20 80 | readinessProbe: 81 | httpGet: 82 | path: /readyz 83 | port: 8081 84 | initialDelaySeconds: 5 85 | periodSeconds: 10 86 | # TODO(user): Configure the resources accordingly based on the project requirements. 87 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 88 | resources: 89 | limits: 90 | cpu: 500m 91 | memory: 128Mi 92 | requests: 93 | cpu: 10m 94 | memory: 64Mi 95 | volumeMounts: [] 96 | volumes: [] 97 | serviceAccountName: controller-manager 98 | terminationGracePeriodSeconds: 10 99 | -------------------------------------------------------------------------------- /api/v1alpha1/sampleclusterissuer_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "github.com/cert-manager/issuer-lib/api/v1alpha1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // +kubebuilder:object:root=true 25 | // +kubebuilder:subresource:status 26 | // +kubebuilder:resource:scope=Cluster 27 | // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status" 28 | // +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason" 29 | // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message" 30 | // +kubebuilder:printcolumn:name="LastTransition",type="string",type="date",JSONPath=".status.conditions[?(@.type==\"Ready\")].lastTransitionTime" 31 | // +kubebuilder:printcolumn:name="ObservedGeneration",type="integer",JSONPath=".status.conditions[?(@.type==\"Ready\")].observedGeneration" 32 | // +kubebuilder:printcolumn:name="Generation",type="integer",JSONPath=".metadata.generation" 33 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 34 | 35 | // SampleClusterIssuer is the Schema for the sampleclusterissuers API. 36 | type SampleClusterIssuer struct { 37 | metav1.TypeMeta `json:",inline"` 38 | metav1.ObjectMeta `json:"metadata,omitempty"` 39 | 40 | Spec IssuerSpec `json:"spec,omitempty"` 41 | Status v1alpha1.IssuerStatus `json:"status,omitempty"` 42 | } 43 | 44 | func (vi *SampleClusterIssuer) GetConditions() []metav1.Condition { 45 | return vi.Status.Conditions 46 | } 47 | 48 | // GetIssuerTypeIdentifier returns a string that uniquely identifies the 49 | // issuer type. This should be a constant across all instances of this 50 | // issuer type. This string is used as a prefix when determining the 51 | // issuer type for a Kubernetes CertificateSigningRequest resource based 52 | // on the issuerName field. The value should be formatted as follows: 53 | // ".". For example, the value 54 | // "simpleclusterissuers.issuer.cert-manager.io" will match all CSRs 55 | // with an issuerName set to eg. "simpleclusterissuers.issuer.cert-manager.io/issuer1". 56 | func (vi *SampleClusterIssuer) GetIssuerTypeIdentifier() string { 57 | // ACTION REQUIRED: Change this to a unique string that identifies your cluster issuer 58 | return "sampleclusterissuers.sample-issuer.example.com" 59 | } 60 | 61 | // issuer-lib requires that we implement the Issuer interface 62 | // so that it can interact with our Issuer resource. 63 | var _ v1alpha1.Issuer = &SampleClusterIssuer{} 64 | 65 | // +kubebuilder:object:root=true 66 | 67 | // SampleClusterIssuerList contains a list of SampleClusterIssuer. 68 | type SampleClusterIssuerList struct { 69 | metav1.TypeMeta `json:",inline"` 70 | metav1.ListMeta `json:"metadata,omitempty"` 71 | Items []SampleClusterIssuer `json:"items"` 72 | } 73 | 74 | func init() { 75 | SchemeBuilder.Register(&SampleClusterIssuer{}, &SampleClusterIssuerList{}) 76 | } 77 | -------------------------------------------------------------------------------- /api/v1alpha1/sampleissuer_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "github.com/cert-manager/issuer-lib/api/v1alpha1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // +kubebuilder:object:root=true 25 | // +kubebuilder:subresource:status 26 | // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status" 27 | // +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].reason" 28 | // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message" 29 | // +kubebuilder:printcolumn:name="LastTransition",type="string",type="date",JSONPath=".status.conditions[?(@.type==\"Ready\")].lastTransitionTime" 30 | // +kubebuilder:printcolumn:name="ObservedGeneration",type="integer",JSONPath=".status.conditions[?(@.type==\"Ready\")].observedGeneration" 31 | // +kubebuilder:printcolumn:name="Generation",type="integer",JSONPath=".metadata.generation" 32 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 33 | 34 | // SampleIssuer is the Schema for the sampleissuers API. 35 | type SampleIssuer struct { 36 | metav1.TypeMeta `json:",inline"` 37 | metav1.ObjectMeta `json:"metadata,omitempty"` 38 | 39 | Spec IssuerSpec `json:"spec,omitempty"` 40 | Status v1alpha1.IssuerStatus `json:"status,omitempty"` 41 | } 42 | 43 | // IssuerSpec defines the desired state of SampleIssuer 44 | type IssuerSpec struct { 45 | // URL is the base URL for the endpoint of the signing service, 46 | // for example: "https://sample-signer.example.com/api". 47 | URL string `json:"url"` 48 | 49 | // A reference to a Secret in the same namespace as the referent. If the 50 | // referent is a SampleClusterIssuer, the reference instead refers to the resource 51 | // with the given name in the configured 'cluster resource namespace', which 52 | // is set as a flag on the controller component (and defaults to the 53 | // namespace that the controller runs in). 54 | AuthSecretName string `json:"authSecretName"` 55 | } 56 | 57 | func (vi *SampleIssuer) GetConditions() []metav1.Condition { 58 | return vi.Status.Conditions 59 | } 60 | 61 | // GetIssuerTypeIdentifier returns a string that uniquely identifies the 62 | // issuer type. This should be a constant across all instances of this 63 | // issuer type. This string is used as a prefix when determining the 64 | // issuer type for a Kubernetes CertificateSigningRequest resource based 65 | // on the issuerName field. The value should be formatted as follows: 66 | // ".". For example, the value 67 | // "simpleclusterissuers.issuer.cert-manager.io" will match all CSRs 68 | // with an issuerName set to eg. "simpleclusterissuers.issuer.cert-manager.io/issuer1". 69 | func (vi *SampleIssuer) GetIssuerTypeIdentifier() string { 70 | // ACTION REQUIRED: Change this to a unique string that identifies your issuer 71 | return "sampleissuers.sample-issuer.example.com" 72 | } 73 | 74 | // issuer-lib requires that we implement the Issuer interface 75 | // so that it can interact with our Issuer resource. 76 | var _ v1alpha1.Issuer = &SampleIssuer{} 77 | 78 | // +kubebuilder:object:root=true 79 | 80 | // SampleIssuerList contains a list of SampleIssuer. 81 | type SampleIssuerList struct { 82 | metav1.TypeMeta `json:",inline"` 83 | metav1.ListMeta `json:"metadata,omitempty"` 84 | Items []SampleIssuer `json:"items"` 85 | } 86 | 87 | func init() { 88 | SchemeBuilder.Register(&SampleIssuer{}, &SampleIssuerList{}) 89 | } 90 | -------------------------------------------------------------------------------- /internal/signer/example.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package signer 18 | 19 | import ( 20 | "crypto/x509" 21 | "encoding/pem" 22 | "time" 23 | 24 | capi "k8s.io/api/certificates/v1beta1" 25 | 26 | sampleissuerapi "github.com/cert-manager/sample-external-issuer/api/v1alpha1" 27 | "github.com/cert-manager/sample-external-issuer/internal/controllers" 28 | ) 29 | 30 | func ExampleHealthCheckerFromIssuerAndSecretData(*sampleissuerapi.IssuerSpec, map[string][]byte) (controllers.HealthChecker, error) { 31 | return &exampleSigner{}, nil 32 | } 33 | 34 | func ExampleSignerFromIssuerAndSecretData(*sampleissuerapi.IssuerSpec, map[string][]byte) (controllers.Signer, error) { 35 | return &exampleSigner{}, nil 36 | } 37 | 38 | type exampleSigner struct { 39 | } 40 | 41 | func (o *exampleSigner) Check() error { 42 | return nil 43 | } 44 | 45 | var ( 46 | keyPEM = []byte(` 47 | -----BEGIN RSA PRIVATE KEY----- 48 | MIIEowIBAAKCAQEAyyD731YDc88ZkdbtaQyi65adORoenBB0FvR6JLmuupm0bqhH 49 | erNzHbFdKkPfES7NTkALGYId0AfA6Y9zpnPmfYO4BIDGQaVlk9A00p77PZovA6dQ 50 | aEyHiTwSwH0/3hXGe0M32Lk30EUyhF6dU1/DFgoGMRtd2Tf+Z121fyyEB5AEtn1I 51 | Xkwkb5/BXukxRH5jGjm+o1usE8CyKFFwnT+gtILKJ7DNzqpIkFQBe9wXr0z/nEFl 52 | GjtR21iF1amdY6dNIG9dPw6IQl6Swz6zWUCvh9rIFKJqPfknwrtiD6s8d38H2Xv3 53 | dNAQFM2cRKCz4iR5KVqUOFwyD9tOg2e6lMUXHwIDAQABAoIBAFIPpTF4ojRq+j18 54 | wrSpsjfSxPmIn80UqJGNerrTeM9RwR7jRN1BGcRpHuYwPTHH4pE2NkW71ydvunOg 55 | zGv2bqtOR00qaO2kUAEDIBPmvkEIxO2I7mb0Y90BM+Int2GVEnZBlZIsYWv2SI5J 56 | Wu2PxlRlAFNeZu+WO2Su6t/RsBUNVSUOjFhbT2zTQtwinalD2pIE5WCrnvEFpAeK 57 | bhAsL3Vht6clGDYk5INYxTLnbiLwSl7Dl38/q8/D+hJNFe7XlQ2X5cOrShvtUarP 58 | Q1L3RlQTXq7kyx6PJ3tyQvQiBVSd7jb3bxUZwxfRC5sZ2dROVOSLI/NVSR7aooAn 59 | De4yEeECgYEA7xnJ8Zbv5qP/3Vvuzq2264xGLleboKLAV8Br3AWy8+OTpUH2DI5n 60 | exLR4FSLS4n48E9GmdY9FsgNYOGTlQDDuYYB1jDFQqGtKo4YkiYxLrC2o7JI8I0p 61 | XRRiPAXtzY00bF1dw8vA689zEnf9XvovjPmLWUuUqjdXv7z+v8F3XPUCgYEA2XxU 62 | zMqcy211vH1zz+PYy7JQAaRFZ7eoZH0vDOO4deFcPZp1LpjRzYbRI/EB9vAWAovt 63 | FKVtZW5+rrJ/sd4ZB0/jkxahUj5r49ELicxFmWgBzzJRfQ/RZXpjh4S40Qz9zptG 64 | e6HbbeVRiEA6ZKpOBZHG3wtSaeik9g2ZOlve10MCgYATlZEs8KgFxDkY8IbG9wOc 65 | l4jIEvT0W2BVz7UF+JGH2IQnbReyP5fKROhb75DZRxvU0yl9QEcQrqIp5VApTD67 66 | 23YbDTObGZMNgUYR8n7kzCSpk9jVmzpgHWNOd03bIE3C8oLTnsTWi89pG9rtBKEQ 67 | cwAu+DndF1tgoSJcooQcYQKBgFmacPGi9HCXm29aHHHlRLe/sljKzlGKCFXGgbEE 68 | zUW74J382hSln6LWzanKLO4JQngwIDBma6jjmkvtfNDSWWt6zZ8XLsXMs/S7ds6C 69 | G5a1lDFCYPJupu3xO7pkwyRV/ue1b5eWOuqPFUVWePhqdhSzV8UjTAQYdoZtWdkC 70 | atAzAoGBAODs3qUjU25kqnUEJS8P3llrNZQunMRWhD1eVovyVgsJkRTNaF2nKrjW 71 | EM9qDV5Wq1hIKW93f9lVKhbj7dMkRQrHNz1ToAuvKFsWnL7mW89FdbHqZpNfomQU 72 | Cs+nfLWBLLXpABSikgdzD27/w436jk5nqRr7/Jh9WDEO7roHOneY 73 | -----END RSA PRIVATE KEY----- 74 | `) 75 | certPEM = []byte(` 76 | -----BEGIN CERTIFICATE----- 77 | MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl 78 | cm5ldGVzMB4XDTIwMDQwNzEzMzgxMFoXDTMwMDQwNTEzMzgxMFowFTETMBEGA1UE 79 | AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMsg 80 | +99WA3PPGZHW7WkMouuWnTkaHpwQdBb0eiS5rrqZtG6oR3qzcx2xXSpD3xEuzU5A 81 | CxmCHdAHwOmPc6Zz5n2DuASAxkGlZZPQNNKe+z2aLwOnUGhMh4k8EsB9P94VxntD 82 | N9i5N9BFMoRenVNfwxYKBjEbXdk3/mddtX8shAeQBLZ9SF5MJG+fwV7pMUR+Yxo5 83 | vqNbrBPAsihRcJ0/oLSCyiewzc6qSJBUAXvcF69M/5xBZRo7UdtYhdWpnWOnTSBv 84 | XT8OiEJeksM+s1lAr4fayBSiaj35J8K7Yg+rPHd/B9l793TQEBTNnESgs+IkeSla 85 | lDhcMg/bToNnupTFFx8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB 86 | /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAD7h2TrMPlAl22BQija0EMKEokWL 87 | 2ZNL4+l8F6go/epU1QQYS6PmWBqySvhK2aek65LaWaowLUzUey70k/f9oJvGBo6W 88 | AJvBJ6eBVSuiEid6FW7gj/+gAKEC2vd78zs3QmusCCISO6h1dDTQS0swyS/HBBVx 89 | 1T33EWRlxdF42vTHMzO8bEUJlokBxvWvkzkjpAfuJQ1MuVTfkbuJFeIVER2xtLL7 90 | ai85UCdnGfwgzKGx1URCjcE67oKuUQDiulXk4bnQT2Zbj0IcEHcB4XAeuYuYJdB4 91 | YcXl/jdU/2nHdY6r7m6xIapxs0hdDMF/lML2SszUIukZw73NJp3x7L9enCY= 92 | -----END CERTIFICATE----- 93 | `) 94 | duration = time.Hour * 24 * 365 95 | ) 96 | 97 | func (o *exampleSigner) Sign(certTemplate *x509.Certificate) ([]byte, error) { 98 | key, err := parseKey(keyPEM) 99 | if err != nil { 100 | return nil, err 101 | } 102 | cert, err := parseCert(certPEM) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | ca := &CertificateAuthority{ 108 | Certificate: cert, 109 | PrivateKey: key, 110 | Backdate: 5 * time.Minute, 111 | } 112 | 113 | crtDER, err := ca.Sign(certTemplate, PermissiveSigningPolicy{ 114 | TTL: duration, 115 | Usages: []capi.KeyUsage{ 116 | capi.UsageServerAuth, 117 | }, 118 | }) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | return pem.EncodeToMemory(&pem.Block{ 124 | Type: "CERTIFICATE", 125 | Bytes: crtDER, 126 | }), nil 127 | } 128 | -------------------------------------------------------------------------------- /internal/signer/policies.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package signer 18 | 19 | import ( 20 | "crypto/x509" 21 | "fmt" 22 | "sort" 23 | "time" 24 | 25 | capi "k8s.io/api/certificates/v1beta1" 26 | ) 27 | 28 | // SigningPolicy validates a CertificateRequest before it's signed by the 29 | // CertificateAuthority. It may default or otherwise mutate a certificate 30 | // template. 31 | type SigningPolicy interface { 32 | // not-exporting apply forces signing policy implementations to be internal 33 | // to this package. 34 | apply(template *x509.Certificate) error 35 | } 36 | 37 | // PermissiveSigningPolicy is the signing policy historically used by the local 38 | // signer. 39 | // 40 | // - It forwards all SANs from the original signing request. 41 | // - It sets allowed usages as configured in the policy. 42 | // - It sets NotAfter based on the TTL configured in the policy. 43 | // - It zeros all extensions. 44 | // - It sets BasicConstraints to true. 45 | // - It sets IsCA to false. 46 | type PermissiveSigningPolicy struct { 47 | // TTL is the certificate TTL. It's used to calculate the NotAfter value of 48 | // the certificate. 49 | TTL time.Duration 50 | // Usages are the allowed usages of a certificate. 51 | Usages []capi.KeyUsage 52 | } 53 | 54 | func (p PermissiveSigningPolicy) apply(tmpl *x509.Certificate) error { 55 | usage, extUsages, err := keyUsagesFromStrings(p.Usages) 56 | if err != nil { 57 | return err 58 | } 59 | tmpl.KeyUsage = usage 60 | tmpl.ExtKeyUsage = extUsages 61 | tmpl.NotAfter = tmpl.NotBefore.Add(p.TTL) 62 | 63 | tmpl.ExtraExtensions = nil 64 | tmpl.Extensions = nil 65 | tmpl.BasicConstraintsValid = true 66 | tmpl.IsCA = false 67 | 68 | return nil 69 | } 70 | 71 | var keyUsageDict = map[capi.KeyUsage]x509.KeyUsage{ 72 | capi.UsageSigning: x509.KeyUsageDigitalSignature, 73 | capi.UsageDigitalSignature: x509.KeyUsageDigitalSignature, 74 | capi.UsageContentCommitment: x509.KeyUsageContentCommitment, 75 | capi.UsageKeyEncipherment: x509.KeyUsageKeyEncipherment, 76 | capi.UsageKeyAgreement: x509.KeyUsageKeyAgreement, 77 | capi.UsageDataEncipherment: x509.KeyUsageDataEncipherment, 78 | capi.UsageCertSign: x509.KeyUsageCertSign, 79 | capi.UsageCRLSign: x509.KeyUsageCRLSign, 80 | capi.UsageEncipherOnly: x509.KeyUsageEncipherOnly, 81 | capi.UsageDecipherOnly: x509.KeyUsageDecipherOnly, 82 | } 83 | 84 | var extKeyUsageDict = map[capi.KeyUsage]x509.ExtKeyUsage{ 85 | capi.UsageAny: x509.ExtKeyUsageAny, 86 | capi.UsageServerAuth: x509.ExtKeyUsageServerAuth, 87 | capi.UsageClientAuth: x509.ExtKeyUsageClientAuth, 88 | capi.UsageCodeSigning: x509.ExtKeyUsageCodeSigning, 89 | capi.UsageEmailProtection: x509.ExtKeyUsageEmailProtection, 90 | capi.UsageSMIME: x509.ExtKeyUsageEmailProtection, 91 | capi.UsageIPsecEndSystem: x509.ExtKeyUsageIPSECEndSystem, 92 | capi.UsageIPsecTunnel: x509.ExtKeyUsageIPSECTunnel, 93 | capi.UsageIPsecUser: x509.ExtKeyUsageIPSECUser, 94 | capi.UsageTimestamping: x509.ExtKeyUsageTimeStamping, 95 | capi.UsageOCSPSigning: x509.ExtKeyUsageOCSPSigning, 96 | capi.UsageMicrosoftSGC: x509.ExtKeyUsageMicrosoftServerGatedCrypto, 97 | capi.UsageNetscapeSGC: x509.ExtKeyUsageNetscapeServerGatedCrypto, 98 | } 99 | 100 | // keyUsagesFromStrings will translate a slice of usage strings from the 101 | // certificates API ("pkg/apis/certificates".KeyUsage) to x509.KeyUsage and 102 | // x509.ExtKeyUsage types. 103 | func keyUsagesFromStrings(usages []capi.KeyUsage) (x509.KeyUsage, []x509.ExtKeyUsage, error) { 104 | var keyUsage x509.KeyUsage 105 | var unrecognized []capi.KeyUsage 106 | extKeyUsages := make(map[x509.ExtKeyUsage]struct{}) 107 | for _, usage := range usages { 108 | if val, ok := keyUsageDict[usage]; ok { 109 | keyUsage |= val 110 | } else if val, ok := extKeyUsageDict[usage]; ok { 111 | extKeyUsages[val] = struct{}{} 112 | } else { 113 | unrecognized = append(unrecognized, usage) 114 | } 115 | } 116 | 117 | var sorted sortedExtKeyUsage 118 | for eku := range extKeyUsages { 119 | sorted = append(sorted, eku) 120 | } 121 | sort.Sort(sorted) 122 | 123 | if len(unrecognized) > 0 { 124 | return 0, nil, fmt.Errorf("unrecognized usage values: %q", unrecognized) 125 | } 126 | 127 | return keyUsage, []x509.ExtKeyUsage(sorted), nil 128 | } 129 | 130 | type sortedExtKeyUsage []x509.ExtKeyUsage 131 | 132 | func (s sortedExtKeyUsage) Len() int { 133 | return len(s) 134 | } 135 | 136 | func (s sortedExtKeyUsage) Swap(i, j int) { 137 | s[i], s[j] = s[j], s[i] 138 | } 139 | 140 | func (s sortedExtKeyUsage) Less(i, j int) bool { 141 | return s[i] < s[j] 142 | } 143 | -------------------------------------------------------------------------------- /test/e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "testing" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | 28 | "github.com/cert-manager/sample-external-issuer/test/utils" 29 | ) 30 | 31 | var ( 32 | // Optional Environment Variables: 33 | // - PROMETHEUS_INSTALL_SKIP=true: Skips Prometheus Operator installation during test setup. 34 | // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. 35 | // These variables are useful if Prometheus or CertManager is already installed, avoiding 36 | // re-installation and conflicts. 37 | skipPrometheusInstall = os.Getenv("PROMETHEUS_INSTALL_SKIP") == "true" 38 | skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" 39 | // isPrometheusOperatorAlreadyInstalled will be set true when prometheus CRDs be found on the cluster 40 | isPrometheusOperatorAlreadyInstalled = false 41 | // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster 42 | isCertManagerAlreadyInstalled = false 43 | 44 | // projectImage is the name of the image which will be build and loaded 45 | // with the code source changes to be tested. 46 | projectImage = "example.com/sample-external-issuer:v0.0.1" 47 | ) 48 | 49 | // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, 50 | // temporary environment to validate project changes with the the purposed to be used in CI jobs. 51 | // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs 52 | // CertManager and Prometheus. 53 | func TestE2E(t *testing.T) { 54 | RegisterFailHandler(Fail) 55 | _, _ = fmt.Fprintf(GinkgoWriter, "Starting sample-external-issuer integration test suite\n") 56 | RunSpecs(t, "e2e suite") 57 | } 58 | 59 | var _ = BeforeSuite(func() { 60 | By("Ensure that Prometheus is enabled") 61 | _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") 62 | 63 | By("building the manager(Operator) image") 64 | cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage)) 65 | _, err := utils.Run(cmd) 66 | ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image") 67 | 68 | // TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is 69 | // built and available before running the tests. Also, remove the following block. 70 | By("loading the manager(Operator) image on Kind") 71 | err = utils.LoadImageToKindClusterWithName(projectImage) 72 | ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") 73 | 74 | // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. 75 | // To prevent errors when tests run in environments with Prometheus or CertManager already installed, 76 | // we check for their presence before execution. 77 | // Setup Prometheus and CertManager before the suite if not skipped and if not already installed 78 | if !skipPrometheusInstall { 79 | By("checking if prometheus is installed already") 80 | isPrometheusOperatorAlreadyInstalled = utils.IsPrometheusCRDsInstalled() 81 | if !isPrometheusOperatorAlreadyInstalled { 82 | _, _ = fmt.Fprintf(GinkgoWriter, "Installing Prometheus Operator...\n") 83 | Expect(utils.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") 84 | } else { 85 | _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: Prometheus Operator is already installed. Skipping installation...\n") 86 | } 87 | } 88 | if !skipCertManagerInstall { 89 | By("checking if cert manager is installed already") 90 | isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() 91 | if !isCertManagerAlreadyInstalled { 92 | _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") 93 | Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") 94 | } else { 95 | _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") 96 | } 97 | } 98 | }) 99 | 100 | var _ = AfterSuite(func() { 101 | // Tear down Prometheus and CertManager after the suite if not skipped and if they were not already installed 102 | if !skipPrometheusInstall && !isPrometheusOperatorAlreadyInstalled { 103 | _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Prometheus Operator...\n") 104 | utils.UninstallPrometheusOperator() 105 | } 106 | if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { 107 | _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") 108 | utils.UninstallCertManager() 109 | } 110 | }) 111 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2025 The cert-manager Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *IssuerSpec) DeepCopyInto(out *IssuerSpec) { 29 | *out = *in 30 | } 31 | 32 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IssuerSpec. 33 | func (in *IssuerSpec) DeepCopy() *IssuerSpec { 34 | if in == nil { 35 | return nil 36 | } 37 | out := new(IssuerSpec) 38 | in.DeepCopyInto(out) 39 | return out 40 | } 41 | 42 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 43 | func (in *SampleClusterIssuer) DeepCopyInto(out *SampleClusterIssuer) { 44 | *out = *in 45 | out.TypeMeta = in.TypeMeta 46 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 47 | out.Spec = in.Spec 48 | in.Status.DeepCopyInto(&out.Status) 49 | } 50 | 51 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SampleClusterIssuer. 52 | func (in *SampleClusterIssuer) DeepCopy() *SampleClusterIssuer { 53 | if in == nil { 54 | return nil 55 | } 56 | out := new(SampleClusterIssuer) 57 | in.DeepCopyInto(out) 58 | return out 59 | } 60 | 61 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 62 | func (in *SampleClusterIssuer) DeepCopyObject() runtime.Object { 63 | if c := in.DeepCopy(); c != nil { 64 | return c 65 | } 66 | return nil 67 | } 68 | 69 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 70 | func (in *SampleClusterIssuerList) DeepCopyInto(out *SampleClusterIssuerList) { 71 | *out = *in 72 | out.TypeMeta = in.TypeMeta 73 | in.ListMeta.DeepCopyInto(&out.ListMeta) 74 | if in.Items != nil { 75 | in, out := &in.Items, &out.Items 76 | *out = make([]SampleClusterIssuer, len(*in)) 77 | for i := range *in { 78 | (*in)[i].DeepCopyInto(&(*out)[i]) 79 | } 80 | } 81 | } 82 | 83 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SampleClusterIssuerList. 84 | func (in *SampleClusterIssuerList) DeepCopy() *SampleClusterIssuerList { 85 | if in == nil { 86 | return nil 87 | } 88 | out := new(SampleClusterIssuerList) 89 | in.DeepCopyInto(out) 90 | return out 91 | } 92 | 93 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 94 | func (in *SampleClusterIssuerList) DeepCopyObject() runtime.Object { 95 | if c := in.DeepCopy(); c != nil { 96 | return c 97 | } 98 | return nil 99 | } 100 | 101 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 102 | func (in *SampleIssuer) DeepCopyInto(out *SampleIssuer) { 103 | *out = *in 104 | out.TypeMeta = in.TypeMeta 105 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 106 | out.Spec = in.Spec 107 | in.Status.DeepCopyInto(&out.Status) 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SampleIssuer. 111 | func (in *SampleIssuer) DeepCopy() *SampleIssuer { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(SampleIssuer) 116 | in.DeepCopyInto(out) 117 | return out 118 | } 119 | 120 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 121 | func (in *SampleIssuer) DeepCopyObject() runtime.Object { 122 | if c := in.DeepCopy(); c != nil { 123 | return c 124 | } 125 | return nil 126 | } 127 | 128 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 129 | func (in *SampleIssuerList) DeepCopyInto(out *SampleIssuerList) { 130 | *out = *in 131 | out.TypeMeta = in.TypeMeta 132 | in.ListMeta.DeepCopyInto(&out.ListMeta) 133 | if in.Items != nil { 134 | in, out := &in.Items, &out.Items 135 | *out = make([]SampleIssuer, len(*in)) 136 | for i := range *in { 137 | (*in)[i].DeepCopyInto(&(*out)[i]) 138 | } 139 | } 140 | } 141 | 142 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SampleIssuerList. 143 | func (in *SampleIssuerList) DeepCopy() *SampleIssuerList { 144 | if in == nil { 145 | return nil 146 | } 147 | out := new(SampleIssuerList) 148 | in.DeepCopyInto(out) 149 | return out 150 | } 151 | 152 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 153 | func (in *SampleIssuerList) DeepCopyObject() runtime.Object { 154 | if c := in.DeepCopy(); c != nil { 155 | return c 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cert-manager/sample-external-issuer 2 | 3 | go 1.25.0 4 | 5 | godebug default=go1.23 6 | 7 | require ( 8 | github.com/cert-manager/cert-manager v1.19.2 9 | github.com/cert-manager/issuer-lib v0.9.0 10 | github.com/onsi/ginkgo/v2 v2.27.3 11 | github.com/onsi/gomega v1.38.3 12 | k8s.io/api v0.35.0 13 | k8s.io/apimachinery v0.35.0 14 | k8s.io/client-go v0.35.0 15 | k8s.io/klog/v2 v2.130.1 16 | sigs.k8s.io/controller-runtime v0.22.4 17 | ) 18 | 19 | require ( 20 | cel.dev/expr v0.24.0 // indirect 21 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 22 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 23 | github.com/antlr4-go/antlr/v4 v4.13.1 // indirect 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/blang/semver/v4 v4.0.0 // indirect 26 | github.com/cenkalti/backoff/v5 v5.0.3 // indirect 27 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 29 | github.com/emicklei/go-restful/v3 v3.13.0 // indirect 30 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 31 | github.com/felixge/httpsnoop v1.0.4 // indirect 32 | github.com/fsnotify/fsnotify v1.9.0 // indirect 33 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 34 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect 35 | github.com/go-ldap/ldap/v3 v3.4.12 // indirect 36 | github.com/go-logr/logr v1.4.3 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/go-logr/zapr v1.3.0 // indirect 39 | github.com/go-openapi/jsonpointer v0.22.1 // indirect 40 | github.com/go-openapi/jsonreference v0.21.2 // indirect 41 | github.com/go-openapi/swag v0.23.1 // indirect 42 | github.com/go-openapi/swag/jsonname v0.25.1 // indirect 43 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 44 | github.com/gogo/protobuf v1.3.2 // indirect 45 | github.com/google/btree v1.1.3 // indirect 46 | github.com/google/cel-go v0.26.0 // indirect 47 | github.com/google/gnostic-models v0.7.0 // indirect 48 | github.com/google/go-cmp v0.7.0 // indirect 49 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 50 | github.com/google/uuid v1.6.0 // indirect 51 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect 52 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 53 | github.com/josharian/intern v1.0.0 // indirect 54 | github.com/json-iterator/go v1.1.12 // indirect 55 | github.com/mailru/easyjson v0.9.0 // indirect 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 57 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 59 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 60 | github.com/prometheus/client_golang v1.23.2 // indirect 61 | github.com/prometheus/client_model v0.6.2 // indirect 62 | github.com/prometheus/common v0.66.1 // indirect 63 | github.com/prometheus/procfs v0.17.0 // indirect 64 | github.com/spf13/cobra v1.10.1 // indirect 65 | github.com/spf13/pflag v1.0.10 // indirect 66 | github.com/stoewer/go-strcase v1.3.1 // indirect 67 | github.com/x448/float16 v0.8.4 // indirect 68 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 69 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 70 | go.opentelemetry.io/otel v1.37.0 // indirect 71 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect 72 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect 73 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 74 | go.opentelemetry.io/otel/sdk v1.37.0 // indirect 75 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 76 | go.opentelemetry.io/proto/otlp v1.7.0 // indirect 77 | go.uber.org/multierr v1.11.0 // indirect 78 | go.uber.org/zap v1.27.0 // indirect 79 | go.yaml.in/yaml/v2 v2.4.3 // indirect 80 | go.yaml.in/yaml/v3 v3.0.4 // indirect 81 | golang.org/x/crypto v0.45.0 // indirect 82 | golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect 83 | golang.org/x/mod v0.29.0 // indirect 84 | golang.org/x/net v0.47.0 // indirect 85 | golang.org/x/oauth2 v0.31.0 // indirect 86 | golang.org/x/sync v0.18.0 // indirect 87 | golang.org/x/sys v0.38.0 // indirect 88 | golang.org/x/term v0.37.0 // indirect 89 | golang.org/x/text v0.31.0 // indirect 90 | golang.org/x/time v0.13.0 // indirect 91 | golang.org/x/tools v0.38.0 // indirect 92 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 93 | google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 // indirect 94 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect 95 | google.golang.org/grpc v1.75.1 // indirect 96 | google.golang.org/protobuf v1.36.9 // indirect 97 | gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect 98 | gopkg.in/inf.v0 v0.9.1 // indirect 99 | gopkg.in/yaml.v3 v3.0.1 // indirect 100 | k8s.io/apiextensions-apiserver v0.34.1 // indirect 101 | k8s.io/apiserver v0.34.1 // indirect 102 | k8s.io/component-base v0.34.1 // indirect 103 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect 104 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect 105 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect 106 | sigs.k8s.io/gateway-api v1.4.0 // indirect 107 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 108 | sigs.k8s.io/randfill v1.0.0 // indirect 109 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 110 | sigs.k8s.io/yaml v1.6.0 // indirect 111 | ) 112 | -------------------------------------------------------------------------------- /config/crd/bases/sample-issuer.example.com_sampleissuers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.17.1 7 | name: sampleissuers.sample-issuer.example.com 8 | spec: 9 | group: sample-issuer.example.com 10 | names: 11 | kind: SampleIssuer 12 | listKind: SampleIssuerList 13 | plural: sampleissuers 14 | singular: sampleissuer 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .status.conditions[?(@.type=="Ready")].status 19 | name: Ready 20 | type: string 21 | - jsonPath: .status.conditions[?(@.type=="Ready")].reason 22 | name: Reason 23 | type: string 24 | - jsonPath: .status.conditions[?(@.type=="Ready")].message 25 | name: Message 26 | type: string 27 | - jsonPath: .status.conditions[?(@.type=="Ready")].lastTransitionTime 28 | name: LastTransition 29 | type: date 30 | - jsonPath: .status.conditions[?(@.type=="Ready")].observedGeneration 31 | name: ObservedGeneration 32 | type: integer 33 | - jsonPath: .metadata.generation 34 | name: Generation 35 | type: integer 36 | - jsonPath: .metadata.creationTimestamp 37 | name: Age 38 | type: date 39 | name: v1alpha1 40 | schema: 41 | openAPIV3Schema: 42 | description: SampleIssuer is the Schema for the sampleissuers API. 43 | properties: 44 | apiVersion: 45 | description: |- 46 | APIVersion defines the versioned schema of this representation of an object. 47 | Servers should convert recognized schemas to the latest internal value, and 48 | may reject unrecognized values. 49 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 50 | type: string 51 | kind: 52 | description: |- 53 | Kind is a string value representing the REST resource this object represents. 54 | Servers may infer this from the endpoint the client submits requests to. 55 | Cannot be updated. 56 | In CamelCase. 57 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 58 | type: string 59 | metadata: 60 | type: object 61 | spec: 62 | description: IssuerSpec defines the desired state of SampleIssuer 63 | properties: 64 | authSecretName: 65 | description: |- 66 | A reference to a Secret in the same namespace as the referent. If the 67 | referent is a SampleClusterIssuer, the reference instead refers to the resource 68 | with the given name in the configured 'cluster resource namespace', which 69 | is set as a flag on the controller component (and defaults to the 70 | namespace that the controller runs in). 71 | type: string 72 | url: 73 | description: |- 74 | URL is the base URL for the endpoint of the signing service, 75 | for example: "https://sample-signer.example.com/api". 76 | type: string 77 | required: 78 | - authSecretName 79 | - url 80 | type: object 81 | status: 82 | properties: 83 | conditions: 84 | description: |- 85 | List of status conditions to indicate the status of an Issuer. 86 | Known condition types are `Ready`. 87 | items: 88 | description: Condition contains details for one aspect of the current 89 | state of this API Resource. 90 | properties: 91 | lastTransitionTime: 92 | description: |- 93 | lastTransitionTime is the last time the condition transitioned from one status to another. 94 | This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 95 | format: date-time 96 | type: string 97 | message: 98 | description: |- 99 | message is a human readable message indicating details about the transition. 100 | This may be an empty string. 101 | maxLength: 32768 102 | type: string 103 | observedGeneration: 104 | description: |- 105 | observedGeneration represents the .metadata.generation that the condition was set based upon. 106 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 107 | with respect to the current state of the instance. 108 | format: int64 109 | minimum: 0 110 | type: integer 111 | reason: 112 | description: |- 113 | reason contains a programmatic identifier indicating the reason for the condition's last transition. 114 | Producers of specific condition types may define expected values and meanings for this field, 115 | and whether the values are considered a guaranteed API. 116 | The value should be a CamelCase string. 117 | This field may not be empty. 118 | maxLength: 1024 119 | minLength: 1 120 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 121 | type: string 122 | status: 123 | description: status of the condition, one of True, False, Unknown. 124 | enum: 125 | - "True" 126 | - "False" 127 | - Unknown 128 | type: string 129 | type: 130 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 131 | maxLength: 316 132 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 133 | type: string 134 | required: 135 | - lastTransitionTime 136 | - message 137 | - reason 138 | - status 139 | - type 140 | type: object 141 | type: array 142 | x-kubernetes-list-map-keys: 143 | - type 144 | x-kubernetes-list-type: map 145 | type: object 146 | type: object 147 | served: true 148 | storage: true 149 | subresources: 150 | status: {} 151 | -------------------------------------------------------------------------------- /config/crd/bases/sample-issuer.example.com_sampleclusterissuers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.17.1 7 | name: sampleclusterissuers.sample-issuer.example.com 8 | spec: 9 | group: sample-issuer.example.com 10 | names: 11 | kind: SampleClusterIssuer 12 | listKind: SampleClusterIssuerList 13 | plural: sampleclusterissuers 14 | singular: sampleclusterissuer 15 | scope: Cluster 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .status.conditions[?(@.type=="Ready")].status 19 | name: Ready 20 | type: string 21 | - jsonPath: .status.conditions[?(@.type=="Ready")].reason 22 | name: Reason 23 | type: string 24 | - jsonPath: .status.conditions[?(@.type=="Ready")].message 25 | name: Message 26 | type: string 27 | - jsonPath: .status.conditions[?(@.type=="Ready")].lastTransitionTime 28 | name: LastTransition 29 | type: date 30 | - jsonPath: .status.conditions[?(@.type=="Ready")].observedGeneration 31 | name: ObservedGeneration 32 | type: integer 33 | - jsonPath: .metadata.generation 34 | name: Generation 35 | type: integer 36 | - jsonPath: .metadata.creationTimestamp 37 | name: Age 38 | type: date 39 | name: v1alpha1 40 | schema: 41 | openAPIV3Schema: 42 | description: SampleClusterIssuer is the Schema for the sampleclusterissuers 43 | API. 44 | properties: 45 | apiVersion: 46 | description: |- 47 | APIVersion defines the versioned schema of this representation of an object. 48 | Servers should convert recognized schemas to the latest internal value, and 49 | may reject unrecognized values. 50 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 51 | type: string 52 | kind: 53 | description: |- 54 | Kind is a string value representing the REST resource this object represents. 55 | Servers may infer this from the endpoint the client submits requests to. 56 | Cannot be updated. 57 | In CamelCase. 58 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 59 | type: string 60 | metadata: 61 | type: object 62 | spec: 63 | description: IssuerSpec defines the desired state of SampleIssuer 64 | properties: 65 | authSecretName: 66 | description: |- 67 | A reference to a Secret in the same namespace as the referent. If the 68 | referent is a SampleClusterIssuer, the reference instead refers to the resource 69 | with the given name in the configured 'cluster resource namespace', which 70 | is set as a flag on the controller component (and defaults to the 71 | namespace that the controller runs in). 72 | type: string 73 | url: 74 | description: |- 75 | URL is the base URL for the endpoint of the signing service, 76 | for example: "https://sample-signer.example.com/api". 77 | type: string 78 | required: 79 | - authSecretName 80 | - url 81 | type: object 82 | status: 83 | properties: 84 | conditions: 85 | description: |- 86 | List of status conditions to indicate the status of an Issuer. 87 | Known condition types are `Ready`. 88 | items: 89 | description: Condition contains details for one aspect of the current 90 | state of this API Resource. 91 | properties: 92 | lastTransitionTime: 93 | description: |- 94 | lastTransitionTime is the last time the condition transitioned from one status to another. 95 | This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 96 | format: date-time 97 | type: string 98 | message: 99 | description: |- 100 | message is a human readable message indicating details about the transition. 101 | This may be an empty string. 102 | maxLength: 32768 103 | type: string 104 | observedGeneration: 105 | description: |- 106 | observedGeneration represents the .metadata.generation that the condition was set based upon. 107 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 108 | with respect to the current state of the instance. 109 | format: int64 110 | minimum: 0 111 | type: integer 112 | reason: 113 | description: |- 114 | reason contains a programmatic identifier indicating the reason for the condition's last transition. 115 | Producers of specific condition types may define expected values and meanings for this field, 116 | and whether the values are considered a guaranteed API. 117 | The value should be a CamelCase string. 118 | This field may not be empty. 119 | maxLength: 1024 120 | minLength: 1 121 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 122 | type: string 123 | status: 124 | description: status of the condition, one of True, False, Unknown. 125 | enum: 126 | - "True" 127 | - "False" 128 | - Unknown 129 | type: string 130 | type: 131 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 132 | maxLength: 316 133 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 134 | type: string 135 | required: 136 | - lastTransitionTime 137 | - message 138 | - reason 139 | - status 140 | - type 141 | type: object 142 | type: array 143 | x-kubernetes-list-map-keys: 144 | - type 145 | x-kubernetes-list-type: map 146 | type: object 147 | type: object 148 | served: true 149 | storage: true 150 | subresources: 151 | status: {} 152 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: sample-external-issuer-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: sample-external-issuer- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | # [METRICS] Expose the controller manager metrics service. 29 | - metrics_service.yaml 30 | # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. 31 | # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. 32 | # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will 33 | # be able to communicate with the Webhook Server. 34 | #- ../network-policy 35 | 36 | # Uncomment the patches line if you enable Metrics 37 | patches: 38 | # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port 8443. 39 | # More info: https://book.kubebuilder.io/reference/metrics 40 | - path: manager_metrics_patch.yaml 41 | target: 42 | kind: Deployment 43 | 44 | # Uncomment the patches line if you enable Metrics and CertManager 45 | # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. 46 | # This patch will protect the metrics with certManager self-signed certs. 47 | #- path: cert_metrics_manager_patch.yaml 48 | # target: 49 | # kind: Deployment 50 | 51 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 52 | # crd/kustomization.yaml 53 | #- path: manager_webhook_patch.yaml 54 | # target: 55 | # kind: Deployment 56 | 57 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 58 | # Uncomment the following replacements to add the cert-manager CA injection annotations 59 | #replacements: 60 | # - source: # Uncomment the following block to enable certificates for metrics 61 | # kind: Service 62 | # version: v1 63 | # name: controller-manager-metrics-service 64 | # fieldPath: metadata.name 65 | # targets: 66 | # - select: 67 | # kind: Certificate 68 | # group: cert-manager.io 69 | # version: v1 70 | # name: metrics-certs 71 | # fieldPaths: 72 | # - spec.dnsNames.0 73 | # - spec.dnsNames.1 74 | # options: 75 | # delimiter: '.' 76 | # index: 0 77 | # create: true 78 | # 79 | # - source: 80 | # kind: Service 81 | # version: v1 82 | # name: controller-manager-metrics-service 83 | # fieldPath: metadata.namespace 84 | # targets: 85 | # - select: 86 | # kind: Certificate 87 | # group: cert-manager.io 88 | # version: v1 89 | # name: metrics-certs 90 | # fieldPaths: 91 | # - spec.dnsNames.0 92 | # - spec.dnsNames.1 93 | # options: 94 | # delimiter: '.' 95 | # index: 1 96 | # create: true 97 | # 98 | # - source: # Uncomment the following block if you have any webhook 99 | # kind: Service 100 | # version: v1 101 | # name: webhook-service 102 | # fieldPath: .metadata.name # Name of the service 103 | # targets: 104 | # - select: 105 | # kind: Certificate 106 | # group: cert-manager.io 107 | # version: v1 108 | # name: serving-cert 109 | # fieldPaths: 110 | # - .spec.dnsNames.0 111 | # - .spec.dnsNames.1 112 | # options: 113 | # delimiter: '.' 114 | # index: 0 115 | # create: true 116 | # - source: 117 | # kind: Service 118 | # version: v1 119 | # name: webhook-service 120 | # fieldPath: .metadata.namespace # Namespace of the service 121 | # targets: 122 | # - select: 123 | # kind: Certificate 124 | # group: cert-manager.io 125 | # version: v1 126 | # name: serving-cert 127 | # fieldPaths: 128 | # - .spec.dnsNames.0 129 | # - .spec.dnsNames.1 130 | # options: 131 | # delimiter: '.' 132 | # index: 1 133 | # create: true 134 | # 135 | # - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) 136 | # kind: Certificate 137 | # group: cert-manager.io 138 | # version: v1 139 | # name: serving-cert # This name should match the one in certificate.yaml 140 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 141 | # targets: 142 | # - select: 143 | # kind: ValidatingWebhookConfiguration 144 | # fieldPaths: 145 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 146 | # options: 147 | # delimiter: '/' 148 | # index: 0 149 | # create: true 150 | # - source: 151 | # kind: Certificate 152 | # group: cert-manager.io 153 | # version: v1 154 | # name: serving-cert 155 | # fieldPath: .metadata.name 156 | # targets: 157 | # - select: 158 | # kind: ValidatingWebhookConfiguration 159 | # fieldPaths: 160 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 161 | # options: 162 | # delimiter: '/' 163 | # index: 1 164 | # create: true 165 | # 166 | # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) 167 | # kind: Certificate 168 | # group: cert-manager.io 169 | # version: v1 170 | # name: serving-cert 171 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 172 | # targets: 173 | # - select: 174 | # kind: MutatingWebhookConfiguration 175 | # fieldPaths: 176 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 177 | # options: 178 | # delimiter: '/' 179 | # index: 0 180 | # create: true 181 | # - source: 182 | # kind: Certificate 183 | # group: cert-manager.io 184 | # version: v1 185 | # name: serving-cert 186 | # fieldPath: .metadata.name 187 | # targets: 188 | # - select: 189 | # kind: MutatingWebhookConfiguration 190 | # fieldPaths: 191 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 192 | # options: 193 | # delimiter: '/' 194 | # index: 1 195 | # create: true 196 | # 197 | # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) 198 | # kind: Certificate 199 | # group: cert-manager.io 200 | # version: v1 201 | # name: serving-cert 202 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 203 | # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 204 | # +kubebuilder:scaffold:crdkustomizecainjectionns 205 | # - source: 206 | # kind: Certificate 207 | # group: cert-manager.io 208 | # version: v1 209 | # name: serving-cert 210 | # fieldPath: .metadata.name 211 | # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 212 | # +kubebuilder:scaffold:crdkustomizecainjectionname 213 | -------------------------------------------------------------------------------- /internal/controllers/signer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "crypto/x509" 22 | "errors" 23 | "fmt" 24 | "time" 25 | 26 | "github.com/cert-manager/cert-manager/pkg/util/pki" 27 | issuerapi "github.com/cert-manager/issuer-lib/api/v1alpha1" 28 | "github.com/cert-manager/issuer-lib/controllers" 29 | "github.com/cert-manager/issuer-lib/controllers/signer" 30 | corev1 "k8s.io/api/core/v1" 31 | "k8s.io/apimachinery/pkg/types" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | 35 | sampleissuerapi "github.com/cert-manager/sample-external-issuer/api/v1alpha1" 36 | ) 37 | 38 | var ( 39 | errGetAuthSecret = errors.New("failed to get Secret containing Issuer credentials") 40 | errHealthCheckerBuilder = errors.New("failed to build the healthchecker") 41 | errHealthCheckerCheck = errors.New("healthcheck failed") 42 | 43 | errSignerBuilder = errors.New("failed to build the signer") 44 | errSignerSign = errors.New("failed to sign") 45 | ) 46 | 47 | type HealthChecker interface { 48 | Check() error 49 | } 50 | 51 | type HealthCheckerBuilder func(*sampleissuerapi.IssuerSpec, map[string][]byte) (HealthChecker, error) 52 | 53 | type Signer interface { 54 | Sign(*x509.Certificate) ([]byte, error) 55 | } 56 | 57 | type SignerBuilder func(*sampleissuerapi.IssuerSpec, map[string][]byte) (Signer, error) 58 | 59 | type Issuer struct { 60 | HealthCheckerBuilder HealthCheckerBuilder 61 | SignerBuilder SignerBuilder 62 | ClusterResourceNamespace string 63 | 64 | client client.Client 65 | } 66 | 67 | // +kubebuilder:rbac:groups=sample-issuer.example.com,resources=sampleclusterissuers;sampleissuers,verbs=get;list;watch 68 | // +kubebuilder:rbac:groups=sample-issuer.example.com,resources=sampleclusterissuers/status;sampleissuers/status,verbs=patch 69 | // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch 70 | // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch 71 | // +kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests,verbs=get;list;watch 72 | // +kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests/status,verbs=patch 73 | // +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests,verbs=get;list;watch 74 | // +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/status,verbs=patch 75 | // +kubebuilder:rbac:groups=certificates.k8s.io,resources=signers,verbs=sign,resourceNames=sampleclusterissuers.sample-issuer.example.com/*;sampleissuers.sample-issuer.example.com/* 76 | 77 | func (s Issuer) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { 78 | s.client = mgr.GetClient() 79 | 80 | return (&controllers.CombinedController{ 81 | IssuerTypes: []issuerapi.Issuer{&sampleissuerapi.SampleIssuer{}}, 82 | ClusterIssuerTypes: []issuerapi.Issuer{&sampleissuerapi.SampleClusterIssuer{}}, 83 | 84 | FieldOwner: "sampleissuer.cert-manager.io", 85 | MaxRetryDuration: 1 * time.Minute, 86 | 87 | Sign: s.Sign, 88 | Check: s.Check, 89 | EventRecorder: mgr.GetEventRecorderFor("sampleissuer.cert-manager.io"), 90 | }).SetupWithManager(ctx, mgr) 91 | } 92 | 93 | func (o *Issuer) getIssuerDetails(issuerObject issuerapi.Issuer) (*sampleissuerapi.IssuerSpec, string, error) { 94 | switch t := issuerObject.(type) { 95 | case *sampleissuerapi.SampleIssuer: 96 | return &t.Spec, issuerObject.GetNamespace(), nil 97 | case *sampleissuerapi.SampleClusterIssuer: 98 | return &t.Spec, o.ClusterResourceNamespace, nil 99 | default: 100 | // A permanent error will cause the Issuer to not retry until the 101 | // Issuer is updated. 102 | return nil, "", signer.PermanentError{ 103 | Err: fmt.Errorf("unexpected issuer type: %t", issuerObject), 104 | } 105 | } 106 | } 107 | 108 | func (o *Issuer) getSecretData(ctx context.Context, issuerSpec *sampleissuerapi.IssuerSpec, namespace string) (map[string][]byte, error) { 109 | secretName := types.NamespacedName{ 110 | Namespace: namespace, 111 | Name: issuerSpec.AuthSecretName, 112 | } 113 | 114 | var secret corev1.Secret 115 | if err := o.client.Get(ctx, secretName, &secret); err != nil { 116 | return nil, fmt.Errorf("%w, secret name: %s, reason: %v", errGetAuthSecret, secretName, err) 117 | } 118 | 119 | checker, err := o.HealthCheckerBuilder(issuerSpec, secret.Data) 120 | if err != nil { 121 | return nil, fmt.Errorf("%w: %v", errHealthCheckerBuilder, err) 122 | } 123 | 124 | if err := checker.Check(); err != nil { 125 | return nil, fmt.Errorf("%w: %v", errHealthCheckerCheck, err) 126 | } 127 | 128 | return secret.Data, nil 129 | } 130 | 131 | // Check checks that the CA it is available. Certificate requests will not be 132 | // processed until this check passes. 133 | func (o *Issuer) Check(ctx context.Context, issuerObject issuerapi.Issuer) error { 134 | issuerSpec, namespace, err := o.getIssuerDetails(issuerObject) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | _, err = o.getSecretData(ctx, issuerSpec, namespace) 140 | return err 141 | } 142 | 143 | // Sign returns a signed certificate for the supplied CertificateRequestObject (a cert-manager CertificateRequest resource or 144 | // a kubernetes CertificateSigningRequest resource). The CertificateRequestObject contains a GetRequest method that returns 145 | // a certificate template that can be used as a starting point for the generated certificate. 146 | // The Sign method should return a PEMBundle containing the signed certificate and any intermediate certificates (see the PEMBundle docs for more information). 147 | // If the Sign method returns an error, the issuance will be retried until the MaxRetryDuration is reached. 148 | // Special errors and cases can be found in the issuer-lib README: https://github.com/cert-manager/issuer-lib/tree/main?tab=readme-ov-file#how-it-works 149 | func (o *Issuer) Sign(ctx context.Context, cr signer.CertificateRequestObject, issuerObject issuerapi.Issuer) (signer.PEMBundle, error) { 150 | issuerSpec, namespace, err := o.getIssuerDetails(issuerObject) 151 | if err != nil { 152 | // Returning an IssuerError will change the status of the Issuer to Failed too. 153 | return signer.PEMBundle{}, signer.IssuerError{ 154 | Err: err, 155 | } 156 | } 157 | 158 | secretData, err := o.getSecretData(ctx, issuerSpec, namespace) 159 | if err != nil { 160 | // Returning an IssuerError will change the status of the Issuer to Failed too. 161 | return signer.PEMBundle{}, signer.IssuerError{ 162 | Err: err, 163 | } 164 | } 165 | 166 | certDetails, err := cr.GetCertificateDetails() 167 | if err != nil { 168 | return signer.PEMBundle{}, err 169 | } 170 | 171 | certTemplate, err := certDetails.CertificateTemplate() 172 | if err != nil { 173 | return signer.PEMBundle{}, err 174 | } 175 | 176 | signerObj, err := o.SignerBuilder(issuerSpec, secretData) 177 | if err != nil { 178 | return signer.PEMBundle{}, fmt.Errorf("%w: %v", errSignerBuilder, err) 179 | } 180 | 181 | signed, err := signerObj.Sign(certTemplate) 182 | if err != nil { 183 | return signer.PEMBundle{}, fmt.Errorf("%w: %v", errSignerSign, err) 184 | } 185 | 186 | bundle, err := pki.ParseSingleCertificateChainPEM(signed) 187 | if err != nil { 188 | return signer.PEMBundle{}, err 189 | } 190 | 191 | return signer.PEMBundle(bundle), nil 192 | } 193 | -------------------------------------------------------------------------------- /test/utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "bufio" 21 | "bytes" 22 | "fmt" 23 | "os" 24 | "os/exec" 25 | "strings" 26 | 27 | . "github.com/onsi/ginkgo/v2" 28 | ) 29 | 30 | const ( 31 | prometheusOperatorVersion = "v0.79.2" 32 | prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + 33 | "releases/download/%s/bundle.yaml" 34 | 35 | certmanagerVersion = "v1.16.3" 36 | certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" 37 | ) 38 | 39 | func warnError(err error) { 40 | _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) 41 | } 42 | 43 | // Run executes the provided command within this context 44 | func Run(cmd *exec.Cmd) (string, error) { 45 | dir, _ := GetProjectDir() 46 | cmd.Dir = dir 47 | 48 | if err := os.Chdir(cmd.Dir); err != nil { 49 | _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) 50 | } 51 | 52 | cmd.Env = append(os.Environ(), "GO111MODULE=on") 53 | command := strings.Join(cmd.Args, " ") 54 | _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) 55 | output, err := cmd.CombinedOutput() 56 | if err != nil { 57 | return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) 58 | } 59 | 60 | return string(output), nil 61 | } 62 | 63 | // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. 64 | func InstallPrometheusOperator() error { 65 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 66 | cmd := exec.Command("kubectl", "create", "-f", url) 67 | _, err := Run(cmd) 68 | return err 69 | } 70 | 71 | // UninstallPrometheusOperator uninstalls the prometheus 72 | func UninstallPrometheusOperator() { 73 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 74 | cmd := exec.Command("kubectl", "delete", "-f", url) 75 | if _, err := Run(cmd); err != nil { 76 | warnError(err) 77 | } 78 | } 79 | 80 | // IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed 81 | // by verifying the existence of key CRDs related to Prometheus. 82 | func IsPrometheusCRDsInstalled() bool { 83 | // List of common Prometheus CRDs 84 | prometheusCRDs := []string{ 85 | "prometheuses.monitoring.coreos.com", 86 | "prometheusrules.monitoring.coreos.com", 87 | "prometheusagents.monitoring.coreos.com", 88 | } 89 | 90 | cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name") 91 | output, err := Run(cmd) 92 | if err != nil { 93 | return false 94 | } 95 | crdList := GetNonEmptyLines(output) 96 | for _, crd := range prometheusCRDs { 97 | for _, line := range crdList { 98 | if strings.Contains(line, crd) { 99 | return true 100 | } 101 | } 102 | } 103 | 104 | return false 105 | } 106 | 107 | // UninstallCertManager uninstalls the cert manager 108 | func UninstallCertManager() { 109 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 110 | cmd := exec.Command("kubectl", "delete", "-f", url) 111 | if _, err := Run(cmd); err != nil { 112 | warnError(err) 113 | } 114 | } 115 | 116 | // InstallCertManager installs the cert manager bundle. 117 | func InstallCertManager() error { 118 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 119 | cmd := exec.Command("kubectl", "apply", "-f", url) 120 | if _, err := Run(cmd); err != nil { 121 | return err 122 | } 123 | // Wait for cert-manager-webhook to be ready, which can take time if cert-manager 124 | // was re-installed after uninstalling on a cluster. 125 | cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", 126 | "--for", "condition=Available", 127 | "--namespace", "cert-manager", 128 | "--timeout", "5m", 129 | ) 130 | 131 | _, err := Run(cmd) 132 | return err 133 | } 134 | 135 | // IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed 136 | // by verifying the existence of key CRDs related to Cert Manager. 137 | func IsCertManagerCRDsInstalled() bool { 138 | // List of common Cert Manager CRDs 139 | certManagerCRDs := []string{ 140 | "certificates.cert-manager.io", 141 | "issuers.cert-manager.io", 142 | "clusterissuers.cert-manager.io", 143 | "certificaterequests.cert-manager.io", 144 | "orders.acme.cert-manager.io", 145 | "challenges.acme.cert-manager.io", 146 | } 147 | 148 | // Execute the kubectl command to get all CRDs 149 | cmd := exec.Command("kubectl", "get", "crds") 150 | output, err := Run(cmd) 151 | if err != nil { 152 | return false 153 | } 154 | 155 | // Check if any of the Cert Manager CRDs are present 156 | crdList := GetNonEmptyLines(output) 157 | for _, crd := range certManagerCRDs { 158 | for _, line := range crdList { 159 | if strings.Contains(line, crd) { 160 | return true 161 | } 162 | } 163 | } 164 | 165 | return false 166 | } 167 | 168 | // LoadImageToKindClusterWithName loads a local docker image to the kind cluster 169 | func LoadImageToKindClusterWithName(name string) error { 170 | cluster := "kind" 171 | if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { 172 | cluster = v 173 | } 174 | kindOptions := []string{"load", "docker-image", name, "--name", cluster} 175 | cmd := exec.Command("kind", kindOptions...) 176 | _, err := Run(cmd) 177 | return err 178 | } 179 | 180 | // GetNonEmptyLines converts given command output string into individual objects 181 | // according to line breakers, and ignores the empty elements in it. 182 | func GetNonEmptyLines(output string) []string { 183 | var res []string 184 | elements := strings.Split(output, "\n") 185 | for _, element := range elements { 186 | if element != "" { 187 | res = append(res, element) 188 | } 189 | } 190 | 191 | return res 192 | } 193 | 194 | // GetProjectDir will return the directory where the project is 195 | func GetProjectDir() (string, error) { 196 | wd, err := os.Getwd() 197 | if err != nil { 198 | return wd, err 199 | } 200 | wd = strings.ReplaceAll(wd, "/test/e2e", "") 201 | return wd, nil 202 | } 203 | 204 | // UncommentCode searches for target in the file and remove the comment prefix 205 | // of the target content. The target content may span multiple lines. 206 | func UncommentCode(filename, target, prefix string) error { 207 | // false positive 208 | // nolint:gosec 209 | content, err := os.ReadFile(filename) 210 | if err != nil { 211 | return err 212 | } 213 | strContent := string(content) 214 | 215 | idx := strings.Index(strContent, target) 216 | if idx < 0 { 217 | return fmt.Errorf("unable to find the code %s to be uncomment", target) 218 | } 219 | 220 | out := new(bytes.Buffer) 221 | _, err = out.Write(content[:idx]) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | scanner := bufio.NewScanner(bytes.NewBufferString(target)) 227 | if !scanner.Scan() { 228 | return nil 229 | } 230 | for { 231 | _, err := out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)) 232 | if err != nil { 233 | return err 234 | } 235 | // Avoid writing a newline in case the previous line was the last in target. 236 | if !scanner.Scan() { 237 | break 238 | } 239 | if _, err := out.WriteString("\n"); err != nil { 240 | return err 241 | } 242 | } 243 | 244 | _, err = out.Write(content[idx+len(target):]) 245 | if err != nil { 246 | return err 247 | } 248 | // false positive 249 | // nolint:gosec 250 | return os.WriteFile(filename, out.Bytes(), 0644) 251 | } 252 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | IMG ?= controller:latest 3 | 4 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 5 | ifeq (,$(shell go env GOBIN)) 6 | GOBIN=$(shell go env GOPATH)/bin 7 | else 8 | GOBIN=$(shell go env GOBIN) 9 | endif 10 | 11 | # CONTAINER_TOOL defines the container tool to be used for building images. 12 | # Be aware that the target commands are only tested with Docker which is 13 | # scaffolded by default. However, you might want to replace it to use other 14 | # tools. (i.e. podman) 15 | CONTAINER_TOOL ?= docker 16 | 17 | # Setting SHELL to bash allows bash commands to be executed by recipes. 18 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 19 | SHELL = /usr/bin/env bash -o pipefail 20 | .SHELLFLAGS = -ec 21 | 22 | .PHONY: all 23 | all: build 24 | 25 | ##@ General 26 | 27 | # The help target prints out all targets with their descriptions organized 28 | # beneath their categories. The categories are represented by '##@' and the 29 | # target descriptions by '##'. The awk command is responsible for reading the 30 | # entire set of makefiles included in this invocation, looking for lines of the 31 | # file as xyz: ## something, and then pretty-format the target and help. Then, 32 | # if there's a line with ##@ something, that gets pretty-printed as a category. 33 | # More info on the usage of ANSI control characters for terminal formatting: 34 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 35 | # More info on the awk command: 36 | # http://linuxcommand.org/lc3_adv_awk.php 37 | 38 | .PHONY: help 39 | help: ## Display this help. 40 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 41 | 42 | ##@ Development 43 | 44 | .PHONY: manifests 45 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 46 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 47 | 48 | .PHONY: generate 49 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 50 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 51 | 52 | .PHONY: fmt 53 | fmt: ## Run go fmt against code. 54 | go fmt ./... 55 | 56 | .PHONY: vet 57 | vet: ## Run go vet against code. 58 | go vet ./... 59 | 60 | .PHONY: test 61 | test: manifests generate fmt vet setup-envtest ## Run tests. 62 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out 63 | 64 | # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. 65 | # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. 66 | # Prometheus and CertManager are installed by default; skip with: 67 | # - PROMETHEUS_INSTALL_SKIP=true 68 | # - CERT_MANAGER_INSTALL_SKIP=true 69 | .PHONY: test-e2e 70 | test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. 71 | @command -v kind >/dev/null 2>&1 || { \ 72 | echo "Kind is not installed. Please install Kind manually."; \ 73 | exit 1; \ 74 | } 75 | @kind get clusters | grep -q 'kind' || { \ 76 | echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ 77 | exit 1; \ 78 | } 79 | go test ./test/e2e/ -v -ginkgo.v 80 | 81 | .PHONY: lint 82 | lint: golangci-lint ## Run golangci-lint linter 83 | $(GOLANGCI_LINT) run 84 | 85 | .PHONY: lint-fix 86 | lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes 87 | $(GOLANGCI_LINT) run --fix 88 | 89 | .PHONY: lint-config 90 | lint-config: golangci-lint ## Verify golangci-lint linter configuration 91 | $(GOLANGCI_LINT) config verify 92 | 93 | ##@ Build 94 | 95 | .PHONY: build 96 | build: manifests generate fmt vet ## Build manager binary. 97 | go build -o bin/manager cmd/main.go 98 | 99 | .PHONY: run 100 | run: manifests generate fmt vet ## Run a controller from your host. 101 | go run ./cmd/main.go 102 | 103 | # If you wish to build the manager image targeting other platforms you can use the --platform flag. 104 | # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. 105 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 106 | .PHONY: docker-build 107 | docker-build: ## Build docker image with the manager. 108 | $(CONTAINER_TOOL) build -t ${IMG} . 109 | 110 | .PHONY: docker-push 111 | docker-push: ## Push docker image with the manager. 112 | $(CONTAINER_TOOL) push ${IMG} 113 | 114 | # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple 115 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 116 | # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ 117 | # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 118 | # - 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) 119 | # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. 120 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 121 | .PHONY: docker-buildx 122 | docker-buildx: ## Build and push docker image for the manager for cross-platform support 123 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 124 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 125 | - $(CONTAINER_TOOL) buildx create --name sample-external-issuer-builder 126 | $(CONTAINER_TOOL) buildx use sample-external-issuer-builder 127 | - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 128 | - $(CONTAINER_TOOL) buildx rm sample-external-issuer-builder 129 | rm Dockerfile.cross 130 | 131 | .PHONY: build-installer 132 | build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. 133 | mkdir -p dist 134 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 135 | $(KUSTOMIZE) build config/default > dist/install.yaml 136 | 137 | ##@ Deployment 138 | 139 | ifndef ignore-not-found 140 | ignore-not-found = false 141 | endif 142 | 143 | .PHONY: install 144 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 145 | $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 146 | 147 | .PHONY: uninstall 148 | 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. 149 | $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 150 | 151 | .PHONY: deploy 152 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 153 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 154 | $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - 155 | 156 | .PHONY: undeploy 157 | 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. 158 | $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 159 | 160 | ##@ Dependencies 161 | 162 | ## Location to install dependencies to 163 | LOCALBIN ?= $(shell pwd)/bin 164 | $(LOCALBIN): 165 | mkdir -p $(LOCALBIN) 166 | 167 | ## Tool Binaries 168 | KUBECTL ?= kubectl 169 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 170 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 171 | ENVTEST ?= $(LOCALBIN)/setup-envtest 172 | GOLANGCI_LINT = $(LOCALBIN)/golangci-lint 173 | 174 | ## Tool Versions 175 | KUSTOMIZE_VERSION ?= v5.6.0 176 | CONTROLLER_TOOLS_VERSION ?= v0.17.1 177 | #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) 178 | ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') 179 | #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) 180 | ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') 181 | GOLANGCI_LINT_VERSION ?= v1.63.4 182 | 183 | .PHONY: kustomize 184 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 185 | $(KUSTOMIZE): $(LOCALBIN) 186 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) 187 | 188 | .PHONY: controller-gen 189 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 190 | $(CONTROLLER_GEN): $(LOCALBIN) 191 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) 192 | 193 | .PHONY: setup-envtest 194 | setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. 195 | @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." 196 | @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \ 197 | echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ 198 | exit 1; \ 199 | } 200 | 201 | .PHONY: envtest 202 | envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. 203 | $(ENVTEST): $(LOCALBIN) 204 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) 205 | 206 | .PHONY: golangci-lint 207 | golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. 208 | $(GOLANGCI_LINT): $(LOCALBIN) 209 | $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) 210 | 211 | # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist 212 | # $1 - target path with name of binary 213 | # $2 - package url which can be installed 214 | # $3 - specific version of package 215 | define go-install-tool 216 | @[ -f "$(1)-$(3)" ] || { \ 217 | set -e; \ 218 | package=$(2)@$(3) ;\ 219 | echo "Downloading $${package}" ;\ 220 | rm -f $(1) || true ;\ 221 | GOBIN=$(LOCALBIN) go install $${package} ;\ 222 | mv $(1) $(1)-$(3) ;\ 223 | } ;\ 224 | ln -sf $(1)-$(3) $(1) 225 | endef 226 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "crypto/tls" 22 | "errors" 23 | "flag" 24 | "fmt" 25 | "os" 26 | "path/filepath" 27 | 28 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 29 | // to ensure that exec-entrypoint and run can make use of them. 30 | _ "k8s.io/client-go/plugin/pkg/client/auth" 31 | 32 | cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" 33 | "k8s.io/apimachinery/pkg/runtime" 34 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 35 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 36 | "k8s.io/klog/v2" 37 | ctrl "sigs.k8s.io/controller-runtime" 38 | "sigs.k8s.io/controller-runtime/pkg/certwatcher" 39 | "sigs.k8s.io/controller-runtime/pkg/healthz" 40 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 41 | "sigs.k8s.io/controller-runtime/pkg/metrics/filters" 42 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 43 | "sigs.k8s.io/controller-runtime/pkg/webhook" 44 | 45 | "github.com/cert-manager/sample-external-issuer/internal/controllers" 46 | "github.com/cert-manager/sample-external-issuer/internal/signer" 47 | "github.com/cert-manager/sample-external-issuer/internal/version" 48 | 49 | sampleissuerv1alpha1 "github.com/cert-manager/sample-external-issuer/api/v1alpha1" 50 | // +kubebuilder:scaffold:imports 51 | ) 52 | 53 | const inClusterNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 54 | 55 | var ( 56 | scheme = runtime.NewScheme() 57 | setupLog = ctrl.Log.WithName("setup") 58 | ) 59 | 60 | func init() { 61 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 62 | utilruntime.Must(cmapi.AddToScheme(scheme)) 63 | 64 | utilruntime.Must(sampleissuerv1alpha1.AddToScheme(scheme)) 65 | // +kubebuilder:scaffold:scheme 66 | } 67 | 68 | // nolint:gocyclo 69 | func main() { 70 | var clusterResourceNamespace string 71 | var printVersion bool 72 | flag.StringVar(&clusterResourceNamespace, "cluster-resource-namespace", "", 73 | "The namespace for secrets in which cluster-scoped resources are found.") 74 | flag.BoolVar(&printVersion, "version", false, "Print version to stdout and exit") 75 | 76 | var metricsAddr string 77 | var metricsCertPath, metricsCertName, metricsCertKey string 78 | var webhookCertPath, webhookCertName, webhookCertKey string 79 | var enableLeaderElection bool 80 | var probeAddr string 81 | var secureMetrics bool 82 | var enableHTTP2 bool 83 | var tlsOpts []func(*tls.Config) 84 | flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address to which the metrics endpoint binds. "+ 85 | "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") 86 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address to which the probe endpoint binds.") 87 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 88 | "Enable leader election for controller manager. "+ 89 | "Enabling this will ensure there is only one active controller manager.") 90 | flag.BoolVar(&secureMetrics, "metrics-secure", true, 91 | "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") 92 | flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") 93 | flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") 94 | flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") 95 | flag.StringVar(&metricsCertPath, "metrics-cert-path", "", 96 | "The directory that contains the metrics server certificate.") 97 | flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") 98 | flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") 99 | flag.BoolVar(&enableHTTP2, "enable-http2", false, 100 | "If set, HTTP/2 will be enabled for the metrics and webhook servers.") 101 | opts := zap.Options{ 102 | Development: true, 103 | } 104 | opts.BindFlags(flag.CommandLine) 105 | flag.Parse() 106 | 107 | logr := zap.New(zap.UseFlagOptions(&opts)) 108 | klog.SetLogger(logr) 109 | ctrl.SetLogger(logr) 110 | 111 | logr.Info("Version", "version", version.Version) 112 | 113 | if printVersion { 114 | return 115 | } 116 | 117 | if err := getInClusterNamespace(&clusterResourceNamespace); err != nil { 118 | if errors.Is(err, errNotInCluster) { 119 | setupLog.Error(err, "please supply --cluster-resource-namespace") 120 | } else { 121 | setupLog.Error(err, "unexpected error while getting in-cluster Namespace") 122 | } 123 | os.Exit(1) 124 | } 125 | 126 | // if the enable-http2 flag is false (the default), http/2 should be disabled 127 | // due to its vulnerabilities. More specifically, disabling http/2 will 128 | // prevent from being vulnerable to the HTTP/2 Stream Cancellation and 129 | // Rapid Reset CVEs. For more information see: 130 | // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 131 | // - https://github.com/advisories/GHSA-4374-p667-p6c8 132 | disableHTTP2 := func(c *tls.Config) { 133 | setupLog.Info("disabling http/2") 134 | c.NextProtos = []string{"http/1.1"} 135 | } 136 | 137 | if !enableHTTP2 { 138 | tlsOpts = append(tlsOpts, disableHTTP2) 139 | } 140 | 141 | // Create watchers for metrics and webhooks certificates 142 | var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher 143 | 144 | // Initial webhook TLS options 145 | webhookTLSOpts := tlsOpts 146 | 147 | if len(webhookCertPath) > 0 { 148 | setupLog.Info("Initializing webhook certificate watcher using provided certificates", 149 | "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) 150 | 151 | var err error 152 | webhookCertWatcher, err = certwatcher.New( 153 | filepath.Join(webhookCertPath, webhookCertName), 154 | filepath.Join(webhookCertPath, webhookCertKey), 155 | ) 156 | if err != nil { 157 | setupLog.Error(err, "Failed to initialize webhook certificate watcher") 158 | os.Exit(1) 159 | } 160 | 161 | webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { 162 | config.GetCertificate = webhookCertWatcher.GetCertificate 163 | }) 164 | } 165 | 166 | webhookServer := webhook.NewServer(webhook.Options{ 167 | TLSOpts: webhookTLSOpts, 168 | }) 169 | 170 | // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. 171 | // More info: 172 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.4/pkg/metrics/server 173 | // - https://book.kubebuilder.io/reference/metrics.html 174 | metricsServerOptions := metricsserver.Options{ 175 | BindAddress: metricsAddr, 176 | SecureServing: secureMetrics, 177 | TLSOpts: tlsOpts, 178 | } 179 | 180 | if secureMetrics { 181 | // FilterProvider is used to protect the metrics endpoint with authn/authz. 182 | // These configurations ensure that only authorized users and service accounts 183 | // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: 184 | // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.4/pkg/metrics/filters#WithAuthenticationAndAuthorization 185 | metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization 186 | } 187 | 188 | // If the certificate is not specified, controller-runtime will automatically 189 | // generate a self-signed certificate for the metrics server. While convenient for development and testing, 190 | // this setup is not recommended for production. 191 | // 192 | // TODO(user): If you enable certManager, uncomment the following lines: 193 | // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates 194 | // managed by cert-manager for the metrics server. 195 | // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. 196 | if len(metricsCertPath) > 0 { 197 | setupLog.Info("Initializing metrics certificate watcher using provided certificates", 198 | "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) 199 | 200 | var err error 201 | metricsCertWatcher, err = certwatcher.New( 202 | filepath.Join(metricsCertPath, metricsCertName), 203 | filepath.Join(metricsCertPath, metricsCertKey), 204 | ) 205 | if err != nil { 206 | setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) 207 | os.Exit(1) 208 | } 209 | 210 | metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { 211 | config.GetCertificate = metricsCertWatcher.GetCertificate 212 | }) 213 | } 214 | 215 | setupLog.Info( 216 | "starting", 217 | "version", version.Version, 218 | "enable-leader-election", enableLeaderElection, 219 | "metrics-addr", metricsAddr, 220 | "cluster-resource-namespace", clusterResourceNamespace, 221 | ) 222 | 223 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 224 | Scheme: scheme, 225 | Metrics: metricsServerOptions, 226 | WebhookServer: webhookServer, 227 | HealthProbeBindAddress: probeAddr, 228 | LeaderElection: enableLeaderElection, 229 | LeaderElectionID: "54c549fd.example.com", 230 | // LeaderElectionReleaseOnCancel defines whether the leader should step down voluntarily 231 | // when the Manager ends. This requires the binary to immediately end when the 232 | // Manager is stopped, otherwise, this setting is unsafe. Setting this speeds up 233 | // voluntary leader transitions significantly as the new leader don't have to wait 234 | // LeaseDuration time first. 235 | // 236 | // In the default scaffold provided, the program ends immediately after 237 | // the manager stops, so it would be fine to enable this option. However, 238 | // if you are doing or intend to do any operation such as perform cleanups 239 | // after the manager stops then its usage might be unsafe. 240 | LeaderElectionReleaseOnCancel: true, 241 | }) 242 | if err != nil { 243 | setupLog.Error(err, "unable to start manager") 244 | os.Exit(1) 245 | } 246 | 247 | ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler()) 248 | defer cancel() 249 | 250 | if err = (&controllers.Issuer{ 251 | HealthCheckerBuilder: signer.ExampleHealthCheckerFromIssuerAndSecretData, 252 | SignerBuilder: signer.ExampleSignerFromIssuerAndSecretData, 253 | ClusterResourceNamespace: clusterResourceNamespace, 254 | }).SetupWithManager(ctx, mgr); err != nil { 255 | setupLog.Error(err, "unable to create Signer controllers") 256 | os.Exit(1) 257 | } 258 | 259 | // +kubebuilder:scaffold:builder 260 | 261 | if metricsCertWatcher != nil { 262 | setupLog.Info("Adding metrics certificate watcher to manager") 263 | if err := mgr.Add(metricsCertWatcher); err != nil { 264 | setupLog.Error(err, "unable to add metrics certificate watcher to manager") 265 | os.Exit(1) 266 | } 267 | } 268 | 269 | if webhookCertWatcher != nil { 270 | setupLog.Info("Adding webhook certificate watcher to manager") 271 | if err := mgr.Add(webhookCertWatcher); err != nil { 272 | setupLog.Error(err, "unable to add webhook certificate watcher to manager") 273 | os.Exit(1) 274 | } 275 | } 276 | 277 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 278 | setupLog.Error(err, "unable to set up health check") 279 | os.Exit(1) 280 | } 281 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 282 | setupLog.Error(err, "unable to set up ready check") 283 | os.Exit(1) 284 | } 285 | 286 | setupLog.Info("starting manager") 287 | if err := mgr.Start(ctx); err != nil { 288 | setupLog.Error(err, "problem running manager") 289 | os.Exit(1) 290 | } 291 | } 292 | 293 | var errNotInCluster = errors.New("not running in-cluster") 294 | 295 | // Copied from controller-runtime/pkg/leaderelection 296 | func getInClusterNamespace(clusterResourceNamespace *string) error { 297 | if *clusterResourceNamespace != "" { 298 | return nil 299 | } 300 | 301 | // Check whether the namespace file exists. 302 | // If not, we are not running in cluster so can't guess the namespace. 303 | _, err := os.Stat(inClusterNamespacePath) 304 | if os.IsNotExist(err) { 305 | return errNotInCluster 306 | } else if err != nil { 307 | return fmt.Errorf("error checking namespace file: %w", err) 308 | } 309 | 310 | // Load the namespace file and return its content 311 | namespace, err := os.ReadFile(inClusterNamespacePath) 312 | if err != nil { 313 | return fmt.Errorf("error reading namespace file: %w", err) 314 | } 315 | *clusterResourceNamespace = string(namespace) 316 | 317 | return nil 318 | } 319 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The cert-manager Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "os/exec" 24 | "path/filepath" 25 | "time" 26 | 27 | . "github.com/onsi/ginkgo/v2" 28 | . "github.com/onsi/gomega" 29 | 30 | "github.com/cert-manager/sample-external-issuer/test/utils" 31 | ) 32 | 33 | // namespace where the project is deployed in 34 | const namespace = "sample-external-issuer-system" 35 | 36 | // serviceAccountName created for the project 37 | const serviceAccountName = "sample-external-issuer-controller-manager" 38 | 39 | // metricsServiceName is the name of the metrics service of the project 40 | const metricsServiceName = "sample-external-issuer-controller-manager-metrics-service" 41 | 42 | // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data 43 | const metricsRoleBindingName = "sample-external-issuer-metrics-binding" 44 | 45 | var _ = Describe("Manager", Ordered, func() { 46 | var controllerPodName string 47 | 48 | // Before running the tests, set up the environment by creating the namespace, 49 | // enforce the restricted security policy to the namespace, installing CRDs, 50 | // and deploying the controller. 51 | BeforeAll(func() { 52 | By("creating manager namespace") 53 | cmd := exec.Command("kubectl", "create", "ns", namespace) 54 | _, err := utils.Run(cmd) 55 | Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") 56 | 57 | By("labeling the namespace to enforce the restricted security policy") 58 | cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, 59 | "pod-security.kubernetes.io/enforce=restricted") 60 | _, err = utils.Run(cmd) 61 | Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") 62 | 63 | By("installing CRDs") 64 | cmd = exec.Command("make", "install") 65 | _, err = utils.Run(cmd) 66 | Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") 67 | 68 | By("deploying the controller-manager") 69 | cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) 70 | _, err = utils.Run(cmd) 71 | Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") 72 | }) 73 | 74 | // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, 75 | // and deleting the namespace. 76 | AfterAll(func() { 77 | By("cleaning up the curl pod for metrics") 78 | cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) 79 | _, _ = utils.Run(cmd) 80 | 81 | By("undeploying the controller-manager") 82 | cmd = exec.Command("make", "undeploy") 83 | _, _ = utils.Run(cmd) 84 | 85 | By("uninstalling CRDs") 86 | cmd = exec.Command("make", "uninstall") 87 | _, _ = utils.Run(cmd) 88 | 89 | By("removing manager namespace") 90 | cmd = exec.Command("kubectl", "delete", "ns", namespace) 91 | _, _ = utils.Run(cmd) 92 | }) 93 | 94 | // After each test, check for failures and collect logs, events, 95 | // and pod descriptions for debugging. 96 | AfterEach(func() { 97 | specReport := CurrentSpecReport() 98 | if specReport.Failed() { 99 | By("Fetching controller manager pod logs") 100 | cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) 101 | controllerLogs, err := utils.Run(cmd) 102 | if err == nil { 103 | _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) 104 | } else { 105 | _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) 106 | } 107 | 108 | By("Fetching Kubernetes events") 109 | cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") 110 | eventsOutput, err := utils.Run(cmd) 111 | if err == nil { 112 | _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) 113 | } else { 114 | _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err) 115 | } 116 | 117 | By("Fetching curl-metrics logs") 118 | cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) 119 | metricsOutput, err := utils.Run(cmd) 120 | if err == nil { 121 | _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) 122 | } else { 123 | _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err) 124 | } 125 | 126 | By("Fetching controller manager pod description") 127 | cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) 128 | podDescription, err := utils.Run(cmd) 129 | if err == nil { 130 | fmt.Println("Pod description:\n", podDescription) 131 | } else { 132 | fmt.Println("Failed to describe controller pod") 133 | } 134 | } 135 | }) 136 | 137 | SetDefaultEventuallyTimeout(2 * time.Minute) 138 | SetDefaultEventuallyPollingInterval(time.Second) 139 | 140 | Context("Manager", func() { 141 | It("should run successfully", func() { 142 | By("validating that the controller-manager pod is running as expected") 143 | verifyControllerUp := func(g Gomega) { 144 | // Get the name of the controller-manager pod 145 | cmd := exec.Command("kubectl", "get", 146 | "pods", "-l", "control-plane=controller-manager", 147 | "-o", "go-template={{ range .items }}"+ 148 | "{{ if not .metadata.deletionTimestamp }}"+ 149 | "{{ .metadata.name }}"+ 150 | "{{ \"\\n\" }}{{ end }}{{ end }}", 151 | "-n", namespace, 152 | ) 153 | 154 | podOutput, err := utils.Run(cmd) 155 | g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") 156 | podNames := utils.GetNonEmptyLines(podOutput) 157 | g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") 158 | controllerPodName = podNames[0] 159 | g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) 160 | 161 | // Validate the pod's status 162 | cmd = exec.Command("kubectl", "get", 163 | "pods", controllerPodName, "-o", "jsonpath={.status.phase}", 164 | "-n", namespace, 165 | ) 166 | output, err := utils.Run(cmd) 167 | g.Expect(err).NotTo(HaveOccurred()) 168 | g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") 169 | } 170 | Eventually(verifyControllerUp).Should(Succeed()) 171 | }) 172 | 173 | It("should ensure the metrics endpoint is serving metrics", func() { 174 | By("creating a ClusterRoleBinding for the service account to allow access to metrics") 175 | cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, 176 | "--clusterrole=sample-external-issuer-metrics-reader", 177 | fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), 178 | ) 179 | _, err := utils.Run(cmd) 180 | Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") 181 | 182 | By("validating that the metrics service is available") 183 | cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) 184 | _, err = utils.Run(cmd) 185 | Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") 186 | 187 | By("validating that the ServiceMonitor for Prometheus is applied in the namespace") 188 | cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace) 189 | _, err = utils.Run(cmd) 190 | Expect(err).NotTo(HaveOccurred(), "ServiceMonitor should exist") 191 | 192 | By("getting the service account token") 193 | token, err := serviceAccountToken() 194 | Expect(err).NotTo(HaveOccurred()) 195 | Expect(token).NotTo(BeEmpty()) 196 | 197 | By("waiting for the metrics endpoint to be ready") 198 | verifyMetricsEndpointReady := func(g Gomega) { 199 | cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) 200 | output, err := utils.Run(cmd) 201 | g.Expect(err).NotTo(HaveOccurred()) 202 | g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready") 203 | } 204 | Eventually(verifyMetricsEndpointReady).Should(Succeed()) 205 | 206 | By("verifying that the controller manager is serving the metrics server") 207 | verifyMetricsServerStarted := func(g Gomega) { 208 | cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) 209 | output, err := utils.Run(cmd) 210 | g.Expect(err).NotTo(HaveOccurred()) 211 | g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), 212 | "Metrics server not yet started") 213 | } 214 | Eventually(verifyMetricsServerStarted).Should(Succeed()) 215 | 216 | By("creating the curl-metrics pod to access the metrics endpoint") 217 | cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", 218 | "--namespace", namespace, 219 | "--image=curlimages/curl:latest", 220 | "--overrides", 221 | fmt.Sprintf(`{ 222 | "spec": { 223 | "containers": [{ 224 | "name": "curl", 225 | "image": "curlimages/curl:latest", 226 | "command": ["/bin/sh", "-c"], 227 | "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], 228 | "securityContext": { 229 | "allowPrivilegeEscalation": false, 230 | "capabilities": { 231 | "drop": ["ALL"] 232 | }, 233 | "runAsNonRoot": true, 234 | "runAsUser": 1000, 235 | "seccompProfile": { 236 | "type": "RuntimeDefault" 237 | } 238 | } 239 | }], 240 | "serviceAccount": "%s" 241 | } 242 | }`, token, metricsServiceName, namespace, serviceAccountName)) 243 | _, err = utils.Run(cmd) 244 | Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") 245 | 246 | By("waiting for the curl-metrics pod to complete.") 247 | verifyCurlUp := func(g Gomega) { 248 | cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", 249 | "-o", "jsonpath={.status.phase}", 250 | "-n", namespace) 251 | output, err := utils.Run(cmd) 252 | g.Expect(err).NotTo(HaveOccurred()) 253 | g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") 254 | } 255 | Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) 256 | 257 | By("getting the metrics by checking curl-metrics logs") 258 | metricsOutput := getMetricsOutput() 259 | Expect(metricsOutput).To(ContainSubstring( 260 | "controller_runtime_reconcile_total", 261 | )) 262 | }) 263 | 264 | // +kubebuilder:scaffold:e2e-webhooks-checks 265 | 266 | It("should reconcile sampleissuer and sampleclusterissuer", func() { 267 | By("applying sample resources") 268 | cmd := exec.Command("kubectl", "apply", "--kustomize", "config/samples") 269 | _, err := utils.Run(cmd) 270 | Expect(err).NotTo(HaveOccurred(), "Failed to apply samples") 271 | 272 | By("waiting for SampleIssuer sample resources to become Ready") 273 | cmd = exec.Command("kubectl", "wait", "--for=condition=Ready", "--timeout=5s", 274 | "sampleissuers.sample-issuer.example.com", "sampleissuer-sample") 275 | _, err = utils.Run(cmd) 276 | Expect(err).NotTo(HaveOccurred(), "SampleIssuer did not get Ready") 277 | cmd = exec.Command("kubectl", "wait", "--for=condition=Ready", "--timeout=5s", 278 | "certificaterequests.cert-manager.io", "sampleissuer-sample") 279 | _, err = utils.Run(cmd) 280 | Expect(err).NotTo(HaveOccurred(), "SampleIssuer CertificateRequest did not get Ready") 281 | cmd = exec.Command("kubectl", "wait", "--for=condition=Ready", "--timeout=5s", 282 | "certificates.cert-manager.io", "certificate-by-sampleissuer") 283 | _, err = utils.Run(cmd) 284 | Expect(err).NotTo(HaveOccurred(), "Certificate by SampleIssuer did not get Ready") 285 | 286 | By("waiting for SampleClusterIssuer sample resources to become Ready") 287 | cmd = exec.Command("kubectl", "wait", "--for=condition=Ready", "--timeout=5s", 288 | "sampleclusterissuers.sample-issuer.example.com", "sampleclusterissuer-sample") 289 | _, err = utils.Run(cmd) 290 | Expect(err).NotTo(HaveOccurred(), "SampleClusterIssuer did not get Ready") 291 | cmd = exec.Command("kubectl", "wait", "--for=condition=Ready", "--timeout=5s", 292 | "certificaterequests.cert-manager.io", "sampleclusterissuer-sample") 293 | _, err = utils.Run(cmd) 294 | Expect(err).NotTo(HaveOccurred(), "SampleClusterIssuer CertificateRequest did not get Ready") 295 | cmd = exec.Command("kubectl", "wait", "--for=condition=Ready", "--timeout=5s", 296 | "certificates.cert-manager.io", "certificate-by-sampleclusterissuer") 297 | _, err = utils.Run(cmd) 298 | Expect(err).NotTo(HaveOccurred(), "Certificate by SampleClusterIssuer did not get Ready") 299 | 300 | By("deleting sample resources") 301 | cmd = exec.Command("kubectl", "delete", "--kustomize", "config/samples") 302 | _, _ = utils.Run(cmd) 303 | }) 304 | }) 305 | }) 306 | 307 | // serviceAccountToken returns a token for the specified service account in the given namespace. 308 | // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request 309 | // and parsing the resulting token from the API response. 310 | func serviceAccountToken() (string, error) { 311 | const tokenRequestRawString = `{ 312 | "apiVersion": "authentication.k8s.io/v1", 313 | "kind": "TokenRequest" 314 | }` 315 | 316 | // Temporary file to store the token request 317 | secretName := fmt.Sprintf("%s-token-request", serviceAccountName) 318 | tokenRequestFile := filepath.Join("/tmp", secretName) 319 | err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) 320 | if err != nil { 321 | return "", err 322 | } 323 | 324 | var out string 325 | verifyTokenCreation := func(g Gomega) { 326 | // Execute kubectl command to create the token 327 | cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( 328 | "/api/v1/namespaces/%s/serviceaccounts/%s/token", 329 | namespace, 330 | serviceAccountName, 331 | ), "-f", tokenRequestFile) 332 | 333 | output, err := cmd.CombinedOutput() 334 | g.Expect(err).NotTo(HaveOccurred()) 335 | 336 | // Parse the JSON output to extract the token 337 | var token tokenRequest 338 | err = json.Unmarshal(output, &token) 339 | g.Expect(err).NotTo(HaveOccurred()) 340 | 341 | out = token.Status.Token 342 | } 343 | Eventually(verifyTokenCreation).Should(Succeed()) 344 | 345 | return out, err 346 | } 347 | 348 | // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. 349 | func getMetricsOutput() string { 350 | By("getting the curl-metrics logs") 351 | cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) 352 | metricsOutput, err := utils.Run(cmd) 353 | Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") 354 | Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) 355 | return metricsOutput 356 | } 357 | 358 | // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, 359 | // containing only the token field that we need to extract. 360 | type tokenRequest struct { 361 | Status struct { 362 | Token string `json:"token"` 363 | } `json:"status"` 364 | } 365 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | cert-manager project logo 3 |

4 | 5 | # sample-external-issuer 6 | 7 | External issuers extend [cert-manager](https://cert-manager.io/) to issue certificates using APIs and services 8 | which aren't built into the cert-manager core. 9 | 10 | This repository provides an example of an [External Issuer][] built using the [issuer-lib][] library. 11 | 12 | ## Install 13 | 14 | ```console 15 | kubectl apply -f https://github.com/cert-manager/sample-external-issuer/releases/download/v0.1.0/install.yaml 16 | ``` 17 | 18 | ## Demo 19 | 20 | You can run the sample-external-issuer on a local cluster with this command: 21 | 22 | ```console 23 | make kind-cluster deploy-cert-manager docker-build kind-load deploy e2e 24 | ``` 25 | 26 | ## How to write your own external issuer 27 | 28 | If you are writing an external issuer you may find it helpful to review the sample code in this repository 29 | and to follow the steps below, replacing references to `sample-external-issuer` with the name of your project. 30 | 31 | ### Prerequisites 32 | 33 | You will need the following command line tools installed on your PATH: 34 | 35 | * [Git](https://git-scm.com/) 36 | * [Golang v1.20+](https://golang.org/) 37 | * [Docker v17.03+](https://docs.docker.com/install/) 38 | * [Kind v0.18.0+](https://kind.sigs.k8s.io/docs/user/quick-start/) 39 | * [Kubectl v1.26.3+](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 40 | * [Kubebuilder v3.9.1+](https://book.kubebuilder.io/quick-start.html#installation) 41 | * [Kustomize v3.8.1+](https://kustomize.io/) 42 | 43 | You may also want to read: the [Kubebuilder Book][] and the [cert-manager Concepts Documentation][] for further background 44 | information. 45 | 46 | ### Create a test cluster 47 | 48 | We will need a Kubernetes cluster on which to test our issuer and we can quickly create one using `kind`. 49 | 50 | ```console 51 | kind create cluster 52 | ``` 53 | 54 | This will update your KUBECONFIG file with the URL and credentials for the test cluster. 55 | You can explore it using `kubectl` 56 | 57 | ```console 58 | kubectl get nodes 59 | ``` 60 | 61 | This should show you details of a single node. 62 | 63 | ### Copy the sample-external-issuer code 64 | 65 | We need a Git repository to track changes to the issuer code. 66 | You can start by creating a repository on GitHub or you can create it locally. 67 | 68 | ```console 69 | mkdir my-external-issuer 70 | cd my-external-issuer 71 | git clone https://github.com/cert-manager/sample-external-issuer.git . 72 | git remote rm origin 73 | git remote add origin https://github.com//my-external-issuer.git 74 | ``` 75 | 76 | ### Run the controller-manager 77 | 78 | With all these tools in place and with the project initialised you should now be able to run the issuer for the first time. 79 | 80 | ```console 81 | make run 82 | ``` 83 | 84 | This will compile and run the issuer locally and it will connect to the test cluster and log some startup messages. 85 | We will add more to it in the next steps. 86 | 87 | ### Creating MyIssuer and MyClusterIssuer custom resources 88 | 89 | An [External Issuer][] must implement two custom resources for compatibility with cert-manager: `MyIssuer` and `MyClusterIssuer` 90 | 91 | NOTE: It is important to understand the [Concept of Issuers] before proceeding. 92 | 93 | The `MyIssuer` and `MyClusterIssuer` custom resources can be defined in the `api/v1alpha1` directory. 94 | Use the `SampleIssuer` and `SampleClusterIssuer` definitions as a starting point. 95 | 96 | Additionally, the group, version and kind of the custom resources must be customised to be unique to your issuer: 97 | 98 | * `group` is the name given to a collection of custom resource APIs 99 | * `kind` is the name of an individual resource in that group 100 | * `version` allows you to create multiple versions of your APIs as they evolve, whilst providing backwards compatibility for clients using older API versions 101 | 102 | After modifying the API source files you should always regenerate all generated code and configuration, 103 | as follows: 104 | 105 | ```console 106 | make generate manifests 107 | ``` 108 | 109 | You should see a number of new and modified files, reflecting the changes you made to the API source files. 110 | 111 | #### Issuer health checks 112 | 113 | An issuer that connects to a certificate authority API may want to perform periodic health checks and sanity checks, 114 | to ensure that the API server is responding and if not, 115 | to set update the `Ready` condition of the `Issuer` to false, and log a meaningful error message with the condition. 116 | This will give early warning of problems with the configuration or with the API, 117 | rather than waiting a for `CertificateRequest` to fail before being alerted to the problem. 118 | Additionally, this implements the "Circuit Breaker" pattern, it makes all the `CertificateRequest` wait until the `Issuer` 119 | is healthy again. 120 | 121 | The health check is implemented in the `Check` function in the `./internal/controllers/signer.go` file. 122 | 123 | TODO: issuer-lib does not yet support performing the health checks periodically. 124 | There should be some return value for the `Check` function so we can make controller-runtime retry reconciling regularly, even when the current reconcile succeeds. 125 | 126 | See [the issuer-lib README](https://github.com/cert-manager/issuer-lib?tab=readme-ov-file#how-it-works) for more information. 127 | 128 | ### Sign the cert-manager CertificateRequest resources and kubernetes CertificateSigningRequest resources 129 | 130 | The `Sign` function in the `./internal/controllers/signer.go` file is used by the CertificateRequest and CertificateSigningRequest reconcilers 131 | to create signed x509 certificates for the provided x509 certificate signing requests. 132 | 133 | If `Sign` succeeds it returns the bytes of a signed certificate which we then use as the value for `CertificateRequest.Status.Certificate`. 134 | If it returns a normal error, the Sign function will be retried as long as we have not spent more than the configured MaxRetryDuration after the certificate request was created. 135 | 136 | See [the issuer-lib README](https://github.com/cert-manager/issuer-lib?tab=readme-ov-file#how-it-works) for more information. 137 | 138 | #### Get the Issuer or ClusterIssuer credentials from a Secret 139 | 140 | The API for your CA may require some configuration and credentials and the obvious place to store these is in a Kubernetes `Secret`. 141 | We extend the `IssuerSpec` to include a `URL` field and a `AuthSecretName`, which is the name of a `Secret`. 142 | As usual run `make generate manifests` after modifying the API source files: 143 | 144 | ```console 145 | make generate manifests 146 | ``` 147 | 148 | NOTE: The namespace of that Secret is deliberately not specified here, 149 | because that would breach a security boundary and potentially allow someone who has permission to create `Issuer` resources, 150 | to make the controller access secrets in another namespace which that person would not normally have access to. 151 | 152 | For this reason, the Secret for an Issuer MUST be in the same namespace as the Issuer. 153 | The Secret for a ClusterIssuer MUST be in a namespace defined by cluster administrator, 154 | but that is a little more complicated and for now we will concentrate on Issuer Secrets. 155 | 156 | Both the `IssuerReconciler` and the `CertificateRequestReconciler` are updated to `GET` the `Secret` referred to by the `Issuer`. 157 | 158 | Add a new [Kubebuilder RBAC Marker](https://book.kubebuilder.io/reference/markers/rbac.html) to both controllers, 159 | permitting them read-only access to `Secret` resources. 160 | 161 | ```go 162 | // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch 163 | ``` 164 | 165 | Then run `make manifests` to regenerate the RBAC configuration in `config/`. 166 | 167 | Add the `corev1` types to the `Scheme` in the unit-tests. 168 | 169 | NOTE: It has already been added to the `main.go` `Scheme` as part of the `clientgoscheme`. 170 | 171 | Write a test to check that if the `GET` `Secret` operation fails, 172 | the error is returned and triggers a retry-with-backoff. 173 | This important because the `Secret` may not exist at the time the `Issuer` or `CertificateRequest` is created. 174 | 175 | NOTE: Ideally, we would `WATCH` for the particular `Secret` and trigger the reconciliation when it becomes available. 176 | And that may be a future enhancement to this project. 177 | 178 | In the case of the `CertificateRequestReconciler` we need to deal with both `Issuer` and `ClusterIssuer` types, 179 | so we modify the `issuerutil` function to allow us to extract an `IssuerSpec` from either of those types. 180 | 181 | ### Logging and Events 182 | 183 | We want to make it easy to debug problems with the issuer, 184 | so in addition to setting Conditions on the Issuer, ClusterIssuer and CertificateRequest, 185 | we can provide more feedback to the user by logging Kubernetes Events. 186 | You may want to read more about [Application Introspection and Debugging][] before continuing. 187 | 188 | [Application Introspection and Debugging]: https://kubernetes.io/docs/tasks/debug-application-cluster/debug-application-introspection/ 189 | 190 | Kubernetes Events are saved to the API server on a best-effort basis, 191 | they are (usually) associated with some other Kubernetes resource, 192 | and they are temporary; old Events are periodically purged from the API server. 193 | This allows tools such as `kubectl describe ` to show not only the resource details, 194 | but also a table of the recent events associated with that resource. 195 | 196 | The aim is to produce helpful debug output that looks like this: 197 | 198 | ``` 199 | $ kubectl describe clusterissuers.sample-issuer.example.com clusterissuer-sample 200 | ... 201 | Type: Ready 202 | Events: 203 | Type Reason Age From Message 204 | ---- ------ ---- ---- ------- 205 | Normal IssuerReconciler 13s sample-external-issuer First seen 206 | Warning IssuerReconciler 13s (x3 over 13s) sample-external-issuer Temporary error. Retrying: failed to get Secret containing Issuer credentials, secret name: sample-external-issuer-system/clusterissuer-sample-credentials, reason: Secret "clusterissuer-sample-credentials" not found 207 | Normal IssuerReconciler 13s (x3 over 13s) sample-external-issuer Success 208 | ``` 209 | And this: 210 | 211 | ``` 212 | $ kubectl describe certificaterequests.cert-manager.io issuer-sample 213 | ... 214 | Events: 215 | Type Reason Age From Message 216 | ---- ------ ---- ---- ------- 217 | Normal CertificateRequestReconciler 23m sample-external-issuer Initialising Ready condition 218 | Warning CertificateRequestReconciler 23m sample-external-issuer Temporary error. Retrying: error getting issuer: Issuer.sample-issuer.example.com "issuer-sample" not found 219 | Normal CertificateRequestReconciler 23m sample-external-issuer Signed 220 | 221 | ``` 222 | 223 | First add [record.EventRecorder][] attributes to the `IssuerReconciler` and to the `CertificateRequestReconciler`. 224 | And then in the Reconciler code, you can then generate an event by executing `r.recorder.Eventf(...)` whenever a significant change is made to the resource. 225 | 226 | [record.EventRecorder]: https://pkg.go.dev/k8s.io/client-go/tools/record#EventRecorder 227 | 228 | You can also write unit tests to verify the Reconciler events by using a [record.FakeRecorder][]. 229 | 230 | [record.FakeRecorder]: https://pkg.go.dev/k8s.io/client-go/tools/record#FakeRecorder 231 | 232 | See [PR 10: Generate Kubernetes Events](https://github.com/cert-manager/sample-external-issuer/pull/10) for an example of how you might generate events in your issuer. 233 | 234 | ### End-to-end tests 235 | 236 | Now our issuer is almost feature complete and it should be possible to write an end-to-end test that 237 | deploys a cert-manager `Certificate` 238 | referring to an external `Issuer` and check that a signed `Certificate` is saved to the expected secret. 239 | 240 | We can make such a test easier by tidying up the `Makefile` and adding some new targets 241 | which will help create a test cluster and to help install cert-manager. 242 | 243 | We can write a simple end-to-end test which deploys a `Certificate` manifest and waits for it to be ready. 244 | 245 | ```console 246 | kubectl apply --filename config/samples 247 | kubectl wait --for=condition=Ready --timeout=5s sampleissuers.sample-issuer.example.com sampleissuer-sample 248 | kubectl wait --for=condition=Ready --timeout=5s certificates.cert-manager.io certificate-by-sampleissuer 249 | ``` 250 | 251 | You can of course write more complete tests than this, 252 | but this is a good start and demonstrates that the issuer is doing what we hoped it would do. 253 | 254 | Run the tests as follows: 255 | 256 | ```bash 257 | # Create a Kind cluster along with cert-manager. 258 | make kind-cluster deploy-cert-manager 259 | 260 | # Wait for cert-manager to start... 261 | 262 | # Build and install sample-external-issuer and run the E2E tests. 263 | # This step can be run iteratively when ever you make changes to the code or to the installation manifests. 264 | make docker-build kind-load deploy e2e 265 | ``` 266 | 267 | #### Continuous Integration 268 | 269 | You should configure a CI system to automatically run the unit-tests when the code changes. 270 | See the `.github/workflows/` directory for some examples of using GitHub Actions 271 | which are triggered by changes to pull request branches and by any changes to the master branch. 272 | 273 | The E2E tests can be executed with GitHub Actions too. 274 | The GitHub Actions Ubuntu runner has Docker installed and is capable of running a Kind cluster for the E2E tests. 275 | The Kind cluster logs can be saved in the event of an E2E test failure, 276 | and uploaded as a GitHub Actions artifact, 277 | to make it easier to diagnose E2E test failures. 278 | 279 | ## Security considerations 280 | 281 | We use a [Distroless Docker Image][] as our Docker base image, 282 | and we configure our `manager` process to run as `USER: nonroot:nonroot`. 283 | This limits the privileges of the `manager` process in the cluster. 284 | 285 | Additionally we [Configure a Security Context][] for the manager Pod. 286 | We set `runAsNonRoot`, which ensure that the Kubelet will validate the image at runtime 287 | to ensure that it does not run as UID 0 (root) and fail to start the container if it does. 288 | 289 | ## Notes for cert-manager Maintainers 290 | 291 | ### Release Process 292 | 293 | Visit the [GitHub New Release Page][] and fill in the form. 294 | Here are some example values: 295 | 296 | * Tag Version: `v0.1.0-alpha.0`, `v0.1.0` for example. 297 | * Target: `main` 298 | * Release Title: `Release v0.1.0-alpha.2` 299 | * Description: (optional) a short summary of the changes since the last release. 300 | 301 | Click the `Publish release` button to trigger the automated release process: 302 | 303 | * A Docker image will be generated and published to `ghcr.io/cert-manager/sample-external-issuer/controller` with the chosen tag. 304 | * An `install.yaml` file will be generated and attached to the release. 305 | 306 | ## Links 307 | 308 | * [External Issuer] 309 | * [issuer-lib] 310 | * [cert-manager Concepts Documentation] 311 | * [Kubebuilder Book] 312 | * [Kubebuilder Markers] 313 | * [Distroless Docker Image] 314 | * [Configure a Security Context] 315 | * [GitHub New Release Page] 316 | 317 | [External Issuer]: https://cert-manager.io/docs/contributing/external-issuers 318 | [issuer-lib]: https://github.com/cert-manager/issuer-lib 319 | [cert-manager Concepts Documentation]: https://cert-manager.io/docs/concepts 320 | [Kubebuilder Book]: https://book.kubebuilder.io 321 | [Kubebuilder Markers]: https://book.kubebuilder.io/reference/markers.html 322 | [Distroless Docker Image]: https://github.com/GoogleContainerTools/distroless 323 | [Configure a Security Context]: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 324 | [GitHub New Release Page]: https://github.com/cert-manager/sample-external-issuer/releases/new 325 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= 2 | cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 3 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 4 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 5 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 6 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 7 | github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= 8 | github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 9 | github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= 10 | github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= 11 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 12 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 13 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 14 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 15 | github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 16 | github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 17 | github.com/cert-manager/cert-manager v1.19.2 h1:jSprN1h5pgNDSl7HClAmIzXuTxic/5FXJ32kbQHqjlM= 18 | github.com/cert-manager/cert-manager v1.19.2/go.mod h1:e9NzLtOKxTw7y99qLyWGmPo6mrC1Nh0EKKcMkRfK+GE= 19 | github.com/cert-manager/issuer-lib v0.9.0 h1:T+jDp1lY4ONipCCrE+DtEbcVXlQs8RicgBCgR36wJIE= 20 | github.com/cert-manager/issuer-lib v0.9.0/go.mod h1:NJ+n2b2qEe2ilMawtn7xc0RNMTpcdKDxHXIEewS74Fk= 21 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 22 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= 29 | github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 30 | github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= 31 | github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 32 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 33 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 34 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 35 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 36 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 37 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 38 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 39 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 40 | github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= 41 | github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= 42 | github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= 43 | github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= 44 | github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= 45 | github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 46 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= 47 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 48 | github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= 49 | github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= 50 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 51 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 52 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 53 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 54 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 55 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 56 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 57 | github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= 58 | github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= 59 | github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= 60 | github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= 61 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 62 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 63 | github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= 64 | github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= 65 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 66 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 67 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 68 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 69 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 70 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 71 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 72 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 73 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 74 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 75 | github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= 76 | github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= 77 | github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 78 | github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 79 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 80 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 81 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 82 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 83 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 84 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 85 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 86 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 87 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 88 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= 89 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= 90 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 91 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 92 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 93 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 94 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 95 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 96 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 97 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 98 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 99 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 100 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 101 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 102 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 103 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 104 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 105 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 106 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 107 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 108 | github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= 109 | github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 110 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 111 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 112 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 113 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 114 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 115 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 116 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 117 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 118 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 119 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 120 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 121 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 122 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 123 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 124 | github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= 125 | github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= 126 | github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= 127 | github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= 128 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 129 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 130 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 131 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 132 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 133 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 134 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 135 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 136 | github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8= 137 | github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= 138 | github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= 139 | github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= 140 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 141 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 142 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 143 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 144 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 145 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 146 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 147 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 148 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 149 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 150 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 151 | github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= 152 | github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 153 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 154 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 155 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 156 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 157 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 158 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 159 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 160 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 161 | github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= 162 | github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 163 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 164 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 165 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 166 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 167 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 168 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 169 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 170 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 171 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 172 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 173 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 174 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 175 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 176 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 177 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 178 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 179 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 180 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 181 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 182 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 183 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 184 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 185 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 186 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 187 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 188 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= 189 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= 190 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 191 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 192 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= 193 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= 194 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= 195 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= 196 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 197 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 198 | go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 199 | go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 200 | go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 201 | go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 202 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 203 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 204 | go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= 205 | go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= 206 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 207 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 208 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 209 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 210 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 211 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 212 | go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 213 | go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 214 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 215 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 216 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 217 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 218 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 219 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 220 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 221 | golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= 222 | golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= 223 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 224 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 225 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 226 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 227 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 228 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 229 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 230 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 231 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 232 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 233 | golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= 234 | golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 235 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 236 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 237 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 238 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 239 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 240 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 241 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 242 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 243 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 244 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 245 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 246 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 247 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 248 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 249 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 250 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 251 | golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= 252 | golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 253 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 254 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 255 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 256 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 257 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 258 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 259 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 260 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 261 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 263 | gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= 264 | gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 265 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 266 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 267 | google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 h1:mVXdvnmR3S3BQOqHECm9NGMjYiRtEvDYcqAqedTXY6s= 268 | google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I= 269 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= 270 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= 271 | google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= 272 | google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= 273 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 274 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 275 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 276 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 277 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 278 | gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 279 | gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 280 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 281 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 282 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 283 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 284 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 285 | k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= 286 | k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= 287 | k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= 288 | k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= 289 | k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= 290 | k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= 291 | k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= 292 | k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= 293 | k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= 294 | k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= 295 | k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= 296 | k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= 297 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 298 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 299 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= 300 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 301 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= 302 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 303 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0= 304 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= 305 | sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= 306 | sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= 307 | sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ= 308 | sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= 309 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 310 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 311 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 312 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 313 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 314 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 315 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 316 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 317 | --------------------------------------------------------------------------------