diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 000000000..d8f82cf4a --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,35 @@ +name: Build + +on: + push: + branches: + - 'master' + pull_request: + +env: + DOCKER_IMAGE_PATH: us-docker.pkg.dev/cubejs-cloud/cloud/cloud-dns-solver + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 60 + name: Builder + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Authenticate with Google Cloud + run: echo "${{ secrets.GCP_SERVICE_ACCOUNT_KEY_JSON }}" | base64 -d | gcloud auth activate-service-account --key-file=- + env: + CLOUDSDK_CORE_DISABLE_PROMPTS: 1 + - name: Configure Docker to use gcloud as a credential helper + run: gcloud auth configure-docker us-docker.pkg.dev + env: + CLOUDSDK_CORE_DISABLE_PROMPTS: 1 + - name: Build docker image + run: | + docker build -t $DOCKER_IMAGE_PATH:latest . + - name: Push docker Image + run: | + docker push $DOCKER_IMAGE_PATH:latest + diff --git a/.gitignore b/.gitignore index a4be81c16..27273d1af 100644 --- a/.gitignore +++ b/.gitignore @@ -11,9 +11,8 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -# Ignore the built binary -cert-manager-webhook-example - # Make artifacts _out _test + +cloud-dns-solver diff --git a/Dockerfile b/Dockerfile index c92f9fbd3..da2542536 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22-alpine3.19 AS build_deps +FROM golang:1.23-alpine AS build_deps RUN apk add --no-cache git @@ -13,12 +13,12 @@ FROM build_deps AS build COPY . . -RUN CGO_ENABLED=0 go build -o webhook -ldflags '-w -extldflags "-static"' . +RUN CGO_ENABLED=0 go build -o cloud-dns-solver -ldflags '-w -extldflags "-static"' . FROM alpine:3.18 RUN apk add --no-cache ca-certificates -COPY --from=build /workspace/webhook /usr/local/bin/webhook +COPY --from=build /workspace/cloud-dns-solver /usr/local/bin/cloud-dns-solver -ENTRYPOINT ["webhook"] +ENTRYPOINT ["cloud-dns-solver"] diff --git a/deploy/example-webhook/.helmignore b/deploy/example-webhook/.helmignore deleted file mode 100644 index f0c131944..000000000 --- a/deploy/example-webhook/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/deploy/example-webhook/Chart.yaml b/deploy/example-webhook/Chart.yaml deleted file mode 100644 index 77c6eadac..000000000 --- a/deploy/example-webhook/Chart.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -appVersion: "1.0" -description: A Helm chart for Kubernetes -name: example-webhook -version: 0.1.0 diff --git a/deploy/example-webhook/templates/NOTES.txt b/deploy/example-webhook/templates/NOTES.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/deploy/example-webhook/templates/_helpers.tpl b/deploy/example-webhook/templates/_helpers.tpl deleted file mode 100644 index d3c474b64..000000000 --- a/deploy/example-webhook/templates/_helpers.tpl +++ /dev/null @@ -1,48 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "example-webhook.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -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" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "example-webhook.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "example-webhook.selfSignedIssuer" -}} -{{ printf "%s-selfsign" (include "example-webhook.fullname" .) }} -{{- end -}} - -{{- define "example-webhook.rootCAIssuer" -}} -{{ printf "%s-ca" (include "example-webhook.fullname" .) }} -{{- end -}} - -{{- define "example-webhook.rootCACertificate" -}} -{{ printf "%s-ca" (include "example-webhook.fullname" .) }} -{{- end -}} - -{{- define "example-webhook.servingCertificate" -}} -{{ printf "%s-webhook-tls" (include "example-webhook.fullname" .) }} -{{- end -}} diff --git a/deploy/example-webhook/templates/apiservice.yaml b/deploy/example-webhook/templates/apiservice.yaml deleted file mode 100644 index 4f6d5ceb7..000000000 --- a/deploy/example-webhook/templates/apiservice.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apiregistration.k8s.io/v1 -kind: APIService -metadata: - name: v1alpha1.{{ .Values.groupName }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - annotations: - cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/{{ include "example-webhook.servingCertificate" . }}" -spec: - group: {{ .Values.groupName }} - groupPriorityMinimum: 1000 - versionPriority: 15 - service: - name: {{ include "example-webhook.fullname" . }} - namespace: {{ .Release.Namespace }} - version: v1alpha1 diff --git a/deploy/example-webhook/templates/deployment.yaml b/deploy/example-webhook/templates/deployment.yaml deleted file mode 100644 index 057cc42bc..000000000 --- a/deploy/example-webhook/templates/deployment.yaml +++ /dev/null @@ -1,69 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "example-webhook.fullname" . }} - namespace: {{ .Release.Namespace | quote }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - app: {{ include "example-webhook.name" . }} - release: {{ .Release.Name }} - template: - metadata: - labels: - app: {{ include "example-webhook.name" . }} - release: {{ .Release.Name }} - spec: - serviceAccountName: {{ include "example-webhook.fullname" . }} - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - args: - - --tls-cert-file=/tls/tls.crt - - --tls-private-key-file=/tls/tls.key - env: - - name: GROUP_NAME - value: {{ .Values.groupName | quote }} - ports: - - name: https - containerPort: 443 - protocol: TCP - livenessProbe: - httpGet: - scheme: HTTPS - path: /healthz - port: https - readinessProbe: - httpGet: - scheme: HTTPS - path: /healthz - port: https - volumeMounts: - - name: certs - mountPath: /tls - readOnly: true - resources: -{{ toYaml .Values.resources | indent 12 }} - volumes: - - name: certs - secret: - secretName: {{ include "example-webhook.servingCertificate" . }} - {{- with .Values.nodeSelector }} - nodeSelector: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: -{{ toYaml . | indent 8 }} - {{- end }} diff --git a/deploy/example-webhook/templates/pki.yaml b/deploy/example-webhook/templates/pki.yaml deleted file mode 100644 index b4b4c2374..000000000 --- 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 605fcf50d..000000000 --- a/deploy/example-webhook/templates/rbac.yaml +++ /dev/null @@ -1,91 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "example-webhook.fullname" . }} - namespace: {{ .Release.Namespace | quote }} - 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/deploy/example-webhook/templates/service.yaml b/deploy/example-webhook/templates/service.yaml deleted file mode 100644 index a76ddc7c3..000000000 --- a/deploy/example-webhook/templates/service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "example-webhook.fullname" . }} - namespace: {{ .Release.Namespace | quote }} - labels: - app: {{ include "example-webhook.name" . }} - chart: {{ include "example-webhook.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: https - protocol: TCP - name: https - selector: - app: {{ include "example-webhook.name" . }} - release: {{ .Release.Name }} diff --git a/deploy/example-webhook/values.yaml b/deploy/example-webhook/values.yaml deleted file mode 100644 index 31eb15140..000000000 --- a/deploy/example-webhook/values.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# The GroupName here is used to identify your company or business unit that -# created this webhook. -# For example, this may be "acme.mycompany.com". -# This name will need to be referenced in each Issuer's `webhook` stanza to -# inform cert-manager of where to send ChallengePayload resources in order to -# solve the DNS01 challenge. -# This group name should be **unique**, hence using your own company's domain -# here is recommended. -groupName: acme.mycompany.com - -certManager: - namespace: cert-manager - serviceAccountName: cert-manager - -image: - repository: mycompany/webhook-image - tag: latest - pullPolicy: IfNotPresent - -nameOverride: "" -fullnameOverride: "" - -service: - type: ClusterIP - port: 443 - -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 - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -nodeSelector: {} - -tolerations: [] - -affinity: {} diff --git a/example/dns.go b/example/dns.go deleted file mode 100644 index e29597e90..000000000 --- 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 8cfe59e77..000000000 --- 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/cert-manager/cert-manager/pkg/acme/webhook" - acme "github.com/cert-manager/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 ef4dde3c4..000000000 --- a/example/example_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package example - -import ( - "crypto/rand" - "math/big" - "testing" - - acme "github.com/cert-manager/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 83744cbf4..ada6c07a6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/cert-manager/webhook-example +module github.com/cubedevinc/cloud-dns-solver -go 1.22.0 +go 1.23 require ( github.com/cert-manager/cert-manager v1.15.1 diff --git a/main.go b/main.go index 969e6d209..d9b9efe4e 100644 --- a/main.go +++ b/main.go @@ -1,70 +1,32 @@ package main import ( + "bytes" "encoding/json" "fmt" + "io" + "net/http" "os" - extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/client-go/rest" "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd" ) -var GroupName = os.Getenv("GROUP_NAME") - func main() { - if GroupName == "" { - panic("GROUP_NAME must be specified") - } - - // This will register our custom DNS provider with the webhook serving - // library, making it available as an API under the provided GroupName. - // You can register multiple DNS provider implementations with a single - // webhook, where the Name() method will be used to disambiguate between - // the different implementations. - cmd.RunWebhookServer(GroupName, - &customDNSProviderSolver{}, + cmd.RunWebhookServer("cubecloud", + &cubeCloudDNSSolver{}, ) } -// customDNSProviderSolver implements the provider-specific logic needed to +// cubeCloudDNSSolver 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/cert-manager/cert-manager/pkg/acme/webhook.Solver` // interface. -type customDNSProviderSolver 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 -} - -// customDNSProviderConfig 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 -// particular certificate or issuer. -// This typically includes references to Secret resources containing DNS -// provider credentials, in cases where a 'multi-tenant' DNS solver is being -// created. -// If you do *not* require per-issuer or per-certificate configuration to be -// provided to your webhook, you can skip decoding altogether in favour of -// using CLI flags or similar to provide configuration. -// 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 { - // Change the two fields below according to the format of the configuration - // to be decoded. - // These fields will be set by users in the - // `issuer.spec.acme.dns01.providers.webhook.config` field. - - //Email string `json:"email"` - //APIKeySecretRef v1alpha1.SecretKeySelector `json:"apiKeySecretRef"` +type cubeCloudDNSSolver struct { + cloudRouterApiDomain string + cloudRouterApiToken string } // Name is used as the name for this DNS solver when referencing it on the ACME @@ -73,37 +35,66 @@ 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 *cubeCloudDNSSolver) Name() string { + return "cube-cloud-dns-01-solver" } -// Present is responsible for actually presenting the DNS record with the -// DNS provider. -// 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 *cubeCloudDNSSolver) MakeCloudRouterRequest(ch *v1alpha1.ChallengeRequest, action string) error { + apiEndpoint := fmt.Sprintf("https://%s/_cloud-router/dns-challenge/%s", c.cloudRouterApiDomain, action) + requestBody := map[string]string{ + "uid": string(ch.UID), + "action": action, + "key": ch.Key, + "resolvedFQDN": ch.ResolvedFQDN, + "dnsName": ch.DNSName, + } + + body, err := json.Marshal(requestBody) + if err != nil { + return fmt.Errorf("error marshaling request body: %v", err) + } + + req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(body)) if err != nil { - return err + return fmt.Errorf("error creating request: %v", err) } - // TODO: do something more useful with the decoded configuration - fmt.Printf("Decoded configuration %v", cfg) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.cloudRouterApiToken)) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error making HTTP request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to present DNS-01 challenge: received status code %v %s", resp.StatusCode, string(respBody)) + } + + fmt.Printf("TXT record presented for %v", ch.ResolvedFQDN) - // TODO: add code that sets a record in the DNS provider's console return nil } +// Present is responsible for actually presenting the DNS record with the +// DNS provider. +// 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 *cubeCloudDNSSolver) Present(ch *v1alpha1.ChallengeRequest) error { + return c.MakeCloudRouterRequest(ch, "present") +} + // CleanUp should delete the relevant TXT record from the DNS provider console. // If multiple TXT records exist with the same record name (e.g. // _acme-challenge.example.com) then **only** the record with the same `key` // 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 { - // TODO: add code that deletes a record from the DNS provider's console - return nil +func (c *cubeCloudDNSSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { + return c.MakeCloudRouterRequest(ch, "cleanup") } // Initialize will be called when the webhook first starts. @@ -115,32 +106,14 @@ 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 { - ///// 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 - - ///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE - return nil -} - -// loadConfig is a small helper function that decodes JSON configuration into -// the typed config struct. -func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) { - cfg := customDNSProviderConfig{} - // handle the 'base case' where no configuration has been provided - if cfgJSON == nil { - return cfg, nil +func (c *cubeCloudDNSSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { + c.cloudRouterApiDomain = os.Getenv("CLOUD_ROUTER_API_DOMAIN") + if c.cloudRouterApiDomain == "" { + return fmt.Errorf("CLOUD_ROUTER_API_DOMAIN must be set") } - if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil { - return cfg, fmt.Errorf("error decoding solver config: %v", err) + c.cloudRouterApiToken = os.Getenv("CLOUD_ROUTER_API_TOKEN") + if c.cloudRouterApiToken == "" { + return fmt.Errorf("CLOUD_ROUTER_API_TOKEN must be set") } - - return cfg, nil + return nil }