Skip to content

Commit dbb9e7d

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 dbb9e7d

18 files changed

Lines changed: 1090 additions & 53 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) && self.kvNamespaceId != '')",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.

cmd/main.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ 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"
5354
"github.com/deco-sites/decofile-operator/internal/valkey"
5455
webhookv1 "github.com/deco-sites/decofile-operator/internal/webhook/v1"
5556
// +kubebuilder:scaffold:imports
@@ -336,10 +337,20 @@ func main() {
336337

337338
if enabled(controller.DecofileControllerName) {
338339
httpClient := controller.NewHTTPClient()
340+
// Fast-deploy: pluggable FastDeployment strategies for non-configmap
341+
// Decofile targets. tanstack-kv runs a self-cleaning Job that pushes the
342+
// decofile to Cloudflare KV (config from env; inert unless a Decofile
343+
// selects the target).
344+
fastDeployRegistry := deploy.NewDeploymentRegistry()
345+
fastDeployRegistry.Register(
346+
decositesv1alpha1.TargetTanstackKV,
347+
deploy.NewTanstackKV(deploy.TanstackKVConfigFromEnv()),
348+
)
339349
if err = (&controller.DecofileReconciler{
340350
Client: mgr.GetClient(),
341351
Scheme: mgr.GetScheme(),
342352
HTTPClient: httpClient,
353+
FastDeploy: fastDeployRegistry,
343354
}).SetupWithManager(mgr); err != nil {
344355
setupLog.Error(err, "unable to create controller", "controller", "Decofile")
345356
os.Exit(1)
@@ -404,13 +415,25 @@ func main() {
404415
if enabled(api.ControllerName) {
405416
apiUser := os.Getenv("OPERATOR_API_USER")
406417
apiPass := os.Getenv("OPERATOR_API_PASSWORD")
407-
if apiUser != "" && apiPass != "" {
418+
419+
// Git webhook → fast-deploy. The cloudflare-workers DeploymentTarget turns a
420+
// content-only push into a Decofile CR. Authenticated by HMAC signature
421+
// (GITHUB_WEBHOOK_SECRET), independent of the basic-auth redirects API.
422+
targetRegistry := deploy.NewTargetRegistry()
423+
targetRegistry.Register(deploy.ServingCloudflareWorker, deploy.NewCloudflareWorkersTarget())
424+
webhookHandlers := api.NewWebhookHandlers(
425+
mgr.GetClient(), os.Getenv("GITHUB_WEBHOOK_SECRET"), targetRegistry,
426+
)
427+
428+
// Start the HTTP server when EITHER the redirects API (basic-auth creds) or
429+
// the webhook (secret) is configured.
430+
if (apiUser != "" && apiPass != "") || webhookHandlers.Enabled() {
408431
h := api.NewHandlers(mgr.GetClient(), redirectNamespace)
409-
if err = mgr.Add(api.NewServer(operatorAPIAddr, apiUser, apiPass, h)); err != nil {
432+
if err = mgr.Add(api.NewServer(operatorAPIAddr, apiUser, apiPass, h, webhookHandlers)); err != nil {
410433
setupLog.Error(err, "unable to add operator API server")
411434
os.Exit(1)
412435
}
413-
setupLog.Info("Operator API enabled", "addr", operatorAPIAddr)
436+
setupLog.Info("Operator API enabled", "addr", operatorAPIAddr, "webhook", webhookHandlers.Enabled())
414437
}
415438
}
416439

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

config/crd/bases/deco.sites_decos.yaml

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

config/rbac/role.yaml

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

0 commit comments

Comments
 (0)