diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6c6a549c..2c5c384e 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 - - name: Setup Go 1.24 - uses: actions/setup-go@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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@v7 + 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: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Go 1.26 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: 1.26 + - 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 be987501..89967fda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,22 +5,86 @@ on: branches: - "main" create: - tags: - - "v*" pull_request: branches: - "main" 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. + # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Set up Helm + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 + - name: Login to GitHub Container Registry + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + 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 + 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" + done + + 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + 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@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + docker: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Docker meta id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: # list of Docker images to use as base name for tags images: | @@ -35,24 +99,24 @@ jobs: type=semver,pattern={{major}} type=sha - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - name: Login to DockerHub - uses: docker/login-action@v1 + 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@v1 + 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@v2 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: ${{ github.event.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} @@ -61,18 +125,18 @@ 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@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: adyanth/cloudflare-operator readme-filepath: ./README.md short-description: "Cloudflare Operator Controller Manager" - - name: Setup Go 1.24 - uses: actions/setup-go@v5 + - 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 3841e301..cd232bf9 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 - - name: Setup Go 1.24 - uses: actions/setup-go@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - 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 - 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" 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/Makefile b/Makefile index d1fc0aa2..29f9e8ca 100644 --- a/Makefile +++ b/Makefile @@ -95,9 +95,21 @@ help: ## Display this help. ##@ Development .PHONY: manifests -manifests: controller-gen ## 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 +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: 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="./..." @@ -215,7 +227,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 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..188b5b79 --- /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.21.0 + 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..6402680c --- /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.21.0 + 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..59270e4c --- /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.21.0 + 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..796a4bef --- /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.21.0 + 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/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 new file mode 100644 index 00000000..10a72969 --- /dev/null +++ b/charts/cloudflare-operator/templates/deployment.yaml @@ -0,0 +1,95 @@ +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 + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + 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 }} + 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/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-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 new file mode 100644 index 00000000..116a22aa --- /dev/null +++ b/charts/cloudflare-operator/templates/rbac.yaml @@ -0,0 +1,177 @@ +--- +# 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: + - 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 +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/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 new file mode 100644 index 00000000..e8f6f34f --- /dev/null +++ b/charts/cloudflare-operator/values.yaml @@ -0,0 +1,56 @@ +# 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 + +# 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 + +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/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..4ea104ea --- /dev/null +++ b/charts/cloudflare-tunnels/templates/clustertunnel.yaml @@ -0,0 +1,68 @@ +{{- range .Values.tunnels }} +{{- $tunnel := . }} +{{- $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" "quic" "auto")) }} + {{- fail (printf "tunnel %q: invalid protocol %q — must be one of: http2, h2mux, quic, 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 +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: + - "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 new file mode 100644 index 00000000..9d78f3d2 --- /dev/null +++ b/charts/cloudflare-tunnels/values.yaml @@ -0,0 +1,52 @@ +# 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. + # 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 + # 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 + + # 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 + +# List of ClusterTunnels to create. +# Each entry must supply: name, domain. +# Optional per-tunnel overrides: replicas, protocol, edgeIpVersion, strategy. +# +# Example: +# tunnels: +# - name: my-tunnel +# domain: example.com +# - name: another-tunnel +# domain: other.example.com +# replicas: 3 +# protocol: quic +# edgeIpVersion: auto +tunnels: [] 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 diff --git a/docs/getting-started.md b/docs/getting-started.md index f5784a18..6fefca2a 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` | `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 | +| `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 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 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)")