Skip to content

Commit 3cb7864

Browse files
committed
feat(gitops): bootstrap Cloudflare Tunnel ingress
- add STRRL Cloudflare Tunnel ingress controller manifests backed by External Secrets - seed Cloudflare account and tunnel config from Infisical or environment values - stage the controller in Flux before app reconciliation and document Ingress routing
1 parent 8688f6d commit 3cb7864

13 files changed

Lines changed: 198 additions & 43 deletions

File tree

gitops/apps/README.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
11
# Apps
22

3-
Public apps attach to the shared `Gateway` in `gateway-system` with `HTTPRoute`.
3+
Public apps should use the Cloudflare Tunnel ingress class.
44

55
Constraints:
66
- hostnames must be under your cluster base domain, for example `*.intar.app`
7-
- routes should bind to `stardrive-public`
8-
- HTTPS is terminated by the platform wildcard certificate
7+
- routes should set `ingressClassName: cloudflare-tunnel`
8+
- the controller reads `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, and `CLOUDFLARE_TUNNEL_NAME` from the shared Infisical operator path
9+
- the Cloudflare API token needs Zone read, DNS edit, and Cloudflare Tunnel edit permissions
910

1011
Example shape:
1112

1213
```yaml
13-
apiVersion: gateway.networking.k8s.io/v1
14-
kind: HTTPRoute
14+
apiVersion: networking.k8s.io/v1
15+
kind: Ingress
1516
metadata:
1617
name: example
1718
namespace: app
1819
spec:
19-
parentRefs:
20-
- name: stardrive-public
21-
namespace: gateway-system
22-
sectionName: https
23-
hostnames:
24-
- app.intar.app
20+
ingressClassName: cloudflare-tunnel
2521
rules:
26-
- backendRefs:
27-
- name: example
28-
port: 8080
22+
- host: app.intar.app
23+
http:
24+
paths:
25+
- path: /
26+
pathType: Prefix
27+
backend:
28+
service:
29+
name: example
30+
port:
31+
number: 8080
2932
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: external-secrets.io/v1
2+
kind: ExternalSecret
3+
metadata:
4+
name: cloudflare-tunnel-credentials
5+
namespace: cloudflare-tunnel
6+
spec:
7+
refreshInterval: 1h
8+
secretStoreRef:
9+
kind: ClusterSecretStore
10+
name: infisical
11+
target:
12+
name: cloudflare-tunnel-credentials
13+
data:
14+
- secretKey: api-token
15+
remoteRef:
16+
key: CLOUDFLARE_API_TOKEN
17+
- secretKey: cloudflare-account-id
18+
remoteRef:
19+
key: CLOUDFLARE_ACCOUNT_ID
20+
- secretKey: cloudflare-tunnel-name
21+
remoteRef:
22+
key: CLOUDFLARE_TUNNEL_NAME
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
apiVersion: helm.toolkit.fluxcd.io/v2
2+
kind: HelmRelease
3+
metadata:
4+
name: cloudflare-tunnel-ingress-controller
5+
namespace: cloudflare-tunnel
6+
spec:
7+
interval: 30m
8+
chart:
9+
spec:
10+
chart: cloudflare-tunnel-ingress-controller
11+
version: 0.0.23
12+
sourceRef:
13+
kind: HelmRepository
14+
name: cloudflare-tunnel-ingress-controller
15+
namespace: cloudflare-tunnel
16+
values:
17+
cloudflare:
18+
secretRef:
19+
name: cloudflare-tunnel-credentials
20+
accountIDKey: cloudflare-account-id
21+
tunnelNameKey: cloudflare-tunnel-name
22+
apiTokenKey: api-token
23+
ingressClass:
24+
name: cloudflare-tunnel
25+
isDefaultClass: false
26+
dnsCommentTemplate: "managed by stardrive"
27+
cloudflared:
28+
replicaCount: 2
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: source.toolkit.fluxcd.io/v1
2+
kind: HelmRepository
3+
metadata:
4+
name: cloudflare-tunnel-ingress-controller
5+
namespace: cloudflare-tunnel
6+
spec:
7+
interval: 1h
8+
url: https://helm.strrl.dev
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
namespace: cloudflare-tunnel
4+
resources:
5+
- namespace.yaml
6+
- helmrepository.yaml
7+
- external-secret.yaml
8+
- helmrelease.yaml
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
apiVersion: v1
2+
kind: Namespace
3+
metadata:
4+
name: cloudflare-tunnel

