Skip to content

Commit 11ed6ee

Browse files
hugo-ccabralclaude
andcommitted
feat(fastdeploy): git-webhook → Decofile CR → KV-sync Job
Operator-owned, CR-driven fast deploy for content (.deco/blocks). Two pluggable interfaces (internal/deploy): - DeploymentTarget: maps a git push to desired-state CRs. cloudflare-workers impl resolves the repo's Deco CR and emits a Decofile CR (target: tanstack-kv) on a content-only push to a fast-deploy-enabled site. - FastDeployment: drives a Decofile CR to its effect. tanstack-kv impl creates a self-cleaning batch/v1 Job (decofile-syncer image) that pushes the decofile to Cloudflare KV; the existing ConfigMap/Knative path stays the default. Adds POST /webhooks/github (HMAC-verified, outside basic-auth) to the operator API; Decofile CRD gains target/tanstackKV; Deco CRD gains fastDeploy; Decofile reconciler dispatches non-configmap targets and owns Jobs (batch RBAC). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent deb35fe commit 11ed6ee

22 files changed

Lines changed: 1346 additions & 56 deletions

api/v1alpha1/deco_types.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,33 @@ type DecoSpec struct {
3535
// Admin adds entries to Previews.Active on PR open and removes on PR close.
3636
// +optional
3737
Previews *DecoPreviewPolicy `json:"previews,omitempty"`
38+
39+
// FastDeploy enables and configures git-driven, KV-first content fast-deploy
40+
// for this site. When Enabled, content-only pushes (touching only
41+
// .deco/blocks/**) sync to Cloudflare KV via a Decofile CR instead of a full
42+
// build/deploy. This is the per-site config the webhook's DeploymentTarget
43+
// reads to decide whether and how to fast-deploy.
44+
// +optional
45+
FastDeploy *DecoFastDeploy `json:"fastDeploy,omitempty"`
46+
}
47+
48+
// DecoFastDeploy configures KV-first content fast-deploy for a site.
49+
// +kubebuilder:validation:XValidation:rule="!self.enabled || (has(self.kvNamespaceId) && size(self.kvNamespaceId) > 0)",message="kvNamespaceId is required when fastDeploy is enabled"
50+
type DecoFastDeploy struct {
51+
// Enabled gates the fast-deploy content path. When false, content pushes take
52+
// the normal build/deploy path.
53+
// +optional
54+
Enabled bool `json:"enabled,omitempty"`
55+
56+
// KVNamespaceID is the Cloudflare KV namespace id for this site (one per site).
57+
// Required when enabled (enforced via the XValidation rule on this struct).
58+
// +optional
59+
KVNamespaceID string `json:"kvNamespaceId,omitempty"`
60+
61+
// SiteOrigin is the deployed site origin used for cache purge after a sync
62+
// (e.g. https://www.example.com). Optional.
63+
// +optional
64+
SiteOrigin string `json:"siteOrigin,omitempty"`
3865
}
3966

4067
// DecoEnvVar is a plain environment variable injected into the build Job.

