Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Build and Push Container Images

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 Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Set up ko
uses: ko-build/[email protected]

- 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 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 (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'
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
2 changes: 2 additions & 0 deletions .ko.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# ko configuration for cert-manager-sync
defaultBaseImage: cgr.dev/chainguard/static:latest
24 changes: 0 additions & 24 deletions Dockerfile

This file was deleted.

10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion deploy/cert-manager-sync/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ config:
disableCache: "false"

metrics:
enabled: false
enabled: true
port: 9090

serviceAccount:
Expand Down
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
59 changes: 30 additions & 29 deletions stores/cloudflare/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}

Expand Down Expand Up @@ -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{
Expand Down