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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 43 additions & 16 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
name: ci
name: release

on:
push:
branches:
- "main"
create:
tags:
- "v*"
workflow_dispatch:
pull_request:
branches:
- "main"
workflow_dispatch:

jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
env:
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/cloudflare-operator
DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME != '' && format('{0}/cloudflare-operator', secrets.DOCKERHUB_USERNAME) || '' }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -24,8 +29,8 @@ jobs:
with:
# list of Docker images to use as base name for tags
images: |
adyanth/cloudflare-operator
ghcr.io/adyanth/cloudflare-operator
${{ env.GHCR_IMAGE }}
${{ env.DOCKERHUB_IMAGE }}
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
Expand All @@ -40,13 +45,13 @@ jobs:
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
if: (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') && env.DOCKERHUB_IMAGE != ''
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand All @@ -55,29 +60,51 @@ jobs:
uses: docker/build-push-action@v2
with:
context: .
platforms: ${{ github.event.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
push: ${{ github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch' }}
platforms: ${{ startsWith(github.ref, 'refs/tags/') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
push: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Docker Hub Description
if: github.event.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
if: (startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch') && env.DOCKERHUB_IMAGE != ''
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: adyanth/cloudflare-operator
repository: ${{ env.DOCKERHUB_IMAGE }}
readme-filepath: ./README.md
short-description: "Cloudflare Operator Controller Manager"
- name: Setup Go 1.24
uses: actions/setup-go@v5
if: github.event.ref_type == 'tag'
if: startsWith(github.ref, 'refs/tags/')
with:
go-version: 1.24
- name: Resolve installer image base
if: startsWith(github.ref, 'refs/tags/')
run: |
if [[ -n "${DOCKERHUB_IMAGE}" ]]; then
echo "INSTALLER_IMAGE_TAG_BASE=${DOCKERHUB_IMAGE}" >> "${GITHUB_ENV}"
else
echo "INSTALLER_IMAGE_TAG_BASE=${GHCR_IMAGE}" >> "${GITHUB_ENV}"
fi
- name: Build installer
if: github.event.ref_type == 'tag'
run: make build-installer
- name: Publish installer
if: github.event.ref_type == 'tag'
if: startsWith(github.ref, 'refs/tags/')
run: make build-installer VERSION="${GITHUB_REF_NAME#v}" IMAGE_TAG_BASE="${INSTALLER_IMAGE_TAG_BASE}"
- name: Create GitHub release
if: startsWith(github.ref, 'refs/tags/')
run: |
release_args=(
"${GITHUB_REF_NAME}"
--title "${GITHUB_REF_NAME}"
--generate-notes
)
if [[ "${GITHUB_REF_NAME}" == *-* ]]; then
release_args+=(--prerelease)
fi
gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1 || gh release create "${release_args[@]}"
env:
GH_TOKEN: ${{ github.token }}
- name: Publish installer assets
if: startsWith(github.ref, 'refs/tags/')
run: gh release upload ${{ github.ref_name }} dist/*.yaml
env:
GH_TOKEN: ${{ github.token }}
6 changes: 6 additions & 0 deletions api/v1alpha1/tunnel_conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ var sampleFilledOldTunnel = v1alpha1.Tunnel{
AccountName: "cfAccount",
AccountId: "101",
Email: "cf@cf.com",
ApiKeyKey: "key-secret-key",
ApiTokenKey: "token-secret-key",
EmailKey: "email-secret-key",
CLOUDFLARE_API_KEY: "key",
CLOUDFLARE_API_TOKEN: "token",
CLOUDFLARE_TUNNEL_CREDENTIAL_FILE: "file",
Expand Down Expand Up @@ -86,6 +89,9 @@ var sampleFilledNewTunnel = v1alpha2.Tunnel{
AccountName: sampleFilledOldTunnel.Spec.Cloudflare.AccountName,
AccountId: sampleFilledOldTunnel.Spec.Cloudflare.AccountId,
Email: sampleFilledOldTunnel.Spec.Cloudflare.Email,
ApiKeyKey: sampleFilledOldTunnel.Spec.Cloudflare.ApiKeyKey,
ApiTokenKey: sampleFilledOldTunnel.Spec.Cloudflare.ApiTokenKey,
EmailKey: sampleFilledOldTunnel.Spec.Cloudflare.EmailKey,
CLOUDFLARE_API_KEY: sampleFilledOldTunnel.Spec.Cloudflare.CLOUDFLARE_API_KEY,
CLOUDFLARE_API_TOKEN: sampleFilledOldTunnel.Spec.Cloudflare.CLOUDFLARE_API_TOKEN,
CLOUDFLARE_TUNNEL_CREDENTIAL_FILE: sampleFilledOldTunnel.Spec.Cloudflare.CLOUDFLARE_TUNNEL_CREDENTIAL_FILE,
Expand Down
65 changes: 64 additions & 1 deletion api/v1alpha1/tunnel_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
DefaultCloudflareAPIKeySecretKey = "CLOUDFLARE_API_KEY"
DefaultCloudflareAPITokenSecretKey = "CLOUDFLARE_API_TOKEN"
DefaultCloudflareEmailSecretKey = "CLOUDFLARE_EMAIL"
DefaultCloudflareTunnelCredentialFileSecretKey = "CLOUDFLARE_TUNNEL_CREDENTIAL_FILE"
DefaultCloudflareTunnelCredentialSecretKey = "CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET"
)

// ExistingTunnel spec needs either a Tunnel Id or a Name to find it on Cloudflare.
type ExistingTunnel struct {
// +kubebuilder:validation:Optional
Expand Down Expand Up @@ -64,11 +72,26 @@ type CloudflareDetails struct {
// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_API_KEY
// Key in the secret to use for Cloudflare API Key, defaults to CLOUDFLARE_API_KEY. Needs Email also to be provided.
ApiKeyKey string `json:"apiKeyKey,omitempty"`

// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_API_TOKEN
// Key in the secret to use for Cloudflare API token, defaults to CLOUDFLARE_API_TOKEN
ApiTokenKey string `json:"apiTokenKey,omitempty"`

// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_EMAIL
// Key in the secret to use for Cloudflare email when Email is not set explicitly, defaults to CLOUDFLARE_EMAIL.
EmailKey string `json:"emailKey,omitempty"`

// +kubebuilder:validation:Optional
// Deprecated: use apiKeyKey instead.
// 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
CLOUDFLARE_API_KEY string `json:"CLOUDFLARE_API_KEY,omitempty"`

// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_API_TOKEN
// Deprecated: use apiTokenKey instead.
// Key in the secret to use for Cloudflare API token, defaults to CLOUDFLARE_API_TOKEN
CLOUDFLARE_API_TOKEN string `json:"CLOUDFLARE_API_TOKEN,omitempty"`

Expand All @@ -83,6 +106,46 @@ type CloudflareDetails struct {
CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET string `json:"CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET,omitempty"`
}

func (in CloudflareDetails) APIKeySecretKey() string {
return resolveSecretKey(in.ApiKeyKey, in.CLOUDFLARE_API_KEY, DefaultCloudflareAPIKeySecretKey)
}

func (in CloudflareDetails) APITokenSecretKey() string {
return resolveSecretKey(in.ApiTokenKey, in.CLOUDFLARE_API_TOKEN, DefaultCloudflareAPITokenSecretKey)
}

func (in CloudflareDetails) APIEmailSecretKey() string {
if in.EmailKey != "" {
return in.EmailKey
}

return DefaultCloudflareEmailSecretKey
}

func (in CloudflareDetails) TunnelCredentialFileSecretKey() string {
return resolveSecretKey("", in.CLOUDFLARE_TUNNEL_CREDENTIAL_FILE, DefaultCloudflareTunnelCredentialFileSecretKey)
}

func (in CloudflareDetails) TunnelCredentialSecretSecretKey() string {
return resolveSecretKey("", in.CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET, DefaultCloudflareTunnelCredentialSecretKey)
}

func resolveSecretKey(preferred string, legacy string, defaultValue string) string {
switch {
case preferred == "":
if legacy != "" {
return legacy
}
return defaultValue
case preferred != defaultValue:
return preferred
case legacy != "" && legacy != defaultValue:
return legacy
default:
return preferred
}
}

// TunnelSpec defines the desired state of Tunnel
type TunnelSpec struct {
// +kubebuilder:validation:Minimum=0
Expand Down
65 changes: 64 additions & 1 deletion api/v1alpha2/tunnel_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
DefaultCloudflareAPIKeySecretKey = "CLOUDFLARE_API_KEY"
DefaultCloudflareAPITokenSecretKey = "CLOUDFLARE_API_TOKEN"
DefaultCloudflareEmailSecretKey = "CLOUDFLARE_EMAIL"
DefaultCloudflareTunnelCredentialFileSecretKey = "CLOUDFLARE_TUNNEL_CREDENTIAL_FILE"
DefaultCloudflareTunnelCredentialSecretKey = "CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET"
)

// ExistingTunnel spec needs either a Tunnel Id or a Name to find it on Cloudflare.
type ExistingTunnel struct {
// +kubebuilder:validation:Optional
Expand Down Expand Up @@ -63,11 +71,26 @@ type CloudflareDetails struct {
// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_API_KEY
// Key in the secret to use for Cloudflare API Key, defaults to CLOUDFLARE_API_KEY. Needs Email also to be provided.
ApiKeyKey string `json:"apiKeyKey,omitempty"`

// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_API_TOKEN
// Key in the secret to use for Cloudflare API token, defaults to CLOUDFLARE_API_TOKEN
ApiTokenKey string `json:"apiTokenKey,omitempty"`

// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_EMAIL
// Key in the secret to use for Cloudflare email when Email is not set explicitly, defaults to CLOUDFLARE_EMAIL.
EmailKey string `json:"emailKey,omitempty"`

// +kubebuilder:validation:Optional
// Deprecated: use apiKeyKey instead.
// 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
CLOUDFLARE_API_KEY string `json:"CLOUDFLARE_API_KEY,omitempty"`

// +kubebuilder:validation:Optional
// +kubebuilder:default:=CLOUDFLARE_API_TOKEN
// Deprecated: use apiTokenKey instead.
// Key in the secret to use for Cloudflare API token, defaults to CLOUDFLARE_API_TOKEN
CLOUDFLARE_API_TOKEN string `json:"CLOUDFLARE_API_TOKEN,omitempty"`

Expand All @@ -82,6 +105,46 @@ type CloudflareDetails struct {
CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET string `json:"CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET,omitempty"`
}

func (in CloudflareDetails) APIKeySecretKey() string {
return resolveSecretKey(in.ApiKeyKey, in.CLOUDFLARE_API_KEY, DefaultCloudflareAPIKeySecretKey)
}

func (in CloudflareDetails) APITokenSecretKey() string {
return resolveSecretKey(in.ApiTokenKey, in.CLOUDFLARE_API_TOKEN, DefaultCloudflareAPITokenSecretKey)
}

func (in CloudflareDetails) APIEmailSecretKey() string {
if in.EmailKey != "" {
return in.EmailKey
}

return DefaultCloudflareEmailSecretKey
}

func (in CloudflareDetails) TunnelCredentialFileSecretKey() string {
return resolveSecretKey("", in.CLOUDFLARE_TUNNEL_CREDENTIAL_FILE, DefaultCloudflareTunnelCredentialFileSecretKey)
}

func (in CloudflareDetails) TunnelCredentialSecretSecretKey() string {
return resolveSecretKey("", in.CLOUDFLARE_TUNNEL_CREDENTIAL_SECRET, DefaultCloudflareTunnelCredentialSecretKey)
}

func resolveSecretKey(preferred string, legacy string, defaultValue string) string {
switch {
case preferred == "":
if legacy != "" {
return legacy
}
return defaultValue
case preferred != defaultValue:
return preferred
case legacy != "" && legacy != defaultValue:
return legacy
default:
return preferred
}
}

// TunnelSpec defines the desired state of Tunnel
type TunnelSpec struct {
// Deployment patch for the cloudflared deployment.
Expand Down
25 changes: 25 additions & 0 deletions api/v1alpha2/tunnel_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package v1alpha2

import "testing"

func TestAPITokenSecretKeyPrefersLegacyOverrideWhenCamelCaseFieldHasDefaultValue(t *testing.T) {
details := CloudflareDetails{
ApiTokenKey: DefaultCloudflareAPITokenSecretKey,
CLOUDFLARE_API_TOKEN: "legacy-token",
}

if got := details.APITokenSecretKey(); got != "legacy-token" {
t.Fatalf("expected legacy override to win when apiTokenKey is just the default, got %q", got)
}
}

func TestAPITokenSecretKeyPrefersExplicitCamelCaseOverride(t *testing.T) {
details := CloudflareDetails{
ApiTokenKey: "token",
CLOUDFLARE_API_TOKEN: "legacy-token",
}

if got := details.APITokenSecretKey(); got != "token" {
t.Fatalf("expected explicit camelCase override, got %q", got)
}
}
23 changes: 19 additions & 4 deletions bundle/manifests/networking.cfargotunnel.com_clustertunnels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ spec:
description: Cloudflare Credentials
properties:
CLOUDFLARE_API_KEY:
default: CLOUDFLARE_API_KEY
description: |-
Deprecated: use apiKeyKey instead.
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
description: |-
Deprecated: use apiTokenKey instead.
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
Expand All @@ -77,13 +77,28 @@ spec:
cannot be both empty. If both are provided, Account ID is used
if valid, else falls back to Account Name.
type: string
apiKeyKey:
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.
type: string
apiTokenKey:
default: CLOUDFLARE_API_TOKEN
description: Key in the secret to use for Cloudflare API token,
defaults to CLOUDFLARE_API_TOKEN
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
emailKey:
default: CLOUDFLARE_EMAIL
description: Key in the secret to use for Cloudflare email when
Email is not set explicitly, defaults to CLOUDFLARE_EMAIL.
type: string
secret:
description: Secret containing Cloudflare API key/token
type: string
Expand Down
Loading
Loading