From 0e0a0169048125016099c2a6e6b9baaf7681d711 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Thu, 7 May 2026 10:10:34 +0100 Subject: [PATCH 01/15] feat: add Helm charts for operator and tunnel management Adds two Helm charts to complement the existing kustomize-based installation: - charts/cloudflare-operator: installs the controller Deployment, CRDs, RBAC (manager, leader-election, metrics-auth, metrics-reader roles), and ServiceAccount. CRDs are copied verbatim from config/crd/bases/ to avoid the symlink packaging failure that blocked the earlier WIP PR (#143). - charts/cloudflare-tunnels: renders ClusterTunnel CRs from a values list, centralising shared settings (protocol, rolling strategy, cloudflared args including --edge-ip-version auto, hard pod anti-affinity) so individual tunnel definitions only supply name and domain. Also adds: - make helm-sync-crds target (hooked into the existing manifests target) to keep chart CRDs in sync with controller-gen output, preventing drift. - helm/chart-releaser-action job in release.yml to publish both charts to GitHub Pages on version tags. Closes #108 --- .github/workflows/release.yml | 20 + Makefile | 6 +- charts/cloudflare-operator/.helmignore | 4 + charts/cloudflare-operator/Chart.yaml | 19 + ...orking.cfargotunnel.com_accesstunnels.yaml | 111 +++++ ...rking.cfargotunnel.com_clustertunnels.yaml | 390 ++++++++++++++++++ ...rking.cfargotunnel.com_tunnelbindings.yaml | 174 ++++++++ .../networking.cfargotunnel.com_tunnels.yaml | 390 ++++++++++++++++++ .../cloudflare-operator/templates/NOTES.txt | 10 + .../templates/_helpers.tpl | 21 + .../templates/deployment.yaml | 82 ++++ .../templates/namespace.yaml | 9 + .../cloudflare-operator/templates/rbac.yaml | 121 ++++++ .../templates/service.yaml | 17 + .../templates/serviceaccount.yaml | 7 + charts/cloudflare-operator/values.yaml | 37 ++ charts/cloudflare-tunnels/.helmignore | 4 + charts/cloudflare-tunnels/Chart.yaml | 19 + charts/cloudflare-tunnels/templates/NOTES.txt | 24 ++ .../templates/clustertunnel.yaml | 46 +++ charts/cloudflare-tunnels/values.yaml | 55 +++ 21 files changed, 1565 insertions(+), 1 deletion(-) create mode 100644 charts/cloudflare-operator/.helmignore create mode 100644 charts/cloudflare-operator/Chart.yaml create mode 100644 charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml create mode 100644 charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml create mode 100644 charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml create mode 100644 charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml create mode 100644 charts/cloudflare-operator/templates/NOTES.txt create mode 100644 charts/cloudflare-operator/templates/_helpers.tpl create mode 100644 charts/cloudflare-operator/templates/deployment.yaml create mode 100644 charts/cloudflare-operator/templates/namespace.yaml create mode 100644 charts/cloudflare-operator/templates/rbac.yaml create mode 100644 charts/cloudflare-operator/templates/service.yaml create mode 100644 charts/cloudflare-operator/templates/serviceaccount.yaml create mode 100644 charts/cloudflare-operator/values.yaml create mode 100644 charts/cloudflare-tunnels/.helmignore create mode 100644 charts/cloudflare-tunnels/Chart.yaml create mode 100644 charts/cloudflare-tunnels/templates/NOTES.txt create mode 100644 charts/cloudflare-tunnels/templates/clustertunnel.yaml create mode 100644 charts/cloudflare-tunnels/values.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be987501..31f3512b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,26 @@ on: workflow_dispatch: jobs: + release-charts: + # Publish Helm charts to the gh-pages branch via chart-releaser on version tags. + runs-on: ubuntu-latest + if: github.event_name == 'create' && github.event.ref_type == 'tag' && startsWith(github.event.ref, 'v') + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + docker: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index d1fc0aa2..075c965c 100644 --- a/Makefile +++ b/Makefile @@ -95,9 +95,13 @@ help: ## Display this help. ##@ Development .PHONY: manifests -manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. +manifests: controller-gen helm-sync-crds ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases +.PHONY: helm-sync-crds +helm-sync-crds: ## Sync CRD YAMLs from config/crd/bases into charts/cloudflare-operator/crds/ + cp config/crd/bases/*.yaml charts/cloudflare-operator/crds/ + .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." diff --git a/charts/cloudflare-operator/.helmignore b/charts/cloudflare-operator/.helmignore new file mode 100644 index 00000000..2bbc10d7 --- /dev/null +++ b/charts/cloudflare-operator/.helmignore @@ -0,0 +1,4 @@ +.DS_Store +*.tgz +.git/ +.gitignore diff --git a/charts/cloudflare-operator/Chart.yaml b/charts/cloudflare-operator/Chart.yaml new file mode 100644 index 00000000..7aedbad0 --- /dev/null +++ b/charts/cloudflare-operator/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: cloudflare-operator +description: > + Helm chart for the Cloudflare Operator controller manager. + Installs the controller Deployment, CRDs, RBAC, and ServiceAccount + required to manage Cloudflare Tunnels via Kubernetes custom resources. +type: application +version: 0.1.0 +appVersion: "0.13.1" +keywords: + - cloudflare + - tunnel + - operator +home: https://github.com/adyanth/cloudflare-operator +sources: + - https://github.com/adyanth/cloudflare-operator +maintainers: + - name: adyanth + url: https://github.com/adyanth diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml new file mode 100644 index 00000000..58a6ebfb --- /dev/null +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml @@ -0,0 +1,111 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: accesstunnels.networking.cfargotunnel.com +spec: + group: networking.cfargotunnel.com + names: + kind: AccessTunnel + listKind: AccessTunnelList + plural: accesstunnels + singular: accesstunnel + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .target.fqdn + name: Target + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: AccessTunnel is the Schema for the accesstunnels API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + serviceToken: + description: AccessTunnelServiceToken defines the access auth if needed + properties: + CLOUDFLARE_ACCESS_SERVICE_TOKEN_ID: + default: CLOUDFLARE_ACCESS_SERVICE_TOKEN_ID + description: Key in the secret to use for Access Service Token ID, + defaults to CLOUDFLARE_ACCESS_SERVICE_TOKEN_ID + type: string + CLOUDFLARE_ACCESS_SERVICE_TOKEN_TOKEN: + default: CLOUDFLARE_ACCESS_SERVICE_TOKEN_TOKEN + description: Key in the secret to use for Access Service Token Token, + defaults to CLOUDFLARE_ACCESS_SERVICE_TOKEN_TOKEN + type: string + secretRef: + description: Access Service Token Secret + type: string + required: + - secretRef + type: object + status: + description: AccessTunnelStatus defines the observed state of Access + type: object + target: + description: AccessTunnelTarget defines the desired state of Access + properties: + fqdn: + description: |- + Fqdn specifies the DNS name to access + This is not validated and used as provided + type: string + image: + default: cloudflare/cloudflared:2025.4.0 + description: cloudflared image to use + type: string + protocol: + default: tcp + description: Protocol to forward, better to use TCP? + enum: + - tcp + - rdp + - smb + - ssh + type: string + svc: + description: Service Config + properties: + name: + description: |- + Name of the new service to create + Defaults to the name of the Access object + type: string + port: + default: 8000 + description: |- + Service port to expose with + Defaults to 8000 + format: int32 + maximum: 65535 + minimum: 1 + type: integer + type: object + required: + - fqdn + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml new file mode 100644 index 00000000..b44fc2af --- /dev/null +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml @@ -0,0 +1,390 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: clustertunnels.networking.cfargotunnel.com +spec: + group: networking.cfargotunnel.com + names: + kind: ClusterTunnel + listKind: ClusterTunnelList + plural: clustertunnels + singular: clustertunnel + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.tunnelId + name: TunnelID + type: string + deprecated: true + deprecationWarning: networking.cfargotunnel.com/v1alpha1 ClusterTunnel is deprecated, + see https://github.com/adyanth/cloudflare-operator/tree/v0.13.0/docs/migration/crd/v1alpha2.md + for migrating to v1alpha2 + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterTunnel is the Schema for the clustertunnels API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TunnelSpec defines the desired state of Tunnel + properties: + cloudflare: + description: Cloudflare Credentials + properties: + CLOUDFLARE_API_KEY: + default: CLOUDFLARE_API_KEY + description: |- + Key in the secret to use for Cloudflare API Key, defaults to CLOUDFLARE_API_KEY. Needs Email also to be provided. + For Delete operations for new tunnels only, or as an alternate to API Token + type: string + CLOUDFLARE_API_TOKEN: + default: CLOUDFLARE_API_TOKEN + description: Key in the secret to use for Cloudflare API token, + defaults to CLOUDFLARE_API_TOKEN + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_FILE: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + description: Key in the secret to use as credentials.json for + an existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + description: Key in the secret to use as tunnel secret for an + existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + type: string + accountId: + description: Account ID in Cloudflare. AccountId and AccountName + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + accountName: + description: Account Name in Cloudflare. AccountName and AccountId + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + domain: + description: Cloudflare Domain to which this tunnel belongs to + type: string + email: + description: Email to use along with API Key for Delete operations + for new tunnels only, or as an alternate to API Token + type: string + secret: + description: Secret containing Cloudflare API key/token + type: string + required: + - domain + - secret + type: object + existingTunnel: + description: |- + Existing tunnel object. + ExistingTunnel and NewTunnel cannot be both empty and are mutually exclusive. + properties: + id: + description: Existing Tunnel ID to run on. Tunnel ID and Tunnel + Name cannot be both empty. If both are provided, ID is used + if valid, else falls back to Name. + type: string + name: + description: Existing Tunnel name to run on. Tunnel Name and Tunnel + ID cannot be both empty. If both are provided, ID is used if + valid, else falls back to Name. + type: string + type: object + fallbackTarget: + default: http_status:404 + description: FallbackTarget speficies the target for requests that + do not match an ingress. Defaults to http_status:404 + type: string + image: + default: cloudflare/cloudflared:2025.4.0 + description: Image sets the Cloudflared Image to use. Defaults to + the image set during the release of the operator. + type: string + newTunnel: + description: |- + New tunnel object. + NewTunnel and ExistingTunnel cannot be both empty and are mutually exclusive. + properties: + name: + description: Tunnel name to create on Cloudflare. + type: string + required: + - name + type: object + noTlsVerify: + default: false + description: NoTlsVerify disables origin TLS certificate checks when + the endpoint is HTTPS. + type: boolean + nodeSelectors: + additionalProperties: + type: string + description: NodeSelectors specifies the nodeSelectors to apply to + the cloudflared tunnel deployment + type: object + originCaPool: + description: OriginCaPool speficies the secret with tls.crt (and other + certs as needed to be referred in the service annotation) of the + Root CA to be trusted when sending traffic to HTTPS endpoints + type: string + protocol: + default: auto + description: Protocol specifies the protocol to use for the tunnel. + Defaults to auto. Options are "auto", "quic" and "http2" + enum: + - auto + - quic + - http2 + type: string + size: + default: 1 + description: Size defines the number of Daemon pods to run for this + tunnel + format: int32 + minimum: 0 + type: integer + tolerations: + description: Tolerations specifies the tolerations to apply to the + cloudflared tunnel deployment + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + required: + - cloudflare + type: object + status: + description: TunnelStatus defines the observed state of Tunnel + properties: + accountId: + type: string + tunnelId: + type: string + tunnelName: + type: string + zoneId: + type: string + required: + - accountId + - tunnelId + - tunnelName + - zoneId + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .status.tunnelId + name: TunnelID + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: ClusterTunnel is the Schema for the clustertunnels API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TunnelSpec defines the desired state of Tunnel + properties: + cloudflare: + description: Cloudflare Credentials + properties: + CLOUDFLARE_API_KEY: + default: CLOUDFLARE_API_KEY + description: |- + Key in the secret to use for Cloudflare API Key, defaults to CLOUDFLARE_API_KEY. Needs Email also to be provided. + For Delete operations for new tunnels only, or as an alternate to API Token + type: string + CLOUDFLARE_API_TOKEN: + default: CLOUDFLARE_API_TOKEN + description: Key in the secret to use for Cloudflare API token, + defaults to CLOUDFLARE_API_TOKEN + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_FILE: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + description: Key in the secret to use as credentials.json for + an existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + description: Key in the secret to use as tunnel secret for an + existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + type: string + accountId: + description: Account ID in Cloudflare. AccountId and AccountName + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + accountName: + description: Account Name in Cloudflare. AccountName and AccountId + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + domain: + description: Cloudflare Domain to which this tunnel belongs to + type: string + email: + description: Email to use along with API Key for Delete operations + for new tunnels only, or as an alternate to API Token + type: string + secret: + description: Secret containing Cloudflare API key/token + type: string + required: + - domain + - secret + type: object + deployPatch: + default: '{}' + description: |- + Deployment patch for the cloudflared deployment. + Follows https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/ + type: string + existingTunnel: + description: |- + Existing tunnel object. + ExistingTunnel and NewTunnel cannot be both empty and are mutually exclusive. + properties: + id: + description: Existing Tunnel ID to run on. Tunnel ID and Tunnel + Name cannot be both empty. If both are provided, ID is used + if valid, else falls back to Name. + type: string + name: + description: Existing Tunnel name to run on. Tunnel Name and Tunnel + ID cannot be both empty. If both are provided, ID is used if + valid, else falls back to Name. + type: string + type: object + fallbackTarget: + default: http_status:404 + description: FallbackTarget speficies the target for requests that + do not match an ingress. Defaults to http_status:404 + type: string + newTunnel: + description: |- + New tunnel object. + NewTunnel and ExistingTunnel cannot be both empty and are mutually exclusive. + properties: + name: + description: Tunnel name to create on Cloudflare. + type: string + required: + - name + type: object + noTlsVerify: + default: false + description: NoTlsVerify disables origin TLS certificate checks when + the endpoint is HTTPS. + type: boolean + originCaPool: + description: OriginCaPool speficies the secret with tls.crt (and other + certs as needed to be referred in the service annotation) of the + Root CA to be trusted when sending traffic to HTTPS endpoints + type: string + protocol: + default: auto + description: Protocol specifies the protocol to use for the tunnel. + Defaults to auto. Options are "auto", "quic" and "http2" + enum: + - auto + - quic + - http2 + type: string + required: + - cloudflare + type: object + status: + description: TunnelStatus defines the observed state of Tunnel + properties: + accountId: + type: string + tunnelId: + type: string + tunnelName: + type: string + zoneId: + type: string + required: + - accountId + - tunnelId + - tunnelName + - zoneId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml new file mode 100644 index 00000000..f03e13ee --- /dev/null +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml @@ -0,0 +1,174 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: tunnelbindings.networking.cfargotunnel.com +spec: + group: networking.cfargotunnel.com + names: + kind: TunnelBinding + listKind: TunnelBindingList + plural: tunnelbindings + singular: tunnelbinding + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.hostnames + name: FQDNs + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: TunnelBinding is the Schema for the tunnelbindings API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + description: TunnelBindingStatus defines the observed state of TunnelBinding + properties: + hostnames: + description: To show on the kubectl cli + type: string + services: + items: + description: ServiceInfo stores the Hostname and Target for each + service + properties: + hostname: + description: FQDN of the service + type: string + target: + description: Target for cloudflared + type: string + required: + - hostname + - target + type: object + type: array + required: + - hostnames + - services + type: object + subjects: + items: + description: TunnelBindingSubject defines the subject TunnelBinding + connects to the Tunnel + properties: + kind: + default: Service + description: Kind can be Service + type: string + name: + type: string + spec: + properties: + caPool: + description: |- + CaPool trusts the CA certificate referenced by the key in the secret specified in tunnel.spec.originCaPool. + tls.crt is trusted globally and does not need to be specified. Only useful if the protocol is HTTPS. + type: string + fqdn: + description: |- + Fqdn specifies the DNS name to access this service from. + Defaults to the service.metadata.name + tunnel.spec.domain. + If specifying this, make sure to use the same domain that the tunnel belongs to. + This is not validated and used as provided + type: string + http2Origin: + default: false + description: |- + Http2Origin makes the service attempt to connect to origin using HTTP2. + Origin must be configured as https. + type: boolean + noTlsVerify: + default: false + description: |- + NoTlsVerify disables TLS verification for this service. + Only useful if the protocol is HTTPS. + type: boolean + path: + description: |- + Path specifies a regular expression for to match on the request for http/https services + If a rule does not specify a path, all paths will be matched. + type: string + protocol: + description: |- + Protocol specifies the protocol for the service. Should be one of http, https, tcp, udp, ssh or rdp. + Defaults to http, with the exceptions of https for 443, smb for 139 and 445, rdp for 3389 and ssh for 22 if the service has a TCP port. + The only available option for a UDP port is udp, which is default. + type: string + proxyAddress: + default: 127.0.0.1 + description: ProxyAddress configures the listen address for + that proxy + pattern: ((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$)) + type: string + proxyPort: + default: 0 + description: ProxyPort configures the listen port for that proxy + maximum: 65535 + minimum: 0 + type: integer + proxyType: + default: "" + description: ProxyType configures the proxy type. + enum: + - "" + - socks + type: string + target: + description: |- + Target specified where the tunnel should proxy to. + Defaults to the form of ://..svc: + type: string + type: object + required: + - name + type: object + type: array + tunnelRef: + description: TunnelRef defines the Tunnel TunnelBinding connects to + properties: + disableDNSUpdates: + description: DisableDNSUpdates disables the DNS updates on Cloudflare, + just managing the configs. Assumes the DNS entries are manually + added. + type: boolean + kind: + description: Kind can be Tunnel or ClusterTunnel + enum: + - ClusterTunnel + - Tunnel + type: string + name: + description: Name of the tunnel resource + type: string + required: + - kind + - name + type: object + required: + - subjects + - tunnelRef + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml new file mode 100644 index 00000000..26f6f241 --- /dev/null +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml @@ -0,0 +1,390 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: tunnels.networking.cfargotunnel.com +spec: + group: networking.cfargotunnel.com + names: + kind: Tunnel + listKind: TunnelList + plural: tunnels + singular: tunnel + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.tunnelId + name: TunnelID + type: string + deprecated: true + deprecationWarning: networking.cfargotunnel.com/v1alpha1 Tunnel is deprecated, + see https://github.com/adyanth/cloudflare-operator/tree/v0.13.0/docs/migration/crd/v1alpha2.md + for migrating to v1alpha2 + name: v1alpha1 + schema: + openAPIV3Schema: + description: Tunnel is the Schema for the tunnels API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TunnelSpec defines the desired state of Tunnel + properties: + cloudflare: + description: Cloudflare Credentials + properties: + CLOUDFLARE_API_KEY: + default: CLOUDFLARE_API_KEY + description: |- + Key in the secret to use for Cloudflare API Key, defaults to CLOUDFLARE_API_KEY. Needs Email also to be provided. + For Delete operations for new tunnels only, or as an alternate to API Token + type: string + CLOUDFLARE_API_TOKEN: + default: CLOUDFLARE_API_TOKEN + description: Key in the secret to use for Cloudflare API token, + defaults to CLOUDFLARE_API_TOKEN + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_FILE: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + description: Key in the secret to use as credentials.json for + an existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + description: Key in the secret to use as tunnel secret for an + existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + type: string + accountId: + description: Account ID in Cloudflare. AccountId and AccountName + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + accountName: + description: Account Name in Cloudflare. AccountName and AccountId + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + domain: + description: Cloudflare Domain to which this tunnel belongs to + type: string + email: + description: Email to use along with API Key for Delete operations + for new tunnels only, or as an alternate to API Token + type: string + secret: + description: Secret containing Cloudflare API key/token + type: string + required: + - domain + - secret + type: object + existingTunnel: + description: |- + Existing tunnel object. + ExistingTunnel and NewTunnel cannot be both empty and are mutually exclusive. + properties: + id: + description: Existing Tunnel ID to run on. Tunnel ID and Tunnel + Name cannot be both empty. If both are provided, ID is used + if valid, else falls back to Name. + type: string + name: + description: Existing Tunnel name to run on. Tunnel Name and Tunnel + ID cannot be both empty. If both are provided, ID is used if + valid, else falls back to Name. + type: string + type: object + fallbackTarget: + default: http_status:404 + description: FallbackTarget speficies the target for requests that + do not match an ingress. Defaults to http_status:404 + type: string + image: + default: cloudflare/cloudflared:2025.4.0 + description: Image sets the Cloudflared Image to use. Defaults to + the image set during the release of the operator. + type: string + newTunnel: + description: |- + New tunnel object. + NewTunnel and ExistingTunnel cannot be both empty and are mutually exclusive. + properties: + name: + description: Tunnel name to create on Cloudflare. + type: string + required: + - name + type: object + noTlsVerify: + default: false + description: NoTlsVerify disables origin TLS certificate checks when + the endpoint is HTTPS. + type: boolean + nodeSelectors: + additionalProperties: + type: string + description: NodeSelectors specifies the nodeSelectors to apply to + the cloudflared tunnel deployment + type: object + originCaPool: + description: OriginCaPool speficies the secret with tls.crt (and other + certs as needed to be referred in the service annotation) of the + Root CA to be trusted when sending traffic to HTTPS endpoints + type: string + protocol: + default: auto + description: Protocol specifies the protocol to use for the tunnel. + Defaults to auto. Options are "auto", "quic" and "http2" + enum: + - auto + - quic + - http2 + type: string + size: + default: 1 + description: Size defines the number of Daemon pods to run for this + tunnel + format: int32 + minimum: 0 + type: integer + tolerations: + description: Tolerations specifies the tolerations to apply to the + cloudflared tunnel deployment + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + required: + - cloudflare + type: object + status: + description: TunnelStatus defines the observed state of Tunnel + properties: + accountId: + type: string + tunnelId: + type: string + tunnelName: + type: string + zoneId: + type: string + required: + - accountId + - tunnelId + - tunnelName + - zoneId + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .status.tunnelId + name: TunnelID + type: string + name: v1alpha2 + schema: + openAPIV3Schema: + description: Tunnel is the Schema for the tunnels API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TunnelSpec defines the desired state of Tunnel + properties: + cloudflare: + description: Cloudflare Credentials + properties: + CLOUDFLARE_API_KEY: + default: CLOUDFLARE_API_KEY + description: |- + Key in the secret to use for Cloudflare API Key, defaults to CLOUDFLARE_API_KEY. Needs Email also to be provided. + For Delete operations for new tunnels only, or as an alternate to API Token + type: string + CLOUDFLARE_API_TOKEN: + default: CLOUDFLARE_API_TOKEN + description: Key in the secret to use for Cloudflare API token, + defaults to CLOUDFLARE_API_TOKEN + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_FILE: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + description: Key in the secret to use as credentials.json for + an existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_FILE + type: string + CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET: + default: CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + description: Key in the secret to use as tunnel secret for an + existing tunnel, defaults to CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET + type: string + accountId: + description: Account ID in Cloudflare. AccountId and AccountName + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + accountName: + description: Account Name in Cloudflare. AccountName and AccountId + cannot be both empty. If both are provided, Account ID is used + if valid, else falls back to Account Name. + type: string + domain: + description: Cloudflare Domain to which this tunnel belongs to + type: string + email: + description: Email to use along with API Key for Delete operations + for new tunnels only, or as an alternate to API Token + type: string + secret: + description: Secret containing Cloudflare API key/token + type: string + required: + - domain + - secret + type: object + deployPatch: + default: '{}' + description: |- + Deployment patch for the cloudflared deployment. + Follows https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/ + type: string + existingTunnel: + description: |- + Existing tunnel object. + ExistingTunnel and NewTunnel cannot be both empty and are mutually exclusive. + properties: + id: + description: Existing Tunnel ID to run on. Tunnel ID and Tunnel + Name cannot be both empty. If both are provided, ID is used + if valid, else falls back to Name. + type: string + name: + description: Existing Tunnel name to run on. Tunnel Name and Tunnel + ID cannot be both empty. If both are provided, ID is used if + valid, else falls back to Name. + type: string + type: object + fallbackTarget: + default: http_status:404 + description: FallbackTarget speficies the target for requests that + do not match an ingress. Defaults to http_status:404 + type: string + newTunnel: + description: |- + New tunnel object. + NewTunnel and ExistingTunnel cannot be both empty and are mutually exclusive. + properties: + name: + description: Tunnel name to create on Cloudflare. + type: string + required: + - name + type: object + noTlsVerify: + default: false + description: NoTlsVerify disables origin TLS certificate checks when + the endpoint is HTTPS. + type: boolean + originCaPool: + description: OriginCaPool speficies the secret with tls.crt (and other + certs as needed to be referred in the service annotation) of the + Root CA to be trusted when sending traffic to HTTPS endpoints + type: string + protocol: + default: auto + description: Protocol specifies the protocol to use for the tunnel. + Defaults to auto. Options are "auto", "quic" and "http2" + enum: + - auto + - quic + - http2 + type: string + required: + - cloudflare + type: object + status: + description: TunnelStatus defines the observed state of Tunnel + properties: + accountId: + type: string + tunnelId: + type: string + tunnelName: + type: string + zoneId: + type: string + required: + - accountId + - tunnelId + - tunnelName + - zoneId + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/cloudflare-operator/templates/NOTES.txt b/charts/cloudflare-operator/templates/NOTES.txt new file mode 100644 index 00000000..66e7c2cf --- /dev/null +++ b/charts/cloudflare-operator/templates/NOTES.txt @@ -0,0 +1,10 @@ +Cloudflare Operator has been installed! + +Controller manager is running in namespace: {{ include "cloudflare-operator.namespace" . }} + +To verify the installation: + kubectl get pods -n {{ include "cloudflare-operator.namespace" . }} + +To create a Cloudflare Tunnel, see the cloudflare-tunnels chart or the +ClusterTunnel / Tunnel CRD documentation: + https://github.com/adyanth/cloudflare-operator/tree/main/docs diff --git a/charts/cloudflare-operator/templates/_helpers.tpl b/charts/cloudflare-operator/templates/_helpers.tpl new file mode 100644 index 00000000..a46e32ea --- /dev/null +++ b/charts/cloudflare-operator/templates/_helpers.tpl @@ -0,0 +1,21 @@ +{{- define "cloudflare-operator.name" -}} +{{- .Chart.Name }} +{{- end }} + +{{- define "cloudflare-operator.fullname" -}} +{{- .Chart.Name }} +{{- end }} + +{{- define "cloudflare-operator.namespace" -}} +{{- .Values.namespace }} +{{- end }} + +{{- define "cloudflare-operator.serviceAccountName" -}} +{{- .Values.serviceAccountName }} +{{- end }} + +{{- define "cloudflare-operator.labels" -}} +app.kubernetes.io/name: {{ include "cloudflare-operator.name" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} diff --git a/charts/cloudflare-operator/templates/deployment.yaml b/charts/cloudflare-operator/templates/deployment.yaml new file mode 100644 index 00000000..d962c44b --- /dev/null +++ b/charts/cloudflare-operator/templates/deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-controller-manager + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + control-plane: controller-manager + {{- include "cloudflare-operator.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - key: kubernetes.io/os + operator: In + values: + - linux + {{- with .Values.affinity }} + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: {{ include "cloudflare-operator.serviceAccountName" . }} + terminationGracePeriodSeconds: 10 + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: manager + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /manager + args: + - --leader-elect + - --health-probe-bind-address=:8081 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 10 }} diff --git a/charts/cloudflare-operator/templates/namespace.yaml b/charts/cloudflare-operator/templates/namespace.yaml new file mode 100644 index 00000000..542c5edf --- /dev/null +++ b/charts/cloudflare-operator/templates/namespace.yaml @@ -0,0 +1,9 @@ +{{- if .Values.createNamespace }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "cloudflare-operator.namespace" . }} + labels: + control-plane: controller-manager + {{- include "cloudflare-operator.labels" . | nindent 4 }} +{{- end }} diff --git a/charts/cloudflare-operator/templates/rbac.yaml b/charts/cloudflare-operator/templates/rbac.yaml new file mode 100644 index 00000000..e9c07cbc --- /dev/null +++ b/charts/cloudflare-operator/templates/rbac.yaml @@ -0,0 +1,121 @@ +--- +# manager ClusterRole — full API access required by the controller +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-manager-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: +- apiGroups: [""] + resources: [events] + verbs: [create, patch] +- apiGroups: [apps] + resources: [deployments] + verbs: [create, delete, get, list, patch, update, watch] +- apiGroups: [""] + resources: [configmaps, secrets] + verbs: [create, delete, get, list, patch, update, watch] +- apiGroups: [""] + resources: [services] + verbs: [create, get, list, patch, update, watch] +- apiGroups: [networking.cfargotunnel.com] + resources: [accesstunnels, clustertunnels, tunnelbindings, tunnels] + verbs: [create, delete, get, list, patch, update, watch] +- apiGroups: [networking.cfargotunnel.com] + resources: [accesstunnels/status, clustertunnels/status, tunnelbindings/status, tunnels/status] + verbs: [get, patch, update] +- apiGroups: [networking.cfargotunnel.com] + resources: [clustertunnels/finalizers, tunnelbindings/finalizers, tunnels/finalizers] + verbs: [update] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-manager-rolebinding + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "cloudflare-operator.fullname" . }}-manager-role +subjects: +- kind: ServiceAccount + name: {{ include "cloudflare-operator.serviceAccountName" . }} + namespace: {{ include "cloudflare-operator.namespace" . }} +--- +# leader-election Role — namespace-scoped +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-leader-election-role + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: +- apiGroups: [""] + resources: [configmaps] + verbs: [get, list, watch, create, update, patch, delete] +- apiGroups: [coordination.k8s.io] + resources: [leases] + verbs: [get, list, watch, create, update, patch, delete] +- apiGroups: [""] + resources: [events] + verbs: [create, patch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-leader-election-rolebinding + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "cloudflare-operator.fullname" . }}-leader-election-role +subjects: +- kind: ServiceAccount + name: {{ include "cloudflare-operator.serviceAccountName" . }} + namespace: {{ include "cloudflare-operator.namespace" . }} +--- +# metrics-auth ClusterRole — allows the controller to authenticate metric scrapes +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-metrics-auth-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: +- apiGroups: [authentication.k8s.io] + resources: [tokenreviews] + verbs: [create] +- apiGroups: [authorization.k8s.io] + resources: [subjectaccessreviews] + verbs: [create] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-metrics-auth-rolebinding + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "cloudflare-operator.fullname" . }}-metrics-auth-role +subjects: +- kind: ServiceAccount + name: {{ include "cloudflare-operator.serviceAccountName" . }} + namespace: {{ include "cloudflare-operator.namespace" . }} +--- +# metrics-reader ClusterRole — grants read access to /metrics endpoint +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-metrics-reader + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: +- nonResourceURLs: ["/metrics"] + verbs: [get] diff --git a/charts/cloudflare-operator/templates/service.yaml b/charts/cloudflare-operator/templates/service.yaml new file mode 100644 index 00000000..02820f0d --- /dev/null +++ b/charts/cloudflare-operator/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-controller-manager-metrics-service + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + control-plane: controller-manager + app.kubernetes.io/component: metrics + {{- include "cloudflare-operator.labels" . | nindent 4 }} +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + control-plane: controller-manager diff --git a/charts/cloudflare-operator/templates/serviceaccount.yaml b/charts/cloudflare-operator/templates/serviceaccount.yaml new file mode 100644 index 00000000..74e07543 --- /dev/null +++ b/charts/cloudflare-operator/templates/serviceaccount.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "cloudflare-operator.serviceAccountName" . }} + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} diff --git a/charts/cloudflare-operator/values.yaml b/charts/cloudflare-operator/values.yaml new file mode 100644 index 00000000..77dc360a --- /dev/null +++ b/charts/cloudflare-operator/values.yaml @@ -0,0 +1,37 @@ +# Namespace where the operator will be installed. +namespace: cloudflare-operator-system + +# Whether to create the namespace resource. +# Set to false if the namespace already exists. +createNamespace: true + +image: + repository: adyanth/cloudflare-operator + tag: "0.13.1" + pullPolicy: IfNotPresent + +replicaCount: 1 + +resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 100m + memory: 100Mi + +# Node selector applied to the controller Deployment. +nodeSelector: {} + +# Tolerations applied to the controller Deployment. +tolerations: [] + +# Additional affinity rules. The chart already applies a nodeAffinity for +# amd64/arm64 Linux nodes (mirroring the upstream kustomize manifest). +affinity: {} + +# Annotations to add to the controller Pod. +podAnnotations: {} + +# ServiceAccount name used by the controller. +serviceAccountName: cloudflare-operator-controller-manager diff --git a/charts/cloudflare-tunnels/.helmignore b/charts/cloudflare-tunnels/.helmignore new file mode 100644 index 00000000..2bbc10d7 --- /dev/null +++ b/charts/cloudflare-tunnels/.helmignore @@ -0,0 +1,4 @@ +.DS_Store +*.tgz +.git/ +.gitignore diff --git a/charts/cloudflare-tunnels/Chart.yaml b/charts/cloudflare-tunnels/Chart.yaml new file mode 100644 index 00000000..d976162d --- /dev/null +++ b/charts/cloudflare-tunnels/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: cloudflare-tunnels +description: > + Renders ClusterTunnel CRs for the cloudflare-operator. + Centralises shared deployment settings (protocol, rolling strategy, + cloudflared args, anti-affinity) so individual tunnel definitions only + need to supply their unique fields (name, domain). + Requires the cloudflare-operator chart to be installed first. +type: application +version: 0.1.0 +keywords: + - cloudflare + - tunnel +home: https://github.com/adyanth/cloudflare-operator +sources: + - https://github.com/adyanth/cloudflare-operator +maintainers: + - name: adyanth + url: https://github.com/adyanth diff --git a/charts/cloudflare-tunnels/templates/NOTES.txt b/charts/cloudflare-tunnels/templates/NOTES.txt new file mode 100644 index 00000000..63a5d4b4 --- /dev/null +++ b/charts/cloudflare-tunnels/templates/NOTES.txt @@ -0,0 +1,24 @@ +{{- if not .Values.tunnels }} +No ClusterTunnel resources were created because .Values.tunnels is empty. + +To create tunnels, set the tunnels list in your values file: + + cloudflare: + email: you@example.com + accountId: + secret: cloudflare-secrets # name of the pre-existing K8s Secret + + tunnels: + - name: my-site + domain: example.com + +See the cloudflare-operator chart to install the controller first. +{{- else }} +The following ClusterTunnel resources have been created: +{{- range .Values.tunnels }} + - cloudflared-tunnel-{{ .name }} ({{ .domain }}) +{{- end }} + +It may take a few minutes for cloudflared pods to become Ready while the +operator provisions the tunnels with Cloudflare. +{{- end }} diff --git a/charts/cloudflare-tunnels/templates/clustertunnel.yaml b/charts/cloudflare-tunnels/templates/clustertunnel.yaml new file mode 100644 index 00000000..deadabc8 --- /dev/null +++ b/charts/cloudflare-tunnels/templates/clustertunnel.yaml @@ -0,0 +1,46 @@ +{{- range .Values.tunnels }} +{{- $tunnel := . }} +{{- $replicas := $tunnel.replicas | default $.Values.defaults.replicas }} +{{- $protocol := $tunnel.protocol | default $.Values.defaults.protocol }} +{{- $strategy := $tunnel.strategy | default $.Values.defaults.strategy }} +{{- $args := $tunnel.args | default $.Values.defaults.args }} +--- +apiVersion: networking.cfargotunnel.com/v1alpha2 +kind: ClusterTunnel +metadata: + name: cloudflared-tunnel-{{ $tunnel.name }} +spec: + newTunnel: + name: {{ $tunnel.name }} + cloudflare: + email: {{ $.Values.cloudflare.email }} + domain: {{ $tunnel.domain }} + secret: {{ $.Values.cloudflare.secret }} + accountId: {{ $.Values.cloudflare.accountId }} + protocol: {{ $protocol }} + deployPatch: | + spec: + replicas: {{ $replicas }} + strategy: + rollingUpdate: + maxSurge: {{ $strategy.maxSurge }} + maxUnavailable: {{ $strategy.maxUnavailable }} + template: + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: cfargotunnel.com/name + operator: In + values: + - {{ $tunnel.name }} + topologyKey: kubernetes.io/hostname + containers: + - name: cloudflared + args: + {{- range $args }} + - {{ . | quote }} + {{- end }} +{{- end }} diff --git a/charts/cloudflare-tunnels/values.yaml b/charts/cloudflare-tunnels/values.yaml new file mode 100644 index 00000000..5413d176 --- /dev/null +++ b/charts/cloudflare-tunnels/values.yaml @@ -0,0 +1,55 @@ +# Shared Cloudflare account credentials applied to every tunnel. +# These must be set — there are no defaults. +cloudflare: + # Cloudflare account email address. + email: "" + # Cloudflare account ID. + accountId: "" + # Name of the Kubernetes Secret that holds the Cloudflare API token. + # The Secret must exist in the same namespace as the operator. + secret: cloudflare-secrets + +# Defaults applied to every tunnel unless overridden per-tunnel. +defaults: + # cloudflared tunnel protocol. http2 is recommended over auto for stability. + protocol: http2 + + # Number of cloudflared pod replicas per tunnel. + # When using hard pod anti-affinity this must not exceed the number of nodes. + replicas: 2 + + # Rolling update strategy. + # maxSurge must be 0 when replicas == node count and hard anti-affinity is + # in use; surge pods cannot be scheduled until an existing pod is evicted. + strategy: + maxSurge: 0 + maxUnavailable: 1 + + # cloudflared args passed to every tunnel Deployment via deployPatch. + # --edge-ip-version auto allows IPv4+IPv6 edge connections, spreading load + # across more Cloudflare PoPs and reducing the reconnect window on edge + # maintenance events. + args: + - tunnel + - --protocol + - http2 + - --edge-ip-version + - auto + - --config + - /etc/cloudflared/config/config.yaml + - --metrics + - 0.0.0.0:2000 + - run + +# List of ClusterTunnels to create. +# Each entry must supply: name, domain. +# Optional per-tunnel overrides: replicas, protocol, strategy, args. +# +# Example: +# tunnels: +# - name: my-tunnel +# domain: example.com +# - name: another-tunnel +# domain: other.example.com +# replicas: 3 +tunnels: [] From 8f14ade9855ebb657deff2fa052dba8ec3b2086e Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Thu, 7 May 2026 19:00:39 +0100 Subject: [PATCH 02/15] feat: publish Helm charts to ghcr.io as OCI artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add publish-charts job to the CI workflow that packages both Helm charts and pushes them to oci://ghcr.io//charts using helm push. Uses github.repository_owner so the registry path is correct regardless of which fork the workflow runs in — no hardcoded paths. Triggers on pushes to main and feat/helm-charts (for branch testing), and on version tags alongside the existing chart-releaser job. --- .github/workflows/release.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31f3512b..db56b380 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - "main" + - "feat/helm-charts" create: tags: - "v*" @@ -13,6 +14,36 @@ on: workflow_dispatch: jobs: + publish-charts: + # Package and push Helm charts to ghcr.io as OCI artifacts. + # Runs on pushes to main and feat/helm-charts (for testing), and on version tags. + # Charts are published to oci://ghcr.io//charts, so the registry path + # automatically resolves to the correct owner regardless of which fork runs it. + runs-on: ubuntu-latest + if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag') + permissions: + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Helm + uses: azure/setup-helm@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Package and push charts + run: | + REGISTRY="oci://ghcr.io/${{ github.repository_owner }}/charts" + for chart in charts/*/; do + helm package "$chart" --destination /tmp/helm-packages + done + for pkg in /tmp/helm-packages/*.tgz; do + helm push "$pkg" "$REGISTRY" + done + release-charts: # Publish Helm charts to the gh-pages branch via chart-releaser on version tags. runs-on: ubuntu-latest From 0731751b5811d65ffbd2fe5502e768bfdeef5288 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Thu, 7 May 2026 19:33:10 +0100 Subject: [PATCH 03/15] feat: replace raw args with structured protocol/edgeIpVersion values Replace the freeform args list in cloudflare-tunnels/values.yaml with explicit protocol and edgeIpVersion fields. The template now constructs the cloudflared args internally from these values, ensuring correctness and preventing invalid combinations. Adds fail validations for both fields: - protocol: must be one of http2, h2mux, auto - edgeIpVersion: must be one of auto, 4, 6 - replicas: must be >= 1 Updates getting-started.md with a full Helm installation section covering both the cloudflare-operator and cloudflare-tunnels charts, including a values reference table. --- .../templates/clustertunnel.yaml | 36 +++++-- charts/cloudflare-tunnels/values.yaml | 32 +++---- docs/getting-started.md | 95 ++++++++++++++++++- 3 files changed, 136 insertions(+), 27 deletions(-) diff --git a/charts/cloudflare-tunnels/templates/clustertunnel.yaml b/charts/cloudflare-tunnels/templates/clustertunnel.yaml index deadabc8..5d67fc4c 100644 --- a/charts/cloudflare-tunnels/templates/clustertunnel.yaml +++ b/charts/cloudflare-tunnels/templates/clustertunnel.yaml @@ -1,9 +1,24 @@ {{- range .Values.tunnels }} {{- $tunnel := . }} -{{- $replicas := $tunnel.replicas | default $.Values.defaults.replicas }} -{{- $protocol := $tunnel.protocol | default $.Values.defaults.protocol }} -{{- $strategy := $tunnel.strategy | default $.Values.defaults.strategy }} -{{- $args := $tunnel.args | default $.Values.defaults.args }} +{{- $replicas := $tunnel.replicas | default $.Values.defaults.replicas }} +{{- $protocol := $tunnel.protocol | default $.Values.defaults.protocol }} +{{- $edgeIpVersion := $tunnel.edgeIpVersion | default $.Values.defaults.edgeIpVersion }} +{{- $strategy := $tunnel.strategy | default $.Values.defaults.strategy }} + +{{/* Validate protocol */}} +{{- if not (has $protocol (list "http2" "h2mux" "auto")) }} + {{- fail (printf "tunnel %q: invalid protocol %q — must be one of: http2, h2mux, auto" $tunnel.name $protocol) }} +{{- end }} + +{{/* Validate edgeIpVersion */}} +{{- if not (has $edgeIpVersion (list "auto" "4" "6")) }} + {{- fail (printf "tunnel %q: invalid edgeIpVersion %q — must be one of: auto, 4, 6" $tunnel.name $edgeIpVersion) }} +{{- end }} + +{{/* Validate replicas is a positive integer */}} +{{- if lt (int $replicas) 1 }} + {{- fail (printf "tunnel %q: replicas must be >= 1, got %d" $tunnel.name (int $replicas)) }} +{{- end }} --- apiVersion: networking.cfargotunnel.com/v1alpha2 kind: ClusterTunnel @@ -40,7 +55,14 @@ spec: containers: - name: cloudflared args: - {{- range $args }} - - {{ . | quote }} - {{- end }} + - "tunnel" + - "--protocol" + - {{ $protocol | quote }} + - "--edge-ip-version" + - {{ $edgeIpVersion | quote }} + - "--config" + - "/etc/cloudflared/config/config.yaml" + - "--metrics" + - "0.0.0.0:2000" + - "run" {{- end }} diff --git a/charts/cloudflare-tunnels/values.yaml b/charts/cloudflare-tunnels/values.yaml index 5413d176..1126714b 100644 --- a/charts/cloudflare-tunnels/values.yaml +++ b/charts/cloudflare-tunnels/values.yaml @@ -11,9 +11,19 @@ cloudflare: # Defaults applied to every tunnel unless overridden per-tunnel. defaults: - # cloudflared tunnel protocol. http2 is recommended over auto for stability. + # cloudflared tunnel protocol. + # Valid values: http2, h2mux, auto + # http2 is recommended: it uses a single multiplexed connection per edge, + # which reconnects faster than h2mux and is more predictable than auto. protocol: http2 + # Cloudflare edge IP version preference for outbound connections. + # Valid values: auto, 4, 6 + # auto allows cloudflared to connect over IPv4 or IPv6, spreading load + # across more Cloudflare PoPs and reducing the reconnect window during + # Cloudflare edge maintenance events. + edgeIpVersion: auto + # Number of cloudflared pod replicas per tunnel. # When using hard pod anti-affinity this must not exceed the number of nodes. replicas: 2 @@ -25,25 +35,9 @@ defaults: maxSurge: 0 maxUnavailable: 1 - # cloudflared args passed to every tunnel Deployment via deployPatch. - # --edge-ip-version auto allows IPv4+IPv6 edge connections, spreading load - # across more Cloudflare PoPs and reducing the reconnect window on edge - # maintenance events. - args: - - tunnel - - --protocol - - http2 - - --edge-ip-version - - auto - - --config - - /etc/cloudflared/config/config.yaml - - --metrics - - 0.0.0.0:2000 - - run - # List of ClusterTunnels to create. # Each entry must supply: name, domain. -# Optional per-tunnel overrides: replicas, protocol, strategy, args. +# Optional per-tunnel overrides: replicas, protocol, edgeIpVersion, strategy. # # Example: # tunnels: @@ -52,4 +46,6 @@ defaults: # - name: another-tunnel # domain: other.example.com # replicas: 3 +# protocol: http2 +# edgeIpVersion: auto tunnels: [] diff --git a/docs/getting-started.md b/docs/getting-started.md index f5784a18..541364c0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -7,12 +7,103 @@ To install this operator, you need the following: - `kubectl` -- `kustomize` (Optional) +- `kustomize` (Optional, required for the declarative kustomize method) +- `helm` (Optional, required for the Helm method) - A kubernetes cluster with a recent enough version to support Custom Resource Definitions. The operator was initially built on `v1.22.5+k3s1` and being developed on `v1.25.4+k3s1`. ## Installation methods -### Declarative installation (recommended) +### Helm (recommended) + +Helm charts are published to `oci://ghcr.io/adyanth/charts` and provide the +simplest way to install and upgrade the operator. + +#### 1. Install the operator + +```bash +helm install cloudflare-operator oci://ghcr.io/adyanth/charts/cloudflare-operator \ + --namespace cloudflare-operator-system \ + --create-namespace +``` + +Key values (all optional — defaults shown): + +| Value | Default | Description | +|---|---|---| +| `image.tag` | `0.13.1` | Operator image tag | +| `replicaCount` | `1` | Number of controller replicas | +| `namespace` | `cloudflare-operator-system` | Namespace to deploy into | +| `createNamespace` | `true` | Create the namespace if it does not exist | + +#### 2. Create the Cloudflare API token Secret + +Before creating tunnels, create a Kubernetes Secret with your Cloudflare API token +(see [operator-authentication](./examples/operator-authentication)): + +```bash +kubectl create secret generic cloudflare-secrets \ + --namespace cloudflare-operator-system \ + --from-literal=cloudflare.apiToken= +``` + +#### 3. Create tunnels with the cloudflare-tunnels chart + +The `cloudflare-tunnels` chart renders `ClusterTunnel` resources from a simple +values list, centralising shared connection settings across all your tunnels. + +```bash +helm install cloudflare-tunnels oci://ghcr.io/adyanth/charts/cloudflare-tunnels \ + --namespace cloudflare-operator-system \ + --set cloudflare.email=you@example.com \ + --set cloudflare.accountId= \ + --set cloudflare.secret=cloudflare-secrets \ + --set tunnels[0].name=my-tunnel \ + --set tunnels[0].domain=example.com +``` + +For multiple tunnels or persistent configuration, use a values file: + +```yaml +# tunnels-values.yaml +cloudflare: + email: you@example.com + accountId: + secret: cloudflare-secrets + +tunnels: + - name: my-site + domain: example.com + - name: another-site + domain: other.example.com + replicas: 3 # override default of 2 + edgeIpVersion: "4" # override default of auto +``` + +```bash +helm install cloudflare-tunnels oci://ghcr.io/adyanth/charts/cloudflare-tunnels \ + --namespace cloudflare-operator-system \ + -f tunnels-values.yaml +``` + +Key values for the `cloudflare-tunnels` chart: + +| Value | Default | Description | +|---|---|---| +| `cloudflare.email` | `""` | **Required.** Cloudflare account email | +| `cloudflare.accountId` | `""` | **Required.** Cloudflare account ID | +| `cloudflare.secret` | `cloudflare-secrets` | Name of the API token Secret | +| `defaults.protocol` | `http2` | cloudflared protocol (`http2`, `h2mux`, `auto`) | +| `defaults.edgeIpVersion` | `auto` | Edge IP version preference (`auto`, `4`, `6`) | +| `defaults.replicas` | `2` | Replicas per tunnel Deployment | +| `defaults.strategy.maxSurge` | `0` | Rolling update maxSurge | +| `defaults.strategy.maxUnavailable` | `1` | Rolling update maxUnavailable | + +All `defaults.*` values can be overridden per-tunnel by setting the same key +under the tunnel entry in the `tunnels` list. + +--- + +### Declarative installation with kustomize (GitOps) 1. Find the [latest tag for cloudflare-operator.](https://github.com/adyanth/cloudflare-operator/tags) 1. Create a kustomization.yaml in your repository that looks like From 6c85d3ed878fa234a0401f6ef1191bfad633c1f2 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Thu, 7 May 2026 21:21:04 +0100 Subject: [PATCH 04/15] chore: update all GHA action versions to latest and version charts from tag Update all actions to their current latest releases: - actions/checkout: v4 -> v6.0.2 - azure/setup-helm: v4 -> v5.0.0 - docker/login-action: v1/v3 -> v4.1.0 - docker/metadata-action: v3 -> v6.0.0 - docker/setup-qemu-action: v1 -> v4.0.0 - docker/setup-buildx-action: v1 -> v4.0.0 - docker/build-push-action: v2 -> v7.1.0 - peter-evans/dockerhub-description: v3 -> v5.0.0 - helm/chart-releaser-action: v1.6.0 -> v1.7.0 - actions/setup-go: already on v5 (current latest major) Add 'Derive chart version' step to publish-charts job: when triggered by a version tag (e.g. v0.14.0) the tag is used as both --version and --app-version in helm package, ensuring the OCI chart version matches the operator release. On branch pushes the Chart.yaml version is used as-is. --- .github/workflows/release.yml | 44 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db56b380..e7bc7a98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,26 +19,42 @@ jobs: # Runs on pushes to main and feat/helm-charts (for testing), and on version tags. # Charts are published to oci://ghcr.io//charts, so the registry path # automatically resolves to the correct owner regardless of which fork runs it. + # On a version tag (e.g. v0.14.0) the chart version is set to the tag (0.14.0), + # otherwise the chart version from Chart.yaml is used as-is. runs-on: ubuntu-latest if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag') permissions: packages: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6.0.2 - name: Set up Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@v5.0.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Derive chart version + id: chart_version + run: | + if [[ "${{ github.event_name }}" == "create" && "${{ github.event.ref_type }}" == "tag" ]]; then + # Strip leading 'v' from the tag (e.g. v0.14.0 -> 0.14.0) + echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + else + echo "version=" >> "$GITHUB_OUTPUT" + fi - name: Package and push charts run: | REGISTRY="oci://ghcr.io/${{ github.repository_owner }}/charts" + VERSION="${{ steps.chart_version.outputs.version }}" for chart in charts/*/; do - helm package "$chart" --destination /tmp/helm-packages + if [[ -n "$VERSION" ]]; then + helm package "$chart" --destination /tmp/helm-packages --version "$VERSION" --app-version "$VERSION" + else + helm package "$chart" --destination /tmp/helm-packages + fi done for pkg in /tmp/helm-packages/*.tgz; do helm push "$pkg" "$REGISTRY" @@ -52,7 +68,7 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Configure Git @@ -60,7 +76,7 @@ jobs: git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.6.0 + uses: helm/chart-releaser-action@v1.7.0 env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" @@ -68,10 +84,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6.0.2 - name: Docker meta id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v6.0.0 with: # list of Docker images to use as base name for tags images: | @@ -86,24 +102,24 @@ jobs: type=semver,pattern={{major}} type=sha - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v4.0.0 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v4.1.0 if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v4.1.0 if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v7.1.0 with: context: . platforms: ${{ github.event.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} @@ -112,7 +128,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} - name: Docker Hub Description if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' - uses: peter-evans/dockerhub-description@v3 + uses: peter-evans/dockerhub-description@v5.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} From 099ef3fbb97502c9d4f15f822a617ae3070262c2 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Thu, 7 May 2026 21:30:59 +0100 Subject: [PATCH 05/15] chore: pin GHA actions to major version only (not full semver) --- .github/workflows/release.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7bc7a98..b89222b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,11 +27,11 @@ jobs: packages: write steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 - name: Set up Helm - uses: azure/setup-helm@v5.0.0 + uses: azure/setup-helm@v5 - name: Login to GitHub Container Registry - uses: docker/login-action@v4.1.0 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -68,7 +68,7 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Configure Git @@ -84,10 +84,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 - name: Docker meta id: meta - uses: docker/metadata-action@v6.0.0 + uses: docker/metadata-action@v6 with: # list of Docker images to use as base name for tags images: | @@ -102,24 +102,24 @@ jobs: type=semver,pattern={{major}} type=sha - name: Set up QEMU - uses: docker/setup-qemu-action@v4.0.0 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4.0.0 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub - uses: docker/login-action@v4.1.0 + uses: docker/login-action@v4 if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v4.1.0 + uses: docker/login-action@v4 if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v7.1.0 + uses: docker/build-push-action@v7 with: context: . platforms: ${{ github.event.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} @@ -128,7 +128,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} - name: Docker Hub Description if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' - uses: peter-evans/dockerhub-description@v5.0.0 + uses: peter-evans/dockerhub-description@v5 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} From 074bd240734ac8f82133d77447e7afb5722fe340 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Thu, 7 May 2026 21:51:00 +0100 Subject: [PATCH 06/15] chore: add helm-sync-versions target to keep Chart.yaml appVersion in sync with Makefile VERSION --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 075c965c..e08f01e5 100644 --- a/Makefile +++ b/Makefile @@ -95,13 +95,17 @@ help: ## Display this help. ##@ Development .PHONY: manifests -manifests: controller-gen helm-sync-crds ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. +manifests: controller-gen helm-sync-crds helm-sync-versions ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: helm-sync-crds helm-sync-crds: ## Sync CRD YAMLs from config/crd/bases into charts/cloudflare-operator/crds/ cp config/crd/bases/*.yaml charts/cloudflare-operator/crds/ +.PHONY: helm-sync-versions +helm-sync-versions: ## Sync appVersion in charts/cloudflare-operator/Chart.yaml from VERSION + sed -i "s/^appVersion:.*/appVersion: \"$(VERSION)\"/" charts/cloudflare-operator/Chart.yaml + .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." From a18b275a1c9a1cc9acd778b149ae859e919dda2f Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Thu, 7 May 2026 23:27:55 +0100 Subject: [PATCH 07/15] fix: add webhook TLS cert and service to cloudflare-operator Helm chart --- .../templates/certificate.yaml | 28 +++++++++++++++++++ .../templates/deployment.yaml | 13 +++++++++ .../templates/webhook-service.yaml | 14 ++++++++++ charts/cloudflare-operator/values.yaml | 13 +++++++++ 4 files changed, 68 insertions(+) create mode 100644 charts/cloudflare-operator/templates/certificate.yaml create mode 100644 charts/cloudflare-operator/templates/webhook-service.yaml diff --git a/charts/cloudflare-operator/templates/certificate.yaml b/charts/cloudflare-operator/templates/certificate.yaml new file mode 100644 index 00000000..ea31f210 --- /dev/null +++ b/charts/cloudflare-operator/templates/certificate.yaml @@ -0,0 +1,28 @@ +{{- if .Values.webhook.certManager.enabled }} +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-selfsigned-issuer + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-serving-cert + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +spec: + dnsNames: + - {{ include "cloudflare-operator.fullname" . }}-webhook-service.{{ include "cloudflare-operator.namespace" . }}.svc + - {{ include "cloudflare-operator.fullname" . }}-webhook-service.{{ include "cloudflare-operator.namespace" . }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ include "cloudflare-operator.fullname" . }}-selfsigned-issuer + secretName: {{ .Values.webhook.secretName }} +{{- end }} diff --git a/charts/cloudflare-operator/templates/deployment.yaml b/charts/cloudflare-operator/templates/deployment.yaml index d962c44b..10a72969 100644 --- a/charts/cloudflare-operator/templates/deployment.yaml +++ b/charts/cloudflare-operator/templates/deployment.yaml @@ -60,6 +60,7 @@ spec: args: - --leader-elect - --health-probe-bind-address=:8081 + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true @@ -80,3 +81,15 @@ spec: periodSeconds: 10 resources: {{- toYaml .Values.resources | nindent 10 }} + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true + volumes: + - name: webhook-certs + secret: + secretName: {{ .Values.webhook.secretName }} diff --git a/charts/cloudflare-operator/templates/webhook-service.yaml b/charts/cloudflare-operator/templates/webhook-service.yaml new file mode 100644 index 00000000..ffe649a7 --- /dev/null +++ b/charts/cloudflare-operator/templates/webhook-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-webhook-service + namespace: {{ include "cloudflare-operator.namespace" . }} + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/charts/cloudflare-operator/values.yaml b/charts/cloudflare-operator/values.yaml index 77dc360a..655667bf 100644 --- a/charts/cloudflare-operator/values.yaml +++ b/charts/cloudflare-operator/values.yaml @@ -35,3 +35,16 @@ podAnnotations: {} # ServiceAccount name used by the controller. serviceAccountName: cloudflare-operator-controller-manager + +# Webhook TLS configuration. +# The operator always starts a conversion webhook server and requires TLS certs. +webhook: + # Name of the Secret (in the operator namespace) containing tls.crt and tls.key. + # Created automatically when certManager.enabled is true. + secretName: webhook-server-cert + + certManager: + # Set to true to create a self-signed cert-manager Issuer and Certificate. + # Requires cert-manager to be installed in the cluster. + # Set to false if you prefer to provision the Secret yourself. + enabled: true From c5d8dbd18af6ff69a853114a589ae40c478be84a Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Fri, 8 May 2026 01:24:26 +0100 Subject: [PATCH 08/15] fix: change default protocol to quic for faster edge reconnects --- .../cloudflare-tunnels/templates/clustertunnel.yaml | 4 ++-- charts/cloudflare-tunnels/values.yaml | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/charts/cloudflare-tunnels/templates/clustertunnel.yaml b/charts/cloudflare-tunnels/templates/clustertunnel.yaml index 5d67fc4c..4ea104ea 100644 --- a/charts/cloudflare-tunnels/templates/clustertunnel.yaml +++ b/charts/cloudflare-tunnels/templates/clustertunnel.yaml @@ -6,8 +6,8 @@ {{- $strategy := $tunnel.strategy | default $.Values.defaults.strategy }} {{/* Validate protocol */}} -{{- if not (has $protocol (list "http2" "h2mux" "auto")) }} - {{- fail (printf "tunnel %q: invalid protocol %q — must be one of: http2, h2mux, auto" $tunnel.name $protocol) }} +{{- if not (has $protocol (list "http2" "h2mux" "quic" "auto")) }} + {{- fail (printf "tunnel %q: invalid protocol %q — must be one of: http2, h2mux, quic, auto" $tunnel.name $protocol) }} {{- end }} {{/* Validate edgeIpVersion */}} diff --git a/charts/cloudflare-tunnels/values.yaml b/charts/cloudflare-tunnels/values.yaml index 1126714b..9d78f3d2 100644 --- a/charts/cloudflare-tunnels/values.yaml +++ b/charts/cloudflare-tunnels/values.yaml @@ -12,10 +12,11 @@ cloudflare: # Defaults applied to every tunnel unless overridden per-tunnel. defaults: # cloudflared tunnel protocol. - # Valid values: http2, h2mux, auto - # http2 is recommended: it uses a single multiplexed connection per edge, - # which reconnects faster than h2mux and is more predictable than auto. - protocol: http2 + # Valid values: http2, h2mux, quic, auto + # quic is recommended: QUIC (UDP) has sub-second 0-RTT reconnects, whereas + # http2 (TCP) can take 10-20s to reconnect if the initial SYN is dropped + # during Cloudflare edge rotation. + protocol: quic # Cloudflare edge IP version preference for outbound connections. # Valid values: auto, 4, 6 @@ -46,6 +47,6 @@ defaults: # - name: another-tunnel # domain: other.example.com # replicas: 3 -# protocol: http2 +# protocol: quic # edgeIpVersion: auto tunnels: [] From 5abde255b33c6bb41427fe3b5d0c81be2633604d Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Mon, 11 May 2026 23:01:06 +0100 Subject: [PATCH 09/15] docs: fix protocol default in getting-started table (quic, not http2) --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 541364c0..6fefca2a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -92,7 +92,7 @@ Key values for the `cloudflare-tunnels` chart: | `cloudflare.email` | `""` | **Required.** Cloudflare account email | | `cloudflare.accountId` | `""` | **Required.** Cloudflare account ID | | `cloudflare.secret` | `cloudflare-secrets` | Name of the API token Secret | -| `defaults.protocol` | `http2` | cloudflared protocol (`http2`, `h2mux`, `auto`) | +| `defaults.protocol` | `quic` | cloudflared protocol (`http2`, `h2mux`, `quic`, `auto`) | | `defaults.edgeIpVersion` | `auto` | Edge IP version preference (`auto`, `4`, `6`) | | `defaults.replicas` | `2` | Replicas per tunnel Deployment | | `defaults.strategy.maxSurge` | `0` | Rolling update maxSurge | From 4217dcc3dc410bcec1e4fe5c098071599c1913fc Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Tue, 12 May 2026 00:01:30 +0100 Subject: [PATCH 10/15] fix: bump controller-tools to v0.21.0 to fix make manifests under Go 1.24 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e08f01e5..bc4208c2 100644 --- a/Makefile +++ b/Makefile @@ -223,7 +223,7 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions KUSTOMIZE_VERSION ?= v5.4.3 -CONTROLLER_TOOLS_VERSION ?= v0.16.1 +CONTROLLER_TOOLS_VERSION ?= v0.21.0 ENVTEST_VERSION ?= release-0.19 GOLANGCI_LINT_VERSION ?= v2.1.5 From d55cc2decd63f4076ae3e1784a4a0272764ce137 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Tue, 12 May 2026 20:22:53 +0100 Subject: [PATCH 11/15] chore: remake manifests --- ...orking.cfargotunnel.com_accesstunnels.yaml | 2 +- ...rking.cfargotunnel.com_clustertunnels.yaml | 2 +- ...rking.cfargotunnel.com_tunnelbindings.yaml | 2 +- .../networking.cfargotunnel.com_tunnels.yaml | 2 +- ...orking.cfargotunnel.com_accesstunnels.yaml | 2 +- ...rking.cfargotunnel.com_clustertunnels.yaml | 2 +- ...rking.cfargotunnel.com_tunnelbindings.yaml | 2 +- .../networking.cfargotunnel.com_tunnels.yaml | 2 +- config/rbac/role.yaml | 26 +++++++++---------- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml index 58a6ebfb..188b5b79 100644 --- a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_accesstunnels.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: accesstunnels.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml index b44fc2af..6402680c 100644 --- a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_clustertunnels.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: clustertunnels.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml index f03e13ee..59270e4c 100644 --- a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnelbindings.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: tunnelbindings.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml index 26f6f241..796a4bef 100644 --- a/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml +++ b/charts/cloudflare-operator/crds/networking.cfargotunnel.com_tunnels.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: tunnels.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/config/crd/bases/networking.cfargotunnel.com_accesstunnels.yaml b/config/crd/bases/networking.cfargotunnel.com_accesstunnels.yaml index 58a6ebfb..188b5b79 100644 --- a/config/crd/bases/networking.cfargotunnel.com_accesstunnels.yaml +++ b/config/crd/bases/networking.cfargotunnel.com_accesstunnels.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: accesstunnels.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml b/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml index b44fc2af..6402680c 100644 --- a/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml +++ b/config/crd/bases/networking.cfargotunnel.com_clustertunnels.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: clustertunnels.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml b/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml index f03e13ee..59270e4c 100644 --- a/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml +++ b/config/crd/bases/networking.cfargotunnel.com_tunnelbindings.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: tunnelbindings.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml b/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml index 26f6f241..796a4bef 100644 --- a/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml +++ b/config/crd/bases/networking.cfargotunnel.com_tunnels.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.1 + controller-gen.kubebuilder.io/version: v0.21.0 name: tunnels.networking.cfargotunnel.com spec: group: networking.cfargotunnel.com diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1a810ca3..e904aff2 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -7,14 +7,8 @@ rules: - apiGroups: - "" resources: - - events - verbs: - - create - - patch -- apiGroups: - - apps - resources: - - deployments + - configmaps + - secrets verbs: - create - delete @@ -26,22 +20,28 @@ rules: - apiGroups: - "" resources: - - configmaps - - secrets + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - services verbs: - create - - delete - get - list - patch - update - watch - apiGroups: - - "" + - apps resources: - - services + - deployments verbs: - create + - delete - get - list - patch From 753237c47605ef8cbffbdce53c07e9f09a864c89 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Fri, 15 May 2026 00:06:36 +0100 Subject: [PATCH 12/15] feat: address PR comments --- .github/workflows/lint.yml | 29 ++++++++++++++++++++++++++--- .github/workflows/release.yml | 31 ++++++++++++++----------------- .github/workflows/test.yml | 10 +++++----- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6c6a549c..091c2920 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,13 +15,36 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go 1.24 - uses: actions/setup-go@v5 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: 1.24 - name: Lint - uses: golangci/golangci-lint-action@v7 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: only-new-issues: true version: v2.1.5 + check-manifests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Go 1.24 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: 1.24 + - name: Run make manifests + run: make manifests + - name: Check for uncommitted changes + run: | + if ! git diff --exit-code; then + echo "❌ Generated manifests are out of sync!" + echo "" + echo "Please run 'make manifests' locally and commit the changes:" + echo " make manifests" + echo " git add -A" + echo " git commit -m 'chore: update generated manifests'" + echo "" + exit 1 + fi + echo "✅ Generated manifests are up to date" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b89222b2..5a9cd1e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,10 +4,7 @@ on: push: branches: - "main" - - "feat/helm-charts" create: - tags: - - "v*" pull_request: branches: - "main" @@ -27,11 +24,11 @@ jobs: packages: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Helm - uses: azure/setup-helm@v5 + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 - name: Login to GitHub Container Registry - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -68,7 +65,7 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Configure Git @@ -76,7 +73,7 @@ jobs: git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.7.0 + uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" @@ -84,10 +81,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Docker meta id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: # list of Docker images to use as base name for tags images: | @@ -102,24 +99,24 @@ jobs: type=semver,pattern={{major}} type=sha - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - name: Login to DockerHub - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: ${{ github.event.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} @@ -128,7 +125,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} - name: Docker Hub Description if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' - uses: peter-evans/dockerhub-description@v5 + uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -136,7 +133,7 @@ jobs: readme-filepath: ./README.md short-description: "Cloudflare Operator Controller Manager" - name: Setup Go 1.24 - uses: actions/setup-go@v5 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 if: github.event.ref_type == 'tag' with: go-version: 1.24 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3841e301..813b8db8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,15 +11,15 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go 1.24 - uses: actions/setup-go@v5 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: 1.24 - name: Test run: make test - name: Archive code coverage results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: code-coverage path: cover.out @@ -33,8 +33,8 @@ jobs: actions: read pull-requests: write steps: - - uses: actions/checkout@v2 - - uses: fgrosse/go-coverage-report@v1.1.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: fgrosse/go-coverage-report@cbeb2ab2e32591d690337146ba02a911cc566f3f # v1.3.0 with: coverage-artifact-name: "code-coverage" coverage-file-name: "cover.out" From a8f142220a77cdea9e84ed394dccff3038240f0c Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Fri, 15 May 2026 00:20:47 +0100 Subject: [PATCH 13/15] feat: update to go 1.26 --- .github/workflows/lint.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- .golangci.yml | 2 +- Dockerfile | 2 +- go.mod | 4 +--- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 091c2920..2ff012d4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup Go 1.24 + - name: Setup Go 1.26 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: - go-version: 1.24 + go-version: 1.26 - name: Lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: @@ -29,10 +29,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup Go 1.24 + - name: Setup Go 1.26 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: - go-version: 1.24 + go-version: 1.26 - name: Run make manifests run: make manifests - name: Check for uncommitted changes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a9cd1e0..89967fda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,11 +132,11 @@ jobs: repository: adyanth/cloudflare-operator readme-filepath: ./README.md short-description: "Cloudflare Operator Controller Manager" - - name: Setup Go 1.24 + - name: Setup Go 1.26 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 if: github.event.ref_type == 'tag' with: - go-version: 1.24 + go-version: 1.26 - name: Build installer if: github.event.ref_type == 'tag' run: make build-installer diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 813b8db8..cd232bf9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,10 +12,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup Go 1.24 + - name: Setup Go 1.26 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: - go-version: 1.24 + go-version: 1.26 - name: Test run: make test - name: Archive code coverage results diff --git a/.golangci.yml b/.golangci.yml index 0c77a87f..af7556b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,7 @@ version: "2" run: - go: '1.24' + go: '1.26' timeout: 5m allow-parallel-runners: true diff --git a/Dockerfile b/Dockerfile index cb1b130f..86e798a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.24 AS builder +FROM golang:1.26 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/go.mod b/go.mod index 8b831b20..0222b25c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/adyanth/cloudflare-operator -go 1.24.0 - -toolchain go1.24.2 +go 1.26.0 require ( github.com/cloudflare/cloudflare-go v0.115.0 From 6b0703e982110761b927dfae824a4c0ff3f35171 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Fri, 15 May 2026 00:24:32 +0100 Subject: [PATCH 14/15] chore: bump golangci-lint --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2ff012d4..2c5c384e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,7 +24,7 @@ jobs: uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: only-new-issues: true - version: v2.1.5 + version: v2.12.2 check-manifests: runs-on: ubuntu-latest steps: From b8ca28a5a1fd5eb931c2fe80a1ba69d01fea6641 Mon Sep 17 00:00:00 2001 From: David Bibby <17053612+dsbibby@users.noreply.github.com> Date: Mon, 18 May 2026 20:53:37 +0100 Subject: [PATCH 15/15] feat: add helper ClusterRoles (editor/viewer/admin) for all CRDs, synced by helm-sync-rbac --- Makefile | 6 +- .../templates/rbac-helpers.yaml | 246 ++++++++++++++++++ .../cloudflare-operator/templates/rbac.yaml | 98 +++++-- charts/cloudflare-operator/values.yaml | 6 + hack/helm-sync-rbac.py | 109 ++++++++ 5 files changed, 443 insertions(+), 22 deletions(-) create mode 100644 charts/cloudflare-operator/templates/rbac-helpers.yaml create mode 100755 hack/helm-sync-rbac.py diff --git a/Makefile b/Makefile index bc4208c2..29f9e8ca 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ help: ## Display this help. ##@ Development .PHONY: manifests -manifests: controller-gen helm-sync-crds helm-sync-versions ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. +manifests: controller-gen helm-sync-crds helm-sync-versions helm-sync-rbac ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: helm-sync-crds @@ -106,6 +106,10 @@ helm-sync-crds: ## Sync CRD YAMLs from config/crd/bases into charts/cloudflare-o helm-sync-versions: ## Sync appVersion in charts/cloudflare-operator/Chart.yaml from VERSION sed -i "s/^appVersion:.*/appVersion: \"$(VERSION)\"/" charts/cloudflare-operator/Chart.yaml +.PHONY: helm-sync-rbac +helm-sync-rbac: ## Sync manager ClusterRole rules from config/rbac/role.yaml into charts/cloudflare-operator/templates/rbac.yaml + python3 hack/helm-sync-rbac.py + .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." diff --git a/charts/cloudflare-operator/templates/rbac-helpers.yaml b/charts/cloudflare-operator/templates/rbac-helpers.yaml new file mode 100644 index 00000000..73471260 --- /dev/null +++ b/charts/cloudflare-operator/templates/rbac-helpers.yaml @@ -0,0 +1,246 @@ +{{- if .Values.rbac.installHelperRoles }} +# Helper ClusterRoles for end-user access to cloudflare-operator CRDs. +# Not used by the operator itself. Auto-generated by hack/helm-sync-rbac.py. +--- +# permissions for end users to edit accesstunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-accesstunnel-editor-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - accesstunnels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - accesstunnels/status + verbs: + - get +--- +# permissions for end users to view accesstunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-accesstunnel-viewer-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - accesstunnels + verbs: + - get + - list + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - accesstunnels/status + verbs: + - get +--- +# Grants full permissions ('*') over clustertunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-clustertunnel-admin-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - clustertunnels + verbs: + - '*' + - apiGroups: + - networking.cfargotunnel.com + resources: + - clustertunnels/status + verbs: + - get +--- +# permissions for end users to edit clustertunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-clustertunnel-editor-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - clustertunnels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - clustertunnels/status + verbs: + - get +--- +# permissions for end users to view clustertunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-clustertunnel-viewer-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - clustertunnels + verbs: + - get + - list + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - clustertunnels/status + verbs: + - get +--- +# Grants full permissions ('*') over tunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-tunnel-admin-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnels + verbs: + - '*' + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnels/status + verbs: + - get +--- +# permissions for end users to edit tunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-tunnel-editor-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnels/status + verbs: + - get +--- +# permissions for end users to view tunnels. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-tunnel-viewer-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnels + verbs: + - get + - list + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnels/status + verbs: + - get +--- +# permissions for end users to edit tunnelbindings. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-tunnelbinding-editor-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnelbindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnelbindings/status + verbs: + - get +--- +# permissions for end users to view tunnelbindings. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "cloudflare-operator.fullname" . }}-tunnelbinding-viewer-role + labels: + {{- include "cloudflare-operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnelbindings + verbs: + - get + - list + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - tunnelbindings/status + verbs: + - get +{{- end }} diff --git a/charts/cloudflare-operator/templates/rbac.yaml b/charts/cloudflare-operator/templates/rbac.yaml index e9c07cbc..116a22aa 100644 --- a/charts/cloudflare-operator/templates/rbac.yaml +++ b/charts/cloudflare-operator/templates/rbac.yaml @@ -7,27 +7,83 @@ metadata: labels: {{- include "cloudflare-operator.labels" . | nindent 4 }} rules: -- apiGroups: [""] - resources: [events] - verbs: [create, patch] -- apiGroups: [apps] - resources: [deployments] - verbs: [create, delete, get, list, patch, update, watch] -- apiGroups: [""] - resources: [configmaps, secrets] - verbs: [create, delete, get, list, patch, update, watch] -- apiGroups: [""] - resources: [services] - verbs: [create, get, list, patch, update, watch] -- apiGroups: [networking.cfargotunnel.com] - resources: [accesstunnels, clustertunnels, tunnelbindings, tunnels] - verbs: [create, delete, get, list, patch, update, watch] -- apiGroups: [networking.cfargotunnel.com] - resources: [accesstunnels/status, clustertunnels/status, tunnelbindings/status, tunnels/status] - verbs: [get, patch, update] -- apiGroups: [networking.cfargotunnel.com] - resources: [clustertunnels/finalizers, tunnelbindings/finalizers, tunnels/finalizers] - verbs: [update] + - apiGroups: + - "" + resources: + - configmaps + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - "" + resources: + - services + verbs: + - create + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - accesstunnels + - clustertunnels + - tunnelbindings + - tunnels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.cfargotunnel.com + resources: + - accesstunnels/status + - clustertunnels/status + - tunnelbindings/status + - tunnels/status + verbs: + - get + - patch + - update + - apiGroups: + - networking.cfargotunnel.com + resources: + - clustertunnels/finalizers + - tunnelbindings/finalizers + - tunnels/finalizers + verbs: + - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/charts/cloudflare-operator/values.yaml b/charts/cloudflare-operator/values.yaml index 655667bf..e8f6f34f 100644 --- a/charts/cloudflare-operator/values.yaml +++ b/charts/cloudflare-operator/values.yaml @@ -48,3 +48,9 @@ webhook: # Requires cert-manager to be installed in the cluster. # Set to false if you prefer to provision the Secret yourself. enabled: true + +rbac: + # Install editor, viewer, and admin ClusterRoles for each CRD. + # These are not used by the operator itself but are useful helpers for + # granting end-users fine-grained access to cloudflare-operator resources. + installHelperRoles: true diff --git a/hack/helm-sync-rbac.py b/hack/helm-sync-rbac.py new file mode 100755 index 00000000..b36c313e --- /dev/null +++ b/hack/helm-sync-rbac.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Sync RBAC from config/rbac/ into the cloudflare-operator Helm chart. + +Two tasks: +1. Sync the manager ClusterRole rules from config/rbac/role.yaml into + charts/cloudflare-operator/templates/rbac.yaml (rules: block only). +2. Regenerate charts/cloudflare-operator/templates/rbac-helpers.yaml from + the editor/viewer/admin role files, adding Helm template directives. + +Requires: yq (https://github.com/mikefarah/yq) — pre-installed on +ubuntu-latest GitHub Actions runners and available via most package managers. +""" +import re +import subprocess +import sys +from pathlib import Path + +ROLE_SRC = "config/rbac/role.yaml" +RBAC_TMPL = "charts/cloudflare-operator/templates/rbac.yaml" +HELPERS_TMPL = "charts/cloudflare-operator/templates/rbac-helpers.yaml" + +# Helper roles to sync: (source file, name suffix, comment) +HELPER_ROLES = [ + ("config/rbac/accesstunnel_editor_role.yaml", "accesstunnel-editor-role", "permissions for end users to edit accesstunnels."), + ("config/rbac/accesstunnel_viewer_role.yaml", "accesstunnel-viewer-role", "permissions for end users to view accesstunnels."), + ("config/rbac/clustertunnel_admin_role.yaml", "clustertunnel-admin-role", "Grants full permissions ('*') over clustertunnels."), + ("config/rbac/clustertunnel_editor_role.yaml", "clustertunnel-editor-role", "permissions for end users to edit clustertunnels."), + ("config/rbac/clustertunnel_viewer_role.yaml", "clustertunnel-viewer-role", "permissions for end users to view clustertunnels."), + ("config/rbac/tunnel_admin_role.yaml", "tunnel-admin-role", "Grants full permissions ('*') over tunnels."), + ("config/rbac/tunnel_editor_role.yaml", "tunnel-editor-role", "permissions for end users to edit tunnels."), + ("config/rbac/tunnel_viewer_role.yaml", "tunnel-viewer-role", "permissions for end users to view tunnels."), + ("config/rbac/tunnelbinding_editor_role.yaml", "tunnelbinding-editor-role", "permissions for end users to edit tunnelbindings."), + ("config/rbac/tunnelbinding_viewer_role.yaml", "tunnelbinding-viewer-role", "permissions for end users to view tunnelbindings."), +] + +HELM_NAME = '{{ include "cloudflare-operator.fullname" . }}' +HELM_LABELS = ' {{- include "cloudflare-operator.labels" . | nindent 4 }}' + + +def yq(expr, path): + return subprocess.check_output(["yq", expr, path]).decode().rstrip("\n") + + +def rules_indented(src): + raw = yq(".rules", src) + if not raw or raw == "null": + print(f"ERROR: could not extract rules from {src}", file=sys.stderr) + sys.exit(1) + return "\n".join(" " + line if line else "" for line in raw.split("\n")) + + +# ── 1. Sync manager ClusterRole rules into rbac.yaml ───────────────────────── + +with open(RBAC_TMPL) as f: + content = f.read() + +new_content, n = re.subn( + r"(rules:\n)((?:(?!---).)*?)(---)", + "rules:\n" + rules_indented(ROLE_SRC) + "\n---", + content, + count=1, + flags=re.DOTALL, +) + +if n == 0: + print(f"ERROR: could not find rules block in {RBAC_TMPL}", file=sys.stderr) + sys.exit(1) + +with open(RBAC_TMPL, "w") as f: + f.write(new_content) + +print(f"helm-sync-rbac: synced manager ClusterRole rules from {ROLE_SRC}") + + +# ── 2. Regenerate rbac-helpers.yaml ────────────────────────────────────────── + +def build_helper_doc(src, name_suffix, comment): + rules_raw = yq(".rules", src) + if not rules_raw or rules_raw == "null": + print(f"ERROR: could not extract rules from {src}", file=sys.stderr) + sys.exit(1) + rules_block = "\n".join(" " + line if line else "" for line in rules_raw.split("\n")) + return f"""\ +--- +# {comment} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {HELM_NAME}-{name_suffix} + labels: +{HELM_LABELS} +rules: +{rules_block}""" + + +docs = "\n".join(build_helper_doc(src, suffix, comment) for src, suffix, comment in HELPER_ROLES) + +helpers_content = f"""\ +{{{{- if .Values.rbac.installHelperRoles }}}} +# Helper ClusterRoles for end-user access to cloudflare-operator CRDs. +# Not used by the operator itself. Auto-generated by hack/helm-sync-rbac.py. +{docs} +{{{{- end }}}} +""" + +with open(HELPERS_TMPL, "w") as f: + f.write(helpers_content) + +print(f"helm-sync-rbac: regenerated {HELPERS_TMPL} ({len(HELPER_ROLES)} helper roles)")