api/v1alpha1/decofile_types.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,24 @@ import (
2424
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
2525
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
2626

27+
// Decofile source kinds (DecofileSpec.Source).
28+
const (
29+
SourceInline = "inline"
30+
SourceGitHub = "github"
31+
)
32+
33+
// Decofile delivery targets (DecofileSpec.Target) — selects the FastDeployment
34+
// strategy that reconciles the CR.
35+
const (
36+
// TargetConfigMap writes a ConfigMap + notifies Knative pods (default).
37+
TargetConfigMap = "configmap"
38+
// TargetTanstackKV runs a Job that pushes the decofile to Cloudflare KV.
39+
TargetTanstackKV = "tanstack-kv"
40+
)
41+
2742
// DecofileSpec defines the desired state of Decofile.
43+
// +kubebuilder:validation:XValidation:rule="self.target != 'tanstack-kv' || has(self.tanstackKV)",message="spec.tanstackKV is required when target is tanstack-kv"
44+
// +kubebuilder:validation:XValidation:rule="self.target != 'tanstack-kv' || self.source == 'github'",message="source must be 'github' when target is tanstack-kv"
2845
type DecofileSpec struct {
2946
// Source specifies where to get the configuration data
3047
// +kubebuilder:validation:Required
@@ -43,6 +60,33 @@ type DecofileSpec struct {
4360
// Pods are queried using the app.deco/deploymentId label
4461
// +optional
4562
DeploymentId string `json:"deploymentId,omitempty"`
63+
64+
// Target selects how this Decofile is delivered (the FastDeployment strategy).
65+
// "configmap" (default) writes a ConfigMap and notifies Knative pods.
66+
// "tanstack-kv" runs a self-cleaning Job that pushes the decofile to Cloudflare
67+
// KV — the fast-deploy content path for TanStack/Workers sites.
68+
// +kubebuilder:validation:Enum=configmap;tanstack-kv
69+
// +kubebuilder:default=configmap
70+
// +optional
71+
Target string `json:"target,omitempty"`
72+
73+
// TanstackKV configures the tanstack-kv target. Required when target=tanstack-kv.
74+
// The repo/commit to sync come from spec.github (source=github).
75+
// +optional
76+
TanstackKV *TanstackKVTarget `json:"tanstackKV,omitempty"`
77+
}
78+
79+
// TanstackKVTarget configures Cloudflare KV fast-deploy for a TanStack/Workers site.
80+
type TanstackKVTarget struct {
81+
// KVNamespaceID is the Cloudflare KV namespace id for this site (one per site).
82+
// +kubebuilder:validation:Required
83+
// +kubebuilder:validation:MinLength=1
84+
KVNamespaceID string `json:"kvNamespaceId"`
85+
86+
// SiteOrigin is the deployed site origin used to POST /_cache/purge after the
87+
// sync (e.g. https://www.example.com). Optional — purge is skipped when empty.
88+
// +optional
89+
SiteOrigin string `json:"siteOrigin,omitempty"`
4690
}
4791

4892
// InlineSource contains direct JSON configuration data
@@ -98,6 +142,10 @@ type DecofileStatus struct {
98142
// GitHubCommit stores the commit SHA if using GitHub source
99143
// +optional
100144
GitHubCommit string `json:"githubCommit,omitempty"`
145+
146+
// JobName is the K8s Job name for the current tanstack-kv sync (target=tanstack-kv).
147+
// +optional
148+
JobName string `json:"jobName,omitempty"`
101149
}
102150

103151
// +kubebuilder:object:root=true

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chart/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: deco-operator
33
description: Kubernetes operator for Deco CMS that manages Decofile resources and automatically injects configuration into Knative Services
44
type: application
5-
version: 0.2.6
5+
version: 0.3.0
66
appVersion: "0.2.6"
77
keywords:
88
- operator

chart/templates/deployment-operator-controller-manager.yaml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ spec:
4747
command:
4848
- /manager
4949
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
50-
{{- if or (and .Values.github (or .Values.github.token .Values.github.existingSecret)) .Values.operatorApi.existingSecret (and .Values.valkey (get .Values.valkey "sentinelUrls")) .Values.cfworkers.existingSecret .Values.cfworkers.builderImage .Values.cfworkers.artifactsBucket .Values.s3.region .Values.s3.logsBucket .Values.s3.stateBucket .Values.build.serviceAccount .Values.build.roleArn .Values.build.nodeSelector .Values.build.tolerations }}
50+
{{- if or (and .Values.github (or .Values.github.token .Values.github.existingSecret)) .Values.operatorApi.existingSecret (and .Values.valkey (get .Values.valkey "sentinelUrls")) .Values.cfworkers.existingSecret .Values.cfworkers.builderImage .Values.cfworkers.artifactsBucket .Values.s3.region .Values.s3.logsBucket .Values.s3.stateBucket .Values.build.serviceAccount .Values.build.roleArn .Values.build.nodeSelector .Values.build.tolerations (and .Values.fastDeploy .Values.fastDeploy.syncerImage) }}
5151
env:
5252
{{- if and .Values.github .Values.github.existingSecret }}
5353
- name: GITHUB_TOKEN
@@ -131,19 +131,35 @@ spec:
131131
value: {{ .tolerations | toJson | quote }}
132132
{{- end }}
133133
{{- end }}
134+
{{- with .Values.fastDeploy }}
135+
{{- if .syncerImage }}
136+
- name: DECOFILE_SYNCER_IMAGE
137+
value: {{ .syncerImage | quote }}
138+
{{- end }}
139+
{{- end }}
134140
{{- if .Values.operatorApi.existingSecret }}
135141
{{- if .Values.operatorApi.addr }}
136142
- name: OPERATOR_API_ADDR
137143
value: {{ .Values.operatorApi.addr | quote }}
138144
{{- end }}
139145
{{- end }}
140146
{{- end }}
141-
{{- if .Values.operatorApi.existingSecret }}
147+
{{- if or .Values.secretEnv.existingSecret .Values.operatorApi.existingSecret }}
142148
envFrom:
149+
{{- if .Values.secretEnv.existingSecret }}
150+
# Unified operator secret: keys ARE env var names (CLOUDFLARE_*, GITHUB_*,
151+
# etc.), injected wholesale — add a new secret env by adding a key to the
152+
# backing ExternalSecret, no chart change needed.
153+
- secretRef:
154+
name: {{ .Values.secretEnv.existingSecret | quote }}
155+
optional: true
156+
{{- end }}
157+
{{- if .Values.operatorApi.existingSecret }}
143158
- secretRef:
144159
name: {{ .Values.operatorApi.existingSecret | quote }}
145160
optional: true
146161
{{- end }}
162+
{{- end }}
147163
livenessProbe:
148164
httpGet:
149165
path: /healthz

chart/values.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,24 @@ build:
123123
nodeSelector: {} # Default nodeSelector for build pods (overridable per-site via spec.build.nodeSelector)
124124
tolerations: [] # Default tolerations for build pods (overridable per-site via spec.build.tolerations)
125125

126+
# ── Unified secret env ───────────────────────────────────────────────────────
127+
# A single Secret whose KEYS ARE THE ENV VAR NAMES (e.g. CLOUDFLARE_API_WORKERS_TOKEN,
128+
# CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_KV_API_TOKEN, GITHUB_TOKEN, GITHUB_APP_ID,
129+
# GITHUB_APP_PRIVATE_KEY, GITHUB_WEBHOOK_SECRET, DECO_PURGE_TOKEN). Injected
130+
# wholesale via envFrom, so adding a new secret env is just a new key on the
131+
# backing ExternalSecret — no chart change. Preferred over the per-feature
132+
# existingSecret knobs above; those still work when set (and win, being explicit env).
133+
secretEnv:
134+
existingSecret: ""
135+
136+
# ── Fast deploy (KV-first content) ───────────────────────────────────────────
137+
# Git-webhook → Decofile CR → self-cleaning KV-sync Job. The webhook/KV/GitHub-App
138+
# secrets come from secretEnv above (GITHUB_WEBHOOK_SECRET enables the endpoint;
139+
# the operator-api controller must also be enabled). Only the non-secret syncer
140+
# image lives here.
141+
fastDeploy:
142+
syncerImage: "" # decofile-syncer image (repository:tag) → DECOFILE_SYNCER_IMAGE
143+
126144
# ── Operator Management API ──────────────────────────────────────────────────
127145
# General HTTP API for managing operator resources (DecoRedirects, etc.).
128146
# Enabled when username+password (or existingSecret) are set.

cmd/main.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import (
5050
"github.com/deco-sites/decofile-operator/internal/api"
5151
"github.com/deco-sites/decofile-operator/internal/build"
5252
"github.com/deco-sites/decofile-operator/internal/controller"
53+
"github.com/deco-sites/decofile-operator/internal/deploy"
54+
"github.com/deco-sites/decofile-operator/internal/githubapp"
5355
"github.com/deco-sites/decofile-operator/internal/valkey"
5456
webhookv1 "github.com/deco-sites/decofile-operator/internal/webhook/v1"
5557
// +kubebuilder:scaffold:imports
@@ -336,10 +338,30 @@ func main() {
336338

337339
if enabled(controller.DecofileControllerName) {
338340
httpClient := controller.NewHTTPClient()
341+
// Fast-deploy: pluggable FastDeployment strategies for non-configmap
342+
// Decofile targets. tanstack-kv runs a self-cleaning Job that pushes the
343+
// decofile to Cloudflare KV (config from env; inert unless a Decofile
344+
// selects the target).
345+
fastDeployRegistry := deploy.NewDeploymentRegistry()
346+
tkvCfg := deploy.TanstackKVConfigFromEnv()
347+
// Private-repo clones use a GitHub App installation token (short-lived,
348+
// repo-scoped) — same mechanism as admin. Optional: falls back to a static
349+
// GITHUB_TOKEN / public-repo clone when unset.
350+
if ghApp, gerr := githubapp.NewFromEnv(); gerr != nil {
351+
setupLog.Error(gerr, "GitHub App disabled: invalid GITHUB_APP_* config; private-repo fast-deploy will fail")
352+
} else if ghApp != nil {
353+
tkvCfg.GitHubApp = ghApp
354+
setupLog.Info("GitHub App configured for fast-deploy private-repo clones")
355+
}
356+
fastDeployRegistry.Register(
357+
decositesv1alpha1.TargetTanstackKV,
358+
deploy.NewTanstackKV(tkvCfg),
359+
)
339360
if err = (&controller.DecofileReconciler{
340361
Client: mgr.GetClient(),
341362
Scheme: mgr.GetScheme(),
342363
HTTPClient: httpClient,
364+
FastDeploy: fastDeployRegistry,
343365
}).SetupWithManager(mgr); err != nil {
344366
setupLog.Error(err, "unable to create controller", "controller", "Decofile")
345367
os.Exit(1)
@@ -404,13 +426,25 @@ func main() {
404426
if enabled(api.ControllerName) {
405427
apiUser := os.Getenv("OPERATOR_API_USER")
406428
apiPass := os.Getenv("OPERATOR_API_PASSWORD")
407-
if apiUser != "" && apiPass != "" {
429+
430+
// Git webhook → fast-deploy. The cloudflare-workers DeploymentTarget turns a
431+
// content-only push into a Decofile CR. Authenticated by HMAC signature
432+
// (GITHUB_WEBHOOK_SECRET), independent of the basic-auth redirects API.
433+
targetRegistry := deploy.NewTargetRegistry()
434+
targetRegistry.Register(deploy.ServingCloudflareWorker, deploy.NewCloudflareWorkersTarget())
435+
webhookHandlers := api.NewWebhookHandlers(
436+
mgr.GetClient(), os.Getenv("GITHUB_WEBHOOK_SECRET"), targetRegistry,
437+
)
438+
439+
// Start the HTTP server when EITHER the redirects API (basic-auth creds) or
440+
// the webhook (secret) is configured.
441+
if (apiUser != "" && apiPass != "") || webhookHandlers.Enabled() {
408442
h := api.NewHandlers(mgr.GetClient(), redirectNamespace)
409-
if err = mgr.Add(api.NewServer(operatorAPIAddr, apiUser, apiPass, h)); err != nil {
443+
if err = mgr.Add(api.NewServer(operatorAPIAddr, apiUser, apiPass, h, webhookHandlers)); err != nil {
410444
setupLog.Error(err, "unable to add operator API server")
411445
os.Exit(1)
412446
}
413-
setupLog.Info("Operator API enabled", "addr", operatorAPIAddr)
447+
setupLog.Info("Operator API enabled", "addr", operatorAPIAddr, "webhook", webhookHandlers.Enabled())
414448
}
415449
}
416450

config/crd/bases/deco.sites_decofiles.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,43 @@ spec:
9090
- inline
9191
- github
9292
type: string
93+
tanstackKV:
94+
description: |-
95+
TanstackKV configures the tanstack-kv target. Required when target=tanstack-kv.
96+
The repo/commit to sync come from spec.github (source=github).
97+
properties:
98+
kvNamespaceId:
99+
description: KVNamespaceID is the Cloudflare KV namespace id for
100+
this site (one per site).
101+
minLength: 1
102+
type: string
103+
siteOrigin:
104+
description: |-
105+
SiteOrigin is the deployed site origin used to POST /_cache/purge after the
106+
sync (e.g. https://www.example.com). Optional — purge is skipped when empty.
107+
type: string
108+
required:
109+
- kvNamespaceId
110+
type: object
111+
target:
112+
default: configmap
113+
description: |-
114+
Target selects how this Decofile is delivered (the FastDeployment strategy).
115+
"configmap" (default) writes a ConfigMap and notifies Knative pods.
116+
"tanstack-kv" runs a self-cleaning Job that pushes the decofile to Cloudflare
117+
KV — the fast-deploy content path for TanStack/Workers sites.
118+
enum:
119+
- configmap
120+
- tanstack-kv
121+
type: string
93122
required:
94123
- source
95124
type: object
125+
x-kubernetes-validations:
126+
- message: spec.tanstackKV is required when target is tanstack-kv
127+
rule: self.target != 'tanstack-kv' || has(self.tanstackKV)
128+
- message: source must be 'github' when target is tanstack-kv
129+
rule: self.target != 'tanstack-kv' || self.source == 'github'
96130
status:
97131
description: DecofileStatus defines the observed state of Decofile.
98132
properties:
@@ -161,6 +195,10 @@ spec:
161195
githubCommit:
162196
description: GitHubCommit stores the commit SHA if using GitHub source
163197
type: string
198+
jobName:
199+
description: JobName is the K8s Job name for the current tanstack-kv
200+
sync (target=tanstack-kv).
201+
type: string
164202
lastUpdated:
165203
description: LastUpdated is the timestamp of the last update
166204
format: date-time

0 commit comments

Comments
 (0)