Skip to content

Commit 3aad992

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 3aad992

26 files changed

Lines changed: 1422 additions & 58 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/clusterrole-operator-manager-role.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ rules:
6161
- delete
6262
- get
6363
- list
64+
- patch
65+
- update
6466
- watch
6567
- apiGroups:
6668
- cert-manager.io

chart/templates/customresourcedefinition-decofiles.deco.sites.yaml

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

chart/templates/customresourcedefinition-decos.deco.sites.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,34 @@ spec:
179179
required:
180180
- source
181181
type: object
182+
fastDeploy:
183+
description: |-
184+
FastDeploy enables and configures git-driven, KV-first content fast-deploy
185+
for this site. When Enabled, content-only pushes (touching only
186+
.deco/blocks/**) sync to Cloudflare KV via a Decofile CR instead of a full
187+
build/deploy. This is the per-site config the webhook's DeploymentTarget
188+
reads to decide whether and how to fast-deploy.
189+
properties:
190+
enabled:
191+
description: |-
192+
Enabled gates the fast-deploy content path. When false, content pushes take
193+
the normal build/deploy path.
194+
type: boolean
195+
kvNamespaceId:
196+
description: |-
197+
KVNamespaceID is the Cloudflare KV namespace id for this site (one per site).
198+
Required when enabled (enforced via the XValidation rule on this struct).
199+
type: string
200+
siteOrigin:
201+
description: |-
202+
SiteOrigin is the deployed site origin used for cache purge after a sync
203+
(e.g. https://www.example.com). Optional.
204+
type: string
205+
type: object
206+
x-kubernetes-validations:
207+
- message: kvNamespaceId is required when fastDeploy is enabled
208+
rule: '!self.enabled || (has(self.kvNamespaceId) && size(self.kvNamespaceId)
209+
> 0)'
182210
framework:
183211
description: Framework is the site framework. deno | tanstack | next
184212
| remix | static

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

Lines changed: 13 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,30 @@ spec:
131131
value: {{ .tolerations | toJson | quote }}
132132
{{- end }}
133133
{{- end }}
134+
{{- if and .Values.fastDeploy .Values.fastDeploy.syncerImage }}
135+
- name: DECOFILE_SYNCER_IMAGE
136+
value: {{ .Values.fastDeploy.syncerImage | quote }}
137+
{{- end }}
134138
{{- if .Values.operatorApi.existingSecret }}
135139
{{- if .Values.operatorApi.addr }}
136140
- name: OPERATOR_API_ADDR
137141
value: {{ .Values.operatorApi.addr | quote }}
138142
{{- end }}
139143
{{- end }}
140144
{{- end }}
141-
{{- if .Values.operatorApi.existingSecret }}
145+
{{- if or .Values.secretEnv.existingSecret .Values.operatorApi.existingSecret }}
142146
envFrom:
147+
{{- if .Values.secretEnv.existingSecret }}
148+
- secretRef:
149+
name: {{ .Values.secretEnv.existingSecret | quote }}
150+
optional: true
151+
{{- end }}
152+
{{- if .Values.operatorApi.existingSecret }}
143153
- secretRef:
144154
name: {{ .Values.operatorApi.existingSecret | quote }}
145155
optional: true
146156
{{- end }}
157+
{{- end }}
147158
livenessProbe:
148159
httpGet:
149160
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.

0 commit comments

Comments
 (0)