gitops/core/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
22
kind: Kustomization
33
resources:
44
- external-secrets
5+
- cloudflare-tunnel-ingress-controller
56
- cert-manager
67
- cert-manager-issuer
78
- cluster-secrets

internal/workflow/bootstrap.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,10 @@ func (a *App) Bootstrap(ctx context.Context, req BootstrapRequest) error {
142142
return nil, err
143143
}
144144
if err := infClient.SetSecrets(ctx, cfg.Infisical.ProjectID, cfg.Infisical.Environment, paths.OperatorShared, map[string]string{
145-
secretHetznerToken: infra.Hetzner.Token,
146-
secretCloudflareAPIToken: infra.CloudflareToken,
145+
secretHetznerToken: infra.Hetzner.Token,
146+
secretCloudflareAPIToken: infra.CloudflareToken,
147+
secretCloudflareAccountID: infra.CloudflareAccountID,
148+
secretCloudflareTunnelName: infra.CloudflareTunnelName,
147149
}); err != nil {
148150
return nil, err
149151
}
@@ -488,17 +490,20 @@ func (a *App) promptBootstrapInputs(ctx context.Context, cfg *config.Config, edi
488490
}
489491

490492
existing, _ := infClient.GetSecrets(ctx, cfg.Infisical.ProjectID, cfg.Infisical.Environment, cfg.Secrets().OperatorShared)
491-
infra := infraSecrets{
492-
Hetzner: hetzner.Credentials{
493-
Token: defaultSecret(existing[secretHetznerToken], os.Getenv(secretHetznerToken)),
494-
},
495-
CloudflareToken: defaultSecret(existing[secretCloudflareAPIToken], os.Getenv(secretCloudflareAPIToken)),
496-
}
493+
infra := infraSecretsFromValues(existing)
497494
infra.Hetzner.Token, err = promptSecretIfNeeded(ctx, edit, infra.Hetzner.Token, "HCLOUD_TOKEN", "Hetzner Cloud project API token")
498495
if err != nil {
499496
return nil, infraSecrets{}, err
500497
}
501-
infra.CloudflareToken, err = promptSecretIfNeeded(ctx, edit, infra.CloudflareToken, "Cloudflare API token", "Token with DNS edit permissions for the target zone")
498+
infra.CloudflareToken, err = promptSecretIfNeeded(ctx, edit, infra.CloudflareToken, "Cloudflare API token", "Token with Zone read, DNS edit, and Cloudflare Tunnel edit permissions")
499+
if err != nil {
500+
return nil, infraSecrets{}, err
501+
}
502+
infra.CloudflareAccountID, err = promptStringIfNeeded(ctx, edit, infra.CloudflareAccountID, "Cloudflare account ID", "Account that owns the tunnel and DNS zones")
503+
if err != nil {
504+
return nil, infraSecrets{}, err
505+
}
506+
infra.CloudflareTunnelName, err = promptStringIfNeeded(ctx, edit, infra.CloudflareTunnelName, "Cloudflare Tunnel name", "Tunnel to create or reuse for public app ingress")
502507
if err != nil {
503508
return nil, infraSecrets{}, err
504509
}

