From 0ad80e96b4865f8911a4e34f16891334f56d165d Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 15 Aug 2025 19:14:17 -0400 Subject: [PATCH 1/3] feat: migrate Cloudflare store to API token auth and v5 SDK - Replace API key + email authentication with API token - Update to Cloudflare Go SDK v5.1.0 with modern CustomCertificates API - Update README documentation for API token usage - Add GitHub Actions workflow for Docker image building with multi-platform support and caching - Simplify authentication by requiring only api_token secret key BREAKING CHANGE: Cloudflare secrets now require 'api_token' instead of 'api_key' and 'email' --- .github/workflows/docker.yml | 63 ++++++++++++++++++++++++++++ README.md | 10 ++--- deploy/cert-manager-sync/values.yaml | 2 +- go.mod | 7 +++- go.sum | 16 +++++-- stores/cloudflare/cloudflare.go | 59 +++++++++++++------------- 6 files changed, 116 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..848b2a8 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,63 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + - master + tags: + - 'v*' + pull_request: + branches: + - main + - master + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index fde8c47..705232c 100644 --- a/README.md +++ b/README.md @@ -75,21 +75,21 @@ Annotations: ### Cloudflare -Create a Cloudflare API Key and create a kube secret containing this key. +Create a Cloudflare API Token with the necessary permissions for your zone and create a kube secret containing this token. ```bash kubectl -n cert-manager \ create secret generic example-cloudflare-secret \ - --from-literal api_key=XXXXX --from-literal email=XXXXX + --from-literal api_token=XXXXX ``` -You will then annotate your k8s TLS secret with this secret name to tell the operator to retrieve the Cloudflare API secret from this location. +You will then annotate your k8s TLS secret with this secret name to tell the operator to retrieve the Cloudflare API token from this location. Annotations: ```yaml cert-manager-sync.lestak.sh/cloudflare-enabled: "true" # sync certificate to Cloudflare - cert-manager-sync.lestak.sh/cloudflare-secret-name: "example-cloudflare-secret" # secret in same namespace which contains the cloudflare api key. If provided in format "namespace/secret-name", will look in that namespace for the secret + cert-manager-sync.lestak.sh/cloudflare-secret-name: "example-cloudflare-secret" # secret in same namespace which contains the cloudflare api token. If provided in format "namespace/secret-name", will look in that namespace for the secret cert-manager-sync.lestak.sh/cloudflare-zone-id: "example-zone-id" # cloudflare zone id cert-manager-sync.lestak.sh/cloudflare-cert-id: "" # will be auto-filled by operator for in-place renewals ``` @@ -321,7 +321,7 @@ metadata: cert-manager-sync.lestak.sh/acm-certificate-arn: "" # will be auto-filled by operator for in-place renewals cert-manager-sync.lestak.sh/acm-secret-name: "" # (optional if not using IRSA) secret in same namespace which contains the aws credentials. If provided in format "namespace/secret-name", will look in that namespace for the secret cert-manager-sync.lestak.sh/cloudflare-enabled: "true" # sync certificate to Cloudflare - cert-manager-sync.lestak.sh/cloudflare-secret-name: "example-cloudflare-secret" # secret in same namespace which contains the cloudflare api key. If provided in format "namespace/secret-name", will look in that namespace for the secret + cert-manager-sync.lestak.sh/cloudflare-secret-name: "example-cloudflare-secret" # secret in same namespace which contains the cloudflare api token. If provided in format "namespace/secret-name", will look in that namespace for the secret cert-manager-sync.lestak.sh/cloudflare-zone-id: "example-zone-id" # cloudflare zone id cert-manager-sync.lestak.sh/cloudflare-cert-id: "" # will be auto-filled by operator for in-place renewals cert-manager-sync.lestak.sh/digitalocean-enabled: "true" # sync certificate to DigitalOcean diff --git a/deploy/cert-manager-sync/values.yaml b/deploy/cert-manager-sync/values.yaml index 9314171..c47eff0 100644 --- a/deploy/cert-manager-sync/values.yaml +++ b/deploy/cert-manager-sync/values.yaml @@ -24,7 +24,7 @@ config: disableCache: "false" metrics: - enabled: false + enabled: true port: 9090 serviceAccount: diff --git a/go.mod b/go.mod index 7dbfae3..20f5b21 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.3 require ( cloud.google.com/go/certificatemanager v1.9.5 github.com/aws/aws-sdk-go v1.55.7 - github.com/cloudflare/cloudflare-go v0.115.0 + github.com/cloudflare/cloudflare-go/v5 v5.1.0 github.com/digitalocean/godo v1.151.0 github.com/google/uuid v1.6.0 github.com/hashicorp/vault/api v1.20.0 @@ -42,7 +42,6 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -76,6 +75,10 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect diff --git a/go.sum b/go.sum index a94daaa..0c62ba7 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= -github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= +github.com/cloudflare/cloudflare-go/v5 v5.1.0 h1:vvWUtrt5ZPEBFidL2ik64QipXLZmhMBgtRTw4bYvPwE= +github.com/cloudflare/cloudflare-go/v5 v5.1.0/go.mod h1:C6OjOlDHOk/g7lXehothXJRFZrSIJMLzOZB2SXQhcjk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -58,8 +58,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -195,6 +193,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/stores/cloudflare/cloudflare.go b/stores/cloudflare/cloudflare.go index e061d6a..54e30fb 100644 --- a/stores/cloudflare/cloudflare.go +++ b/stores/cloudflare/cloudflare.go @@ -5,7 +5,9 @@ import ( "fmt" "strings" - "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go/v5" + "github.com/cloudflare/cloudflare-go/v5/custom_certificates" + "github.com/cloudflare/cloudflare-go/v5/option" "github.com/robertlestak/cert-manager-sync/pkg/state" "github.com/robertlestak/cert-manager-sync/pkg/tlssecret" log "github.com/sirupsen/logrus" @@ -15,26 +17,21 @@ import ( type CloudflareStore struct { SecretName string SecretNamespace string - ApiKey string - ApiEmail string + ApiToken string ZoneId string CertId string } -func (s *CloudflareStore) GetApiKey(ctx context.Context) error { +func (s *CloudflareStore) GetApiToken(ctx context.Context) error { gopt := metav1.GetOptions{} sc, err := state.KubeClient.CoreV1().Secrets(s.SecretNamespace).Get(ctx, s.SecretName, gopt) if err != nil { return err } - if sc.Data["api_key"] == nil { - return fmt.Errorf("api_key not found in secret %s/%s", s.SecretNamespace, s.SecretName) + if sc.Data["api_token"] == nil { + return fmt.Errorf("api_token not found in secret %s/%s", s.SecretNamespace, s.SecretName) } - if sc.Data["email"] == nil { - return fmt.Errorf("email not found in secret %s/%s", s.SecretNamespace, s.SecretName) - } - s.ApiKey = string(sc.Data["api_key"]) - s.ApiEmail = string(sc.Data["email"]) + s.ApiToken = string(sc.Data["api_token"]) return nil } @@ -76,36 +73,40 @@ func (s *CloudflareStore) Sync(c *tlssecret.Certificate) (map[string]string, err return nil, fmt.Errorf("secret name not found in certificate annotations") } ctx := context.Background() - if err := s.GetApiKey(ctx); err != nil { - l.WithError(err).Errorf("GetApiKey error") + if err := s.GetApiToken(ctx); err != nil { + l.WithError(err).Errorf("GetApiToken error") return nil, err } - client, err := cloudflare.New(s.ApiKey, s.ApiEmail) - if err != nil { - l.WithError(err).Errorf("cloudflare.New error") - return nil, err - } - certRequest := cloudflare.ZoneCustomSSLOptions{ - Certificate: string(c.FullChain()), - PrivateKey: string(c.Key), - } + client := cloudflare.NewClient(option.WithAPIToken(s.ApiToken)) + origCertId := s.CertId - var sslCert cloudflare.ZoneCustomSSL + var cert *custom_certificates.CustomCertificate + var err error if s.CertId != "" { - sslCert, err = client.UpdateSSL(context.Background(), s.ZoneId, s.CertId, certRequest) + // Update existing certificate + cert, err = client.CustomCertificates.Edit(ctx, s.CertId, custom_certificates.CustomCertificateEditParams{ + ZoneID: cloudflare.F(s.ZoneId), + Certificate: cloudflare.F(string(c.FullChain())), + PrivateKey: cloudflare.F(string(c.Key)), + }) if err != nil { - l.WithError(err).Errorf("cloudflare.UpdateZoneCustomSSL error") + l.WithError(err).Errorf("cloudflare.CustomCertificates.Edit error") return nil, err } } else { - sslCert, err = client.CreateSSL(context.Background(), s.ZoneId, certRequest) + // Create new certificate + cert, err = client.CustomCertificates.New(ctx, custom_certificates.CustomCertificateNewParams{ + ZoneID: cloudflare.F(s.ZoneId), + Certificate: cloudflare.F(string(c.FullChain())), + PrivateKey: cloudflare.F(string(c.Key)), + }) if err != nil { - l.WithError(err).Errorf("cloudflare.CreateZoneCustomSSL error") + l.WithError(err).Errorf("cloudflare.CustomCertificates.New error") return nil, err } } - s.CertId = sslCert.ID - l = l.WithField("id", sslCert.ID) + s.CertId = cert.ID + l = l.WithField("id", cert.ID) var newKeys map[string]string if origCertId != s.CertId { newKeys = map[string]string{ From 28386a5b1c43aa3e29c1a281f98c83d7ca729f60 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 15 Aug 2025 19:55:33 -0400 Subject: [PATCH 2/3] perf: replace Docker builds with ko for faster multi-arch builds - Remove Dockerfile and replace with ko-based GitHub Actions workflow - Use ko's native multi-platform support for linux/amd64 and linux/arm64 - Significantly faster builds compared to Docker buildx - Use Chainguard static base image for minimal attack surface - Simplify workflow by leveraging ko's built-in manifest creation - Add .ko.yaml for configuration Benefits: - Faster builds (ko compiles directly, no Docker layers) - Smaller images (distroless/static base) - Better caching (Go module cache) - Simpler CI/CD pipeline --- .github/workflows/{docker.yml => build.yml} | 42 ++++++++++++++------- .ko.yaml | 2 + Dockerfile | 24 ------------ 3 files changed, 30 insertions(+), 38 deletions(-) rename .github/workflows/{docker.yml => build.yml} (51%) create mode 100644 .ko.yaml delete mode 100644 Dockerfile diff --git a/.github/workflows/docker.yml b/.github/workflows/build.yml similarity index 51% rename from .github/workflows/docker.yml rename to .github/workflows/build.yml index 848b2a8..5fc5791 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build and Push Docker Image +name: Build and Push Container Images on: push: @@ -22,13 +22,18 @@ jobs: permissions: contents: read packages: write - steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Set up ko + uses: ko-build/setup-ko@v0.7 - name: Log in to Container Registry if: github.event_name != 'pull_request' @@ -51,13 +56,22 @@ jobs: type=semver,pattern={{major}} type=raw,value=latest,enable={{is_default_branch}} - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Build and push multi-arch images + if: github.event_name != 'pull_request' + env: + KO_DOCKER_REPO: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + run: | + # Convert tags to comma-separated list for ko + TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ',' | sed 's/,$//') + echo "Building for tags: $TAGS" + + # Build and push multi-arch images with ko + ko build --bare --platform=linux/amd64,linux/arm64 --tags="$TAGS" ./cmd/cert-manager-sync + + - name: Build image for PR (dry-run) + if: github.event_name == 'pull_request' + env: + KO_DOCKER_REPO: ko.local + run: | + # For PRs, build locally without pushing (amd64 only for speed) + ko build --platform=linux/amd64 --local ./cmd/cert-manager-sync diff --git a/.ko.yaml b/.ko.yaml new file mode 100644 index 0000000..c4a663d --- /dev/null +++ b/.ko.yaml @@ -0,0 +1,2 @@ +# ko configuration for cert-manager-sync +defaultBaseImage: cgr.dev/chainguard/static:latest diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b585dbb..0000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM golang:1.24.3 as builder - -WORKDIR /app - -COPY . . - -RUN go mod download && go mod verify - -#RUN go test ./... - -RUN CGO_ENABLED=0 go build -o /app/cert-manager-sync cmd/cert-manager-sync/*.go - -FROM alpine:3.21 as alpine - -RUN apk add -U --no-cache ca-certificates - -FROM scratch as app - -WORKDIR /app - -COPY --from=builder /app/cert-manager-sync /app/cert-manager-sync -COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ - -ENTRYPOINT [ "/app/cert-manager-sync" ] From a28ad7eea4ce5d64df6807fcab92ff6cb8c5a6c7 Mon Sep 17 00:00:00 2001 From: Tristan Gosselin-Hane Date: Fri, 15 Aug 2025 19:56:15 -0400 Subject: [PATCH 3/3] fix: add support for all major Go/Docker architectures Add support for: - linux/amd64 (x86_64) - linux/arm64 (aarch64) - linux/arm/v7 (armv7) - linux/386 (i386) - linux/ppc64le (PowerPC 64-bit little-endian) - linux/s390x (IBM System z) --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fc5791..5051acb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,8 +65,8 @@ jobs: TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ',' | sed 's/,$//') echo "Building for tags: $TAGS" - # Build and push multi-arch images with ko - ko build --bare --platform=linux/amd64,linux/arm64 --tags="$TAGS" ./cmd/cert-manager-sync + # Build and push multi-arch images with ko (all major architectures) + ko build --bare --platform=linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x --tags="$TAGS" ./cmd/cert-manager-sync - name: Build image for PR (dry-run) if: github.event_name == 'pull_request'