diff --git a/Dockerfile b/Dockerfile index 9b49cee..c920dd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ RUN apk add --no-cache ca-certificates COPY --from=build /workspace/webhook /usr/local/bin/webhook -ENTRYPOINT ["webhook"] +ENTRYPOINT ["webhook"] \ No newline at end of file diff --git a/Makefile b/Makefile index 5369d0d..9cacafe 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ OS ?= $(shell go env GOOS) ARCH ?= $(shell go env GOARCH) -IMAGE_NAME := "webhook" -IMAGE_TAG := "latest" +IMAGE_NAME := vstadtmueller/cert-manager-webhook-powerdns +IMAGE_TAG := latest OUT := $(shell pwd)/_out @@ -32,7 +32,6 @@ build: .PHONY: rendered-manifest.yaml rendered-manifest.yaml: helm template \ - --name example-webhook \ --set image.repository=$(IMAGE_NAME) \ --set image.tag=$(IMAGE_TAG) \ - deploy/example-webhook > "$(OUT)/rendered-manifest.yaml" + deploy/cert-manager-webhook-powerndns > "$(OUT)/rendered-manifest.yaml" diff --git a/README.md b/README.md index 1fc5031..81b0e81 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,109 @@ -# ACME webhook example +# ACME webhook for PowerDNS API -The ACME issuer type supports an optional 'webhook' solver, which can be used -to implement custom DNS01 challenge solving logic. +This solver can be used when you want to use cert-manager with PowerDNS HTTP API. API documentation is [here](https://doc.powerdns.com/authoritative/http-api/#) -This is useful if you need to use cert-manager with a DNS provider that is not -officially supported in cert-manager core. +## Requirements +- [go](https://golang.org/) >= 1.13.0 +- [helm](https://helm.sh/) >= v3.0.0 +- [kubernetes](https://kubernetes.io/) >= v1.14.0 +- [cert-manager](https://cert-manager.io/) >= 0.12.0 -## Why not in core? +## Installation -As the project & adoption has grown, there has been an influx of DNS provider -pull requests to our core codebase. As this number has grown, the test matrix -has become un-maintainable and so, it's not possible for us to certify that -providers work to a sufficient level. +### cert-manager -By creating this 'interface' between cert-manager and DNS providers, we allow -users to quickly iterate and test out new integrations, and then packaging -those up themselves as 'extensions' to cert-manager. +Follow the [instructions](https://cert-manager.io/docs/installation/) using the cert-manager documentation to install it within your cluster. -We can also then provide a standardised 'testing framework', or set of -conformance tests, which allow us to validate the a DNS provider works as -expected. +### Webhook -## Creating your own webhook +#### Using public helm chart +```bash +helm repo add cert-manager-webhook-powerdns https://lordofsystem.github.io/cert-manager-webhook-powerdns +# Replace the groupName value with your desired domain +helm install --namespace cert-manager cert-manager-webhook-powerdns cert-manager-webhook-powerdns/cert-manager-webhook-powerdns --set groupName=acme.yourdomain.tld +``` -Webhook's themselves are deployed as Kubernetes API services, in order to allow -administrators to restrict access to webhooks with Kubernetes RBAC. +#### From local checkout -This is important, as otherwise it'd be possible for anyone with access to your -webhook to complete ACME challenge validations and obtain certificates. +```bash +helm install --namespace cert-manager cert-manager-webhook-powerdns deploy/cert-manager-webhook-powerdns +``` +**Note**: The kubernetes resources used to install the Webhook should be deployed within the same namespace as the cert-manager. -To make the set up of these webhook's easier, we provide a template repository -that can be used to get started quickly. +To uninstall the webhook run +```bash +helm uninstall --namespace cert-manager cert-manager-webhook-powerdns +``` -### Creating your own repository +## Issuer + +Create a `ClusterIssuer` or `Issuer` resource as following: +```yaml +apiVersion: cert-manager.io/v1alpha2 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + # The ACME server URL + server: https://acme-staging-v02.api.letsencrypt.org/directory + + # Email address used for ACME registration + email: mail@example.com # REPLACE THIS WITH YOUR EMAIL!!! + + # Name of a secret used to store the ACME account private key + privateKeySecretRef: + name: letsencrypt-staging + + solvers: + - dns01: + webhook: + # This group needs to be configured when installing the helm package, otherwise the webhook won't have permission to create an ACME challenge for this API group. + groupName: acme.yourdomain.tld + solverName: pdns + config: + secretName: powerdns-secret + zoneName: example.com. + apiUrl: https://powerndns.com +``` + +### Credentials +In order to access the HTTP API, the webhook needs an API token. + +If you choose another name for the secret than `powerdns-secret`, ensure you modify the value of `secretName` in the `[Cluster]Issuer`. + +The secret for the example above will look like this: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: pdns-secret +type: Opaque +data: + api-key: your-key-base64-encoded +``` + +### Create a certificate + +Finally you can create certificates, for example: + +```yaml +apiVersion: cert-manager.io/v1alpha2 +kind: Certificate +metadata: + name: example-cert + namespace: cert-manager +spec: + commonName: example.com + dnsNames: + - example.com + issuerRef: + name: letsencrypt-staging + kind: ClusterIssuer + secretName: example-cert +``` + +## Development ### Running the test suite @@ -42,13 +113,14 @@ else they will have undetermined behaviour when used with cert-manager. **It is essential that you configure and run the test suite when creating a DNS01 webhook.** -An example Go test file has been provided in [main_test.go](https://github.com/jetstack/cert-manager-webhook-example/blob/master/main_test.go). +You need to replace `zoneName` parameter at `testdata/pdns/config.json` file with actual one. +You also must encode your api token into base64 and put the hash into `testdata/pdns/pdns-secret.yml` file. -You can run the test suite with: +You can then run the test suite with: ```bash -$ TEST_ZONE_NAME=example.com. make test +# first install necessary binaries (only required once) +./scripts/fetch-test-binaries.sh +# then run the tests +TEST_ZONE_NAME=example.com. make verify ``` - -The example file has a number of areas you must fill in and replace with your -own options in order for tests to pass. diff --git a/deploy/example-webhook/.helmignore b/deploy/cert-manager-webhook-powerndns/.helmignore similarity index 100% rename from deploy/example-webhook/.helmignore rename to deploy/cert-manager-webhook-powerndns/.helmignore diff --git a/deploy/example-webhook/Chart.yaml b/deploy/cert-manager-webhook-powerndns/Chart.yaml similarity index 71% rename from deploy/example-webhook/Chart.yaml rename to deploy/cert-manager-webhook-powerndns/Chart.yaml index 77c6ead..41ddd88 100644 --- a/deploy/example-webhook/Chart.yaml +++ b/deploy/cert-manager-webhook-powerndns/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes -name: example-webhook +name: cert-manager-webhook-powerdns version: 0.1.0 diff --git a/deploy/example-webhook/templates/NOTES.txt b/deploy/cert-manager-webhook-powerndns/templates/NOTES.txt similarity index 100% rename from deploy/example-webhook/templates/NOTES.txt rename to deploy/cert-manager-webhook-powerndns/templates/NOTES.txt diff --git a/deploy/example-webhook/templates/_helpers.tpl b/deploy/cert-manager-webhook-powerndns/templates/_helpers.tpl similarity index 58% rename from deploy/example-webhook/templates/_helpers.tpl rename to deploy/cert-manager-webhook-powerndns/templates/_helpers.tpl index d3c474b..eb58ac4 100644 --- a/deploy/example-webhook/templates/_helpers.tpl +++ b/deploy/cert-manager-webhook-powerndns/templates/_helpers.tpl @@ -2,7 +2,7 @@ {{/* Expand the name of the chart. */}} -{{- define "example-webhook.name" -}} +{{- define "cert-manager-webhook-powerdns.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} @@ -11,7 +11,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "example-webhook.fullname" -}} +{{- define "cert-manager-webhook-powerdns.fullname" -}} {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} {{- else -}} @@ -27,22 +27,22 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "example-webhook.chart" -}} +{{- define "cert-manager-webhook-powerdns.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} -{{- define "example-webhook.selfSignedIssuer" -}} -{{ printf "%s-selfsign" (include "example-webhook.fullname" .) }} +{{- define "cert-manager-webhook-powerdns.selfSignedIssuer" -}} +{{ printf "%s-selfsign" (include "cert-manager-webhook-powerdns.fullname" .) }} {{- end -}} -{{- define "example-webhook.rootCAIssuer" -}} -{{ printf "%s-ca" (include "example-webhook.fullname" .) }} +{{- define "cert-manager-webhook-powerdns.rootCAIssuer" -}} +{{ printf "%s-ca" (include "cert-manager-webhook-powerdns.fullname" .) }} {{- end -}} -{{- define "example-webhook.rootCACertificate" -}} -{{ printf "%s-ca" (include "example-webhook.fullname" .) }} +{{- define "cert-manager-webhook-powerdns.rootCACertificate" -}} +{{ printf "%s-ca" (include "cert-manager-webhook-powerdns.fullname" .) }} {{- end -}} -{{- define "example-webhook.servingCertificate" -}} -{{ printf "%s-webhook-tls" (include "example-webhook.fullname" .) }} +{{- define "cert-manager-webhook-powerdns.servingCertificate" -}} +{{ printf "%s-webhook-tls" (include "cert-manager-webhook-powerdns.fullname" .) }} {{- end -}} diff --git a/deploy/example-webhook/templates/apiservice.yaml b/deploy/cert-manager-webhook-powerndns/templates/apiservice.yaml similarity index 62% rename from deploy/example-webhook/templates/apiservice.yaml rename to deploy/cert-manager-webhook-powerndns/templates/apiservice.yaml index 4f6d5ce..5eadb31 100644 --- a/deploy/example-webhook/templates/apiservice.yaml +++ b/deploy/cert-manager-webhook-powerndns/templates/apiservice.yaml @@ -3,17 +3,17 @@ kind: APIService metadata: name: v1alpha1.{{ .Values.groupName }} labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} annotations: - cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/{{ include "example-webhook.servingCertificate" . }}" + cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/{{ include "cert-manager-webhook-powerdns.servingCertificate" . }}" spec: group: {{ .Values.groupName }} groupPriorityMinimum: 1000 versionPriority: 15 service: - name: {{ include "example-webhook.fullname" . }} + name: {{ include "cert-manager-webhook-powerdns.fullname" . }} namespace: {{ .Release.Namespace }} version: v1alpha1 diff --git a/deploy/example-webhook/templates/deployment.yaml b/deploy/cert-manager-webhook-powerndns/templates/deployment.yaml similarity index 76% rename from deploy/example-webhook/templates/deployment.yaml rename to deploy/cert-manager-webhook-powerndns/templates/deployment.yaml index ed49463..f3ff54a 100644 --- a/deploy/example-webhook/templates/deployment.yaml +++ b/deploy/cert-manager-webhook-powerndns/templates/deployment.yaml @@ -1,25 +1,25 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "example-webhook.fullname" . }} + name: {{ include "cert-manager-webhook-powerdns.fullname" . }} labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: - app: {{ include "example-webhook.name" . }} + app: {{ include "cert-manager-webhook-powerdns.name" . }} release: {{ .Release.Name }} template: metadata: labels: - app: {{ include "example-webhook.name" . }} + app: {{ include "cert-manager-webhook-powerdns.name" . }} release: {{ .Release.Name }} spec: - serviceAccountName: {{ include "example-webhook.fullname" . }} + serviceAccountName: {{ include "cert-manager-webhook-powerdns.fullname" . }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -53,7 +53,7 @@ spec: volumes: - name: certs secret: - secretName: {{ include "example-webhook.servingCertificate" . }} + secretName: {{ include "cert-manager-webhook-powerdns.servingCertificate" . }} {{- with .Values.nodeSelector }} nodeSelector: {{ toYaml . | indent 8 }} diff --git a/deploy/cert-manager-webhook-powerndns/templates/pki.yaml b/deploy/cert-manager-webhook-powerndns/templates/pki.yaml new file mode 100644 index 0000000..f43c882 --- /dev/null +++ b/deploy/cert-manager-webhook-powerndns/templates/pki.yaml @@ -0,0 +1,76 @@ +--- +# Create a selfsigned Issuer, in order to create a root CA certificate for +# signing webhook serving certificates +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "cert-manager-webhook-powerdns.selfSignedIssuer" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + selfSigned: {} + +--- + +# Generate a CA Certificate used to sign certificates for the webhook +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "cert-manager-webhook-powerdns.rootCACertificate" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + secretName: {{ include "cert-manager-webhook-powerdns.rootCACertificate" . }} + duration: 43800h # 5y + issuerRef: + name: {{ include "cert-manager-webhook-powerdns.selfSignedIssuer" . }} + commonName: "ca.cert-manager-webhook-powerdns.cert-manager" + isCA: true + +--- + +# Create an Issuer that uses the above generated CA certificate to issue certs +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "cert-manager-webhook-powerdns.rootCAIssuer" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + ca: + secretName: {{ include "cert-manager-webhook-powerdns.rootCACertificate" . }} + +--- + +# Finally, generate a serving certificate for the webhook to use +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "cert-manager-webhook-powerdns.servingCertificate" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + secretName: {{ include "cert-manager-webhook-powerdns.servingCertificate" . }} + duration: 8760h # 1y + issuerRef: + name: {{ include "cert-manager-webhook-powerdns.rootCAIssuer" . }} + dnsNames: + - {{ include "cert-manager-webhook-powerdns.fullname" . }} + - {{ include "cert-manager-webhook-powerdns.fullname" . }}.{{ .Release.Namespace }} + - {{ include "cert-manager-webhook-powerdns.fullname" . }}.{{ .Release.Namespace }}.svc diff --git a/deploy/cert-manager-webhook-powerndns/templates/rbac.yaml b/deploy/cert-manager-webhook-powerndns/templates/rbac.yaml new file mode 100644 index 0000000..2aa8c55 --- /dev/null +++ b/deploy/cert-manager-webhook-powerndns/templates/rbac.yaml @@ -0,0 +1,123 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "cert-manager-webhook-powerdns.fullname" . }} + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +--- +# Grant the webhook permission to read the ConfigMap containing the Kubernetes +# apiserver's requestheader-ca-certificate. +# This ConfigMap is automatically created by the Kubernetes apiserver. +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:webhook-authentication-reader + namespace: kube-system + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: extension-apiserver-authentication-reader +subjects: + - apiGroup: "" + kind: ServiceAccount + name: {{ include "cert-manager-webhook-powerdns.fullname" . }} + namespace: {{ .Release.Namespace }} +--- +# apiserver gets the auth-delegator role to delegate auth decisions to +# the core apiserver +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:auth-delegator + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: + - apiGroup: "" + kind: ServiceAccount + name: {{ include "cert-manager-webhook-powerdns.fullname" . }} + namespace: {{ .Release.Namespace }} +--- +# Grant cert-manager permission to validate using our apiserver +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:domain-solver + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +rules: + - apiGroups: + - {{ .Values.groupName }} + resources: + - '*' + verbs: + - 'create' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:domain-solver + labels: + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:domain-solver +subjects: + - apiGroup: "" + kind: ServiceAccount + name: {{ .Values.certManager.serviceAccountName }} + namespace: {{ .Values.certManager.namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:secret-reader + namespace: {{ .Release.Namespace }} +rules: + - apiGroups: + - "" + resources: + - "secrets" + {{- with .Values.secretName }} + resourceNames: + {{ toYaml . | indent 4 }} + {{- end }} + verbs: + - "get" + - "watch" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:secret-reader + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "cert-manager-webhook-powerdns.fullname" . }}:secret-reader +subjects: + - apiGroup: "" + kind: ServiceAccount + name: {{ include "cert-manager-webhook-powerdns.fullname" . }} + namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/deploy/example-webhook/templates/service.yaml b/deploy/cert-manager-webhook-powerndns/templates/service.yaml similarity index 55% rename from deploy/example-webhook/templates/service.yaml rename to deploy/cert-manager-webhook-powerndns/templates/service.yaml index 572089e..dd2a058 100644 --- a/deploy/example-webhook/templates/service.yaml +++ b/deploy/cert-manager-webhook-powerndns/templates/service.yaml @@ -1,10 +1,10 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "example-webhook.fullname" . }} + name: {{ include "cert-manager-webhook-powerdns.fullname" . }} labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} + app: {{ include "cert-manager-webhook-powerdns.name" . }} + chart: {{ include "cert-manager-webhook-powerdns.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: @@ -15,5 +15,5 @@ spec: protocol: TCP name: https selector: - app: {{ include "example-webhook.name" . }} + app: {{ include "cert-manager-webhook-powerdns.name" . }} release: {{ .Release.Name }} diff --git a/deploy/example-webhook/values.yaml b/deploy/cert-manager-webhook-powerndns/values.yaml similarity index 89% rename from deploy/example-webhook/values.yaml rename to deploy/cert-manager-webhook-powerndns/values.yaml index 31eb151..636508f 100644 --- a/deploy/example-webhook/values.yaml +++ b/deploy/cert-manager-webhook-powerndns/values.yaml @@ -6,15 +6,15 @@ # solve the DNS01 challenge. # This group name should be **unique**, hence using your own company's domain # here is recommended. -groupName: acme.mycompany.com +groupName: acme.stadtmueller.at certManager: namespace: cert-manager serviceAccountName: cert-manager image: - repository: mycompany/webhook-image - tag: latest + repository: vstadtmueller/cert-manager-webhook-powerdns + tag: dev pullPolicy: IfNotPresent nameOverride: "" @@ -24,6 +24,9 @@ service: type: ClusterIP port: 443 +secretName: + - pdns-secret + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little diff --git a/deploy/example-webhook/templates/pki.yaml b/deploy/example-webhook/templates/pki.yaml deleted file mode 100644 index b4b4c23..0000000 --- a/deploy/example-webhook/templates/pki.yaml +++ /dev/null @@ -1,76 +0,0 @@ ---- -# Create a selfsigned Issuer, in order to create a root CA certificate for -# signing webhook serving certificates -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: {{ include "example-webhook.selfSignedIssuer" . }} - namespace: {{ .Release.Namespace | quote }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - selfSigned: {} - ---- - -# Generate a CA Certificate used to sign certificates for the webhook -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: {{ include "example-webhook.rootCACertificate" . }} - namespace: {{ .Release.Namespace | quote }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - secretName: {{ include "example-webhook.rootCACertificate" . }} - duration: 43800h # 5y - issuerRef: - name: {{ include "example-webhook.selfSignedIssuer" . }} - commonName: "ca.example-webhook.cert-manager" - isCA: true - ---- - -# Create an Issuer that uses the above generated CA certificate to issue certs -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: {{ include "example-webhook.rootCAIssuer" . }} - namespace: {{ .Release.Namespace | quote }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - ca: - secretName: {{ include "example-webhook.rootCACertificate" . }} - ---- - -# Finally, generate a serving certificate for the webhook to use -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: {{ include "example-webhook.servingCertificate" . }} - namespace: {{ .Release.Namespace | quote }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - secretName: {{ include "example-webhook.servingCertificate" . }} - duration: 8760h # 1y - issuerRef: - name: {{ include "example-webhook.rootCAIssuer" . }} - dnsNames: - - {{ include "example-webhook.fullname" . }} - - {{ include "example-webhook.fullname" . }}.{{ .Release.Namespace }} - - {{ include "example-webhook.fullname" . }}.{{ .Release.Namespace }}.svc diff --git a/deploy/example-webhook/templates/rbac.yaml b/deploy/example-webhook/templates/rbac.yaml deleted file mode 100644 index d386362..0000000 --- a/deploy/example-webhook/templates/rbac.yaml +++ /dev/null @@ -1,90 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "example-webhook.fullname" . }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} ---- -# Grant the webhook permission to read the ConfigMap containing the Kubernetes -# apiserver's requestheader-ca-certificate. -# This ConfigMap is automatically created by the Kubernetes apiserver. -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "example-webhook.fullname" . }}:webhook-authentication-reader - namespace: kube-system - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: extension-apiserver-authentication-reader -subjects: - - apiGroup: "" - kind: ServiceAccount - name: {{ include "example-webhook.fullname" . }} - namespace: {{ .Release.Namespace }} ---- -# apiserver gets the auth-delegator role to delegate auth decisions to -# the core apiserver -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "example-webhook.fullname" . }}:auth-delegator - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:auth-delegator -subjects: - - apiGroup: "" - kind: ServiceAccount - name: {{ include "example-webhook.fullname" . }} - namespace: {{ .Release.Namespace }} ---- -# Grant cert-manager permission to validate using our apiserver -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "example-webhook.fullname" . }}:domain-solver - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -rules: - - apiGroups: - - {{ .Values.groupName }} - resources: - - '*' - verbs: - - 'create' ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "example-webhook.fullname" . }}:domain-solver - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "example-webhook.fullname" . }}:domain-solver -subjects: - - apiGroup: "" - kind: ServiceAccount - name: {{ .Values.certManager.serviceAccountName }} - namespace: {{ .Values.certManager.namespace }} diff --git a/example/dns.go b/example/dns.go deleted file mode 100644 index e29597e..0000000 --- a/example/dns.go +++ /dev/null @@ -1,69 +0,0 @@ -package example - -import ( - "fmt" - - "github.com/miekg/dns" -) - -func (e *exampleSolver) handleDNSRequest(w dns.ResponseWriter, req *dns.Msg) { - msg := new(dns.Msg) - msg.SetReply(req) - switch req.Opcode { - case dns.OpcodeQuery: - for _, q := range msg.Question { - if err := e.addDNSAnswer(q, msg, req); err != nil { - msg.SetRcode(req, dns.RcodeServerFailure) - break - } - } - } - w.WriteMsg(msg) -} - -func (e *exampleSolver) addDNSAnswer(q dns.Question, msg *dns.Msg, req *dns.Msg) error { - switch q.Qtype { - // Always return loopback for any A query - case dns.TypeA: - rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN A 127.0.0.1", q.Name)) - if err != nil { - return err - } - msg.Answer = append(msg.Answer, rr) - return nil - - // TXT records are the only important record for ACME dns-01 challenges - case dns.TypeTXT: - e.RLock() - record, found := e.txtRecords[q.Name] - e.RUnlock() - if !found { - msg.SetRcode(req, dns.RcodeNameError) - return nil - } - rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN TXT %s", q.Name, record)) - if err != nil { - return err - } - msg.Answer = append(msg.Answer, rr) - return nil - - // NS and SOA are for authoritative lookups, return obviously invalid data - case dns.TypeNS: - rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN NS ns.example-acme-webook.invalid.", q.Name)) - if err != nil { - return err - } - msg.Answer = append(msg.Answer, rr) - return nil - case dns.TypeSOA: - rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN SOA %s 20 5 5 5 5", "ns.example-acme-webook.invalid.", "ns.example-acme-webook.invalid.")) - if err != nil { - return err - } - msg.Answer = append(msg.Answer, rr) - return nil - default: - return fmt.Errorf("unimplemented record type %v", q.Qtype) - } -} diff --git a/example/example.go b/example/example.go deleted file mode 100644 index d31b42d..0000000 --- a/example/example.go +++ /dev/null @@ -1,68 +0,0 @@ -// package example contains a self-contained example of a webhook that passes the cert-manager -// DNS conformance tests -package example - -import ( - "fmt" - "os" - "sync" - - "github.com/jetstack/cert-manager/pkg/acme/webhook" - acme "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" - "github.com/miekg/dns" - "k8s.io/client-go/rest" -) - -type exampleSolver struct { - name string - server *dns.Server - txtRecords map[string]string - sync.RWMutex -} - -func (e *exampleSolver) Name() string { - return e.name -} - -func (e *exampleSolver) Present(ch *acme.ChallengeRequest) error { - e.Lock() - e.txtRecords[ch.ResolvedFQDN] = ch.Key - e.Unlock() - return nil -} - -func (e *exampleSolver) CleanUp(ch *acme.ChallengeRequest) error { - e.Lock() - delete(e.txtRecords, ch.ResolvedFQDN) - e.Unlock() - return nil -} - -func (e *exampleSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { - go func(done <-chan struct{}) { - <-done - if err := e.server.Shutdown(); err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - } - }(stopCh) - go func() { - if err := e.server.ListenAndServe(); err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - os.Exit(1) - } - }() - return nil -} - -func New(port string) webhook.Solver { - e := &exampleSolver{ - name: "example", - txtRecords: make(map[string]string), - } - e.server = &dns.Server{ - Addr: ":" + port, - Net: "udp", - Handler: dns.HandlerFunc(e.handleDNSRequest), - } - return e -} diff --git a/example/example_test.go b/example/example_test.go deleted file mode 100644 index 8c40df2..0000000 --- a/example/example_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package example - -import ( - "crypto/rand" - "math/big" - "testing" - - acme "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" -) - -func TestExampleSolver_Name(t *testing.T) { - port, _ := rand.Int(rand.Reader, big.NewInt(50000)) - port = port.Add(port, big.NewInt(15534)) - solver := New(port.String()) - assert.Equal(t, "example", solver.Name()) -} - -func TestExampleSolver_Initialize(t *testing.T) { - port, _ := rand.Int(rand.Reader, big.NewInt(50000)) - port = port.Add(port, big.NewInt(15534)) - solver := New(port.String()) - done := make(chan struct{}) - err := solver.Initialize(nil, done) - assert.NoError(t, err, "Expected Initialize not to error") - close(done) -} - -func TestExampleSolver_Present_Cleanup(t *testing.T) { - port, _ := rand.Int(rand.Reader, big.NewInt(50000)) - port = port.Add(port, big.NewInt(15534)) - solver := New(port.String()) - done := make(chan struct{}) - err := solver.Initialize(nil, done) - assert.NoError(t, err, "Expected Initialize not to error") - - validTestData := []struct { - hostname string - record string - }{ - {"test1.example.com.", "testkey1"}, - {"test2.example.com.", "testkey2"}, - {"test3.example.com.", "testkey3"}, - } - for _, test := range validTestData { - err := solver.Present(&acme.ChallengeRequest{ - Action: acme.ChallengeActionPresent, - Type: "dns-01", - ResolvedFQDN: test.hostname, - Key: test.record, - }) - assert.NoError(t, err, "Unexpected error while presenting %v", t) - } - - // Resolve test data - for _, test := range validTestData { - msg := new(dns.Msg) - msg.Id = dns.Id() - msg.RecursionDesired = true - msg.Question = make([]dns.Question, 1) - msg.Question[0] = dns.Question{dns.Fqdn(test.hostname), dns.TypeTXT, dns.ClassINET} - in, err := dns.Exchange(msg, "127.0.0.1:"+port.String()) - - assert.NoError(t, err, "Presented record %s not resolvable", test.hostname) - assert.Len(t, in.Answer, 1, "RR response is of incorrect length") - assert.Equal(t, []string{test.record}, in.Answer[0].(*dns.TXT).Txt, "TXT record returned did not match presented record") - } - - // Cleanup test data - for _, test := range validTestData { - err := solver.CleanUp(&acme.ChallengeRequest{ - Action: acme.ChallengeActionCleanUp, - Type: "dns-01", - ResolvedFQDN: test.hostname, - Key: test.record, - }) - assert.NoError(t, err, "Unexpected error while cleaning up %v", t) - } - - // Resolve test data - for _, test := range validTestData { - msg := new(dns.Msg) - msg.Id = dns.Id() - msg.RecursionDesired = true - msg.Question = make([]dns.Question, 1) - msg.Question[0] = dns.Question{dns.Fqdn(test.hostname), dns.TypeTXT, dns.ClassINET} - in, err := dns.Exchange(msg, "127.0.0.1:"+port.String()) - - assert.NoError(t, err, "Presented record %s not resolvable", test.hostname) - assert.Len(t, in.Answer, 0, "RR response is of incorrect length") - assert.Equal(t, dns.RcodeNameError, in.Rcode, "Expexted NXDOMAIN") - } - - close(done) -} diff --git a/go.mod b/go.mod index 42c1fbe..1222da2 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,14 @@ -module github.com/cert-manager/webhook-example +module github.com/lordofsystem/cert-manager-webhook-powerdns go 1.13 require ( github.com/jetstack/cert-manager v1.2.0 + github.com/joeig/go-powerdns/v2 v2.4.1 github.com/miekg/dns v1.1.31 github.com/stretchr/testify v1.6.1 k8s.io/apiextensions-apiserver v0.19.0 + k8s.io/apimachinery v0.19.0 k8s.io/client-go v0.19.0 + k8s.io/klog v1.0.0 ) diff --git a/go.sum b/go.sum index 24a784a..f485d00 100644 --- a/go.sum +++ b/go.sum @@ -348,10 +348,13 @@ github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jetstack/cert-manager v1.2.0 h1:xgXGdvHxGwCFjB13rCQ/fwa4A7FMpPRewa3wiW++EP4= github.com/jetstack/cert-manager v1.2.0/go.mod h1:maDZ7RUO9H6RB+/ks9XBe8jf9zdC8cI0dGY3HBLzTVQ= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joeig/go-powerdns/v2 v2.4.1 h1:bo360+v9N/cDz+fCFqH7axbmkHWd4amjRFWoe0/7ahA= +github.com/joeig/go-powerdns/v2 v2.4.1/go.mod h1:VgLq0WK8knYT+c6RcD5dB/L3LUvUXHNnGZp/nmSwJBk= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= diff --git a/internal/pdns.go b/internal/pdns.go index 37876f9..c0d673a 100644 --- a/internal/pdns.go +++ b/internal/pdns.go @@ -1,5 +1,5 @@ package internal type Config struct { - ApiKey, ZoneName, ApiUrl string + ApiKey, ZoneName, ServerName, ApiUrl string } \ No newline at end of file diff --git a/main.go b/main.go index 85aeac9..190edae 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,24 @@ package main import ( + "context" "encoding/json" "fmt" "os" extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - //"k8s.io/client-go/kubernetes" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/klog" "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/jetstack/cert-manager/pkg/acme/webhook/cmd" + + "github.com/lordofsystem/cert-manager-webhook-powerdns/internal" + + "github.com/joeig/go-powerdns/v2" ) var GroupName = os.Getenv("GROUP_NAME") @@ -26,25 +34,26 @@ func main() { // webhook, where the Name() method will be used to disambiguate between // the different implementations. cmd.RunWebhookServer(GroupName, - &customDNSProviderSolver{}, + &pdnsDNSProviderSolver{}, ) + } -// customDNSProviderSolver implements the provider-specific logic needed to +// pdnsDNSProviderSolver implements the provider-specific logic needed to // 'present' an ACME challenge TXT record for your own DNS provider. // To do so, it must implement the `github.com/jetstack/cert-manager/pkg/acme/webhook.Solver` // interface. -type customDNSProviderSolver struct { +type pdnsDNSProviderSolver struct { // If a Kubernetes 'clientset' is needed, you must: // 1. uncomment the additional `client` field in this structure below // 2. uncomment the "k8s.io/client-go/kubernetes" import at the top of the file // 3. uncomment the relevant code in the Initialize method below // 4. ensure your webhook's service account has the required RBAC role // assigned to it for interacting with the Kubernetes APIs you need. - //client kubernetes.Clientset + client *kubernetes.Clientset } -// customDNSProviderConfig is a structure that is used to decode into when +// pdnsDNSProviderConfig is a structure that is used to decode into when // solving a DNS01 challenge. // This information is provided by cert-manager, and may be a reference to // additional configuration that's needed to solve the challenge for this @@ -58,7 +67,7 @@ type customDNSProviderSolver struct { // You should not include sensitive information here. If credentials need to // be used by your provider here, you should reference a Kubernetes Secret // resource and fetch these credentials using a Kubernetes clientset. -type customDNSProviderConfig struct { +type pdnsDNSProviderConfig struct { // Change the two fields below according to the format of the configuration // to be decoded. // These fields will be set by users in the @@ -66,6 +75,10 @@ type customDNSProviderConfig struct { //Email string `json:"email"` //APIKeySecretRef v1alpha1.SecretKeySelector `json:"apiKeySecretRef"` + SecretRef string `json:"secretName"` + ZoneName string `json:"zoneName"` + ServerName string `json:"server"` + ApiUrl string `json:"apiUrl"` } // Name is used as the name for this DNS solver when referencing it on the ACME @@ -74,8 +87,8 @@ type customDNSProviderConfig struct { // solvers configured with the same Name() **so long as they do not co-exist // within a single webhook deployment**. // For example, `cloudflare` may be used as the name of a solver. -func (c *customDNSProviderSolver) Name() string { - return "my-custom-solver" +func (c *pdnsDNSProviderSolver) Name() string { + return "pdns" } // Present is responsible for actually presenting the DNS record with the @@ -83,16 +96,25 @@ func (c *customDNSProviderSolver) Name() string { // This method should tolerate being called multiple times with the same value. // cert-manager itself will later perform a self check to ensure that the // solver has correctly configured the DNS provider. -func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { - cfg, err := loadConfig(ch.Config) +func (c *pdnsDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { + klog.Infof("call function Present: namespace=%s, zone=%s, fqdn=%s", ch.ResourceNamespace, ch.ResolvedZone, ch.ResolvedFQDN) + + config, err := clientConfig(c, ch) + key := fmt.Sprintf("\"%s\"", ch.Key) + if err != nil { - return err + klog.Errorf("unable to get secret `%s`; %v", ch.ResourceNamespace, err) } - // TODO: do something more useful with the decoded configuration - fmt.Printf("Decoded configuration %v", cfg) + pdns := powerdns.NewClient(config.ApiUrl, config.ServerName, map[string]string{"X-API-Key": config.ApiKey}, nil) + p_err := pdns.Records.Add(config.ZoneName, ch.ResolvedFQDN, powerdns.RRTypeTXT, 120, []string{key}) + + if p_err != nil { + klog.Errorf("Pdns client error: %v", p_err) + } + + klog.Infof("Presented txt record %v", ch.ResolvedFQDN) - // TODO: add code that sets a record in the DNS provider's console return nil } @@ -102,8 +124,22 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { // value provided on the ChallengeRequest should be cleaned up. // This is in order to facilitate multiple DNS validations for the same domain // concurrently. -func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { +func (c *pdnsDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { // TODO: add code that deletes a record from the DNS provider's console + + config, err := clientConfig(c, ch) + + if err != nil { + klog.Errorf("unable to get secret `%s`; %v", ch.ResourceNamespace, err) + } + pdns := powerdns.NewClient(config.ApiUrl, config.ServerName, map[string]string{"X-API-Key": config.ApiKey}, nil) + p_err := pdns.Records.Delete(config.ZoneName, ch.ResolvedFQDN, powerdns.RRTypeTXT) + + if p_err != nil { + klog.Error(p_err) + } + + klog.Infof("Delete TXT record result: %s", ch.ResolvedFQDN) return nil } @@ -116,16 +152,17 @@ func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { // provider accounts. // The stopCh can be used to handle early termination of the webhook, in cases // where a SIGTERM or similar signal is sent to the webhook process. -func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { +func (c *pdnsDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { ///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO ///// YOUR CUSTOM DNS PROVIDER - //cl, err := kubernetes.NewForConfig(kubeClientConfig) - //if err != nil { - // return err - //} - // - //c.client = cl + cl, err := kubernetes.NewForConfig(kubeClientConfig) + klog.V(6).Infof("Input variable stopCh is %d length", len(stopCh)) + if err != nil { + return err + } + + c.client = cl ///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE return nil @@ -133,15 +170,52 @@ func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stop // loadConfig is a small helper function that decodes JSON configuration into // the typed config struct. -func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) { - cfg := customDNSProviderConfig{} +func loadConfig(cfgJSON *extapi.JSON) (pdnsDNSProviderConfig, error) { + cfg := pdnsDNSProviderConfig{} // handle the 'base case' where no configuration has been provided if cfgJSON == nil { return cfg, nil } if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil { + klog.Errorf("error decoding solver config: %v", err) return cfg, fmt.Errorf("error decoding solver config: %v", err) } return cfg, nil } + +func stringFromSecretData(secretData *map[string][]byte, key string) (string, error) { + data, ok := (*secretData)[key] + if !ok { + return "", fmt.Errorf("key %q not found in secret data", key) + } + return string(data), nil +} + +func clientConfig(c *pdnsDNSProviderSolver, ch *v1alpha1.ChallengeRequest) (internal.Config, error) { + var config internal.Config + + cfg, err := loadConfig(ch.Config) + if err != nil { + return config, err + } + config.ZoneName = cfg.ZoneName + config.ApiUrl = cfg.ApiUrl + config.ServerName = cfg.ServerName + + secretName := cfg.SecretRef + sec, err := c.client.CoreV1().Secrets(ch.ResourceNamespace).Get(context.Background(), secretName, metav1.GetOptions{}) + + if err != nil { + return config, fmt.Errorf("unable to get secret `%s/%s`; %v", secretName, ch.ResourceNamespace, err) + } + + apiKey, err := stringFromSecretData(&sec.Data, "api-key") + config.ApiKey = apiKey + + if err != nil { + return config, fmt.Errorf("unable to get api-key from secret `%s/%s`; %v", secretName, ch.ResourceNamespace, err) + } + + return config, nil +} diff --git a/main_test.go b/main_test.go index 0377170..3dc97ce 100644 --- a/main_test.go +++ b/main_test.go @@ -5,12 +5,11 @@ import ( "testing" "github.com/jetstack/cert-manager/test/acme/dns" - - "github.com/cert-manager/webhook-example/example" ) var ( zone = os.Getenv("TEST_ZONE_NAME") + fqdn string ) func TestRunsSuite(t *testing.T) { @@ -18,23 +17,25 @@ func TestRunsSuite(t *testing.T) { // snippet of valid configuration that should be included on the // ChallengeRequest passed as part of the test cases. // + fqdn = "_acme-challenge.test." + zone // Uncomment the below fixture when implementing your custom DNS provider - //fixture := dns.NewFixture(&customDNSProviderSolver{}, - // dns.SetResolvedZone(zone), - // dns.SetAllowAmbientCredentials(false), - // dns.SetManifestPath("testdata/my-custom-solver"), - // dns.SetBinariesPath("_test/kubebuilder/bin"), - //) - - solver := example.New("59351") - fixture := dns.NewFixture(solver, - dns.SetResolvedZone("example.com."), - dns.SetManifestPath("testdata/my-custom-solver"), + fixture := dns.NewFixture(&pdnsDNSProviderSolver{}, + dns.SetResolvedZone(zone), + dns.SetResolvedFQDN(fqdn), + dns.SetAllowAmbientCredentials(false), + dns.SetManifestPath("testdata/pdns"), dns.SetBinariesPath("_test/kubebuilder/bin"), - dns.SetDNSServer("127.0.0.1:59351"), - dns.SetUseAuthoritative(false), ) + // solver := example.New("59351") + // fixture := dns.NewFixture(&pdnsDNSProviderSolver{}, + // dns.SetResolvedZone("example.com."), + // dns.SetManifestPath("testdata/pdns"), + // dns.SetBinariesPath("_test/kubebuilder/bin"), + // dns.SetDNSServer("127.0.0.1:59351"), + // dns.SetUseAuthoritative(false), + // ) + fixture.RunConformance(t) -} +} \ No newline at end of file diff --git a/testdata/my-custom-solver/README.md b/testdata/my-custom-solver/README.md deleted file mode 100644 index feb4cbd..0000000 --- a/testdata/my-custom-solver/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Solver testdata directory - -TODO diff --git a/testdata/my-custom-solver/config.json b/testdata/my-custom-solver/config.json deleted file mode 100644 index 0967ef4..0000000 --- a/testdata/my-custom-solver/config.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/testdata/pdns/config.json b/testdata/pdns/config.json new file mode 100644 index 0000000..03da316 --- /dev/null +++ b/testdata/pdns/config.json @@ -0,0 +1,6 @@ +{ + "secretName": "pdns-secret", + "zoneName": "example.com.", + "server": "localhost", + "apiUrl" : "https://powerdns.api" + } \ No newline at end of file diff --git a/testdata/pdns/pdns-secret.yml b/testdata/pdns/pdns-secret.yml new file mode 100644 index 0000000..ffa17ec --- /dev/null +++ b/testdata/pdns/pdns-secret.yml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: pdns-secret +data: + api-key: VEhpc0FzM2NyM1RlQVNUM3JFZ0c= \ No newline at end of file