internal/workflow/bootstrap_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,42 @@ func TestGenerateStorageBoxPasswordMeetsPolicy(t *testing.T) {
3333
t.Fatalf("password does not meet complexity policy: %q", password)
3434
}
3535
}
36+
37+
func TestInfraSecretsFromValuesReadsCloudflareTunnelEnv(t *testing.T) {
38+
t.Setenv(secretHetznerToken, "hcloud-token")
39+
t.Setenv(secretCloudflareAPIToken, "cloudflare-token")
40+
t.Setenv(secretCloudflareAccountID, "cloudflare-account")
41+
t.Setenv(secretCloudflareTunnelName, "stardrive-tunnel")
42+
43+
secrets := infraSecretsFromValues(nil)
44+
45+
if secrets.Hetzner.Token != "hcloud-token" {
46+
t.Fatalf("expected Hetzner token from env, got %q", secrets.Hetzner.Token)
47+
}
48+
if secrets.CloudflareToken != "cloudflare-token" {
49+
t.Fatalf("expected Cloudflare token from env, got %q", secrets.CloudflareToken)
50+
}
51+
if secrets.CloudflareAccountID != "cloudflare-account" {
52+
t.Fatalf("expected Cloudflare account ID from env, got %q", secrets.CloudflareAccountID)
53+
}
54+
if secrets.CloudflareTunnelName != "stardrive-tunnel" {
55+
t.Fatalf("expected Cloudflare tunnel name from env, got %q", secrets.CloudflareTunnelName)
56+
}
57+
}
58+
59+
func TestInfraSecretsFromValuesPrefersStoredValues(t *testing.T) {
60+
t.Setenv(secretCloudflareAccountID, "env-account")
61+
t.Setenv(secretCloudflareTunnelName, "env-tunnel")
62+
63+
secrets := infraSecretsFromValues(map[string]string{
64+
secretCloudflareAccountID: "stored-account",
65+
secretCloudflareTunnelName: "stored-tunnel",
66+
})
67+
68+
if secrets.CloudflareAccountID != "stored-account" {
69+
t.Fatalf("expected stored Cloudflare account ID, got %q", secrets.CloudflareAccountID)
70+
}
71+
if secrets.CloudflareTunnelName != "stored-tunnel" {
72+
t.Fatalf("expected stored Cloudflare tunnel name, got %q", secrets.CloudflareTunnelName)
73+
}
74+
}

internal/workflow/cluster_runtime.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ func (a *App) waitForFluxBootstrapOCI(ctx context.Context, cfg *config.Config, k
718718

719719
func (a *App) waitForDeferredFluxBootstrapOCI(ctx context.Context, cfg *config.Config, kubeconfigPath string) error {
720720
env := a.kubectlEnv(kubeconfigPath)
721-
for _, name := range []string{fluxIssuerKustomizationName, fluxClusterSecretsKustomizationName, fluxPublicEdgeKustomizationName, fluxAppsKustomizationName} {
721+
for _, name := range []string{fluxIssuerKustomizationName, fluxClusterSecretsKustomizationName, fluxPublicEdgeKustomizationName, fluxCloudflareTunnelKustomizationName, fluxAppsKustomizationName} {
722722
if err := a.runCommand(ctx, env, nil, "kubectl", "wait", "--namespace", fluxNamespace, "--for=condition=Ready", "kustomization/"+name, "--timeout=15m"); err != nil {
723723
return err
724724
}
@@ -1269,6 +1269,22 @@ spec:
12691269
---
12701270
apiVersion: kustomize.toolkit.fluxcd.io/v1
12711271
kind: Kustomization
1272+
metadata:
1273+
name: %s
1274+
namespace: %s
1275+
spec:
1276+
interval: 5m0s
1277+
prune: true
1278+
wait: true
1279+
path: "./core/cloudflare-tunnel-ingress-controller"
1280+
dependsOn:
1281+
- name: %s
1282+
sourceRef:
1283+
kind: OCIRepository
1284+
name: %s
1285+
---
1286+
apiVersion: kustomize.toolkit.fluxcd.io/v1
1287+
kind: Kustomization
12721288
metadata:
12731289
name: %s
12741290
namespace: %s
@@ -1279,6 +1295,7 @@ spec:
12791295
path: "./apps"
12801296
dependsOn:
12811297
- name: %s
1298+
- name: %s
12821299
sourceRef:
12831300
kind: OCIRepository
12841301
name: %s
@@ -1309,9 +1326,14 @@ spec:
13091326
fluxNamespace,
13101327
fluxIssuerKustomizationName,
13111328
fluxOCIRepositoryName,
1329+
fluxCloudflareTunnelKustomizationName,
1330+
fluxNamespace,
1331+
fluxKustomizationName,
1332+
fluxOCIRepositoryName,
13121333
fluxAppsKustomizationName,
13131334
fluxNamespace,
13141335
fluxPublicEdgeKustomizationName,
1336+
fluxCloudflareTunnelKustomizationName,
13151337
fluxOCIRepositoryName,
13161338
))
13171339
}

0 commit comments

Comments
 (0)