@mizchi/k1c 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/canary/dispatcher-template.d.ts +17 -0
  4. package/dist/canary/dispatcher-template.js +42 -0
  5. package/dist/canary/effects-cloudflare.d.ts +9 -0
  6. package/dist/canary/effects-cloudflare.js +66 -0
  7. package/dist/canary/rollout-command.d.ts +15 -0
  8. package/dist/canary/rollout-command.js +92 -0
  9. package/dist/canary/runtime.d.ts +59 -0
  10. package/dist/canary/runtime.js +138 -0
  11. package/dist/canary/state-machine.d.ts +72 -0
  12. package/dist/canary/state-machine.js +161 -0
  13. package/dist/cli/args.d.ts +51 -0
  14. package/dist/cli/args.js +239 -0
  15. package/dist/cli/canary-integration.d.ts +11 -0
  16. package/dist/cli/canary-integration.js +101 -0
  17. package/dist/cli/format.d.ts +4 -0
  18. package/dist/cli/format.js +44 -0
  19. package/dist/cli/main.d.ts +3 -0
  20. package/dist/cli/main.js +158 -0
  21. package/dist/cli/run.d.ts +16 -0
  22. package/dist/cli/run.js +246 -0
  23. package/dist/manifest/lower.d.ts +22 -0
  24. package/dist/manifest/lower.js +913 -0
  25. package/dist/manifest/parse.d.ts +22 -0
  26. package/dist/manifest/parse.js +106 -0
  27. package/dist/manifest/schemas.d.ts +10359 -0
  28. package/dist/manifest/schemas.js +309 -0
  29. package/dist/manifest/types.d.ts +246 -0
  30. package/dist/manifest/types.js +12 -0
  31. package/dist/providers/configmap.d.ts +8 -0
  32. package/dist/providers/configmap.js +29 -0
  33. package/dist/providers/custom-domain.d.ts +11 -0
  34. package/dist/providers/custom-domain.js +120 -0
  35. package/dist/providers/d1-database.d.ts +9 -0
  36. package/dist/providers/d1-database.js +106 -0
  37. package/dist/providers/dispatch-namespace.d.ts +8 -0
  38. package/dist/providers/dispatch-namespace.js +100 -0
  39. package/dist/providers/dns-record.d.ts +14 -0
  40. package/dist/providers/dns-record.js +136 -0
  41. package/dist/providers/errors.d.ts +8 -0
  42. package/dist/providers/errors.js +64 -0
  43. package/dist/providers/hyperdrive.d.ts +27 -0
  44. package/dist/providers/hyperdrive.js +168 -0
  45. package/dist/providers/index.d.ts +6 -0
  46. package/dist/providers/index.js +36 -0
  47. package/dist/providers/kv-namespace.d.ts +8 -0
  48. package/dist/providers/kv-namespace.js +90 -0
  49. package/dist/providers/logpush-job.d.ts +17 -0
  50. package/dist/providers/logpush-job.js +181 -0
  51. package/dist/providers/queue.d.ts +10 -0
  52. package/dist/providers/queue.js +124 -0
  53. package/dist/providers/r2-bucket.d.ts +11 -0
  54. package/dist/providers/r2-bucket.js +98 -0
  55. package/dist/providers/registry.d.ts +9 -0
  56. package/dist/providers/registry.js +22 -0
  57. package/dist/providers/secret.d.ts +8 -0
  58. package/dist/providers/secret.js +30 -0
  59. package/dist/providers/types.d.ts +69 -0
  60. package/dist/providers/types.js +12 -0
  61. package/dist/providers/vectorize.d.ts +11 -0
  62. package/dist/providers/vectorize.js +110 -0
  63. package/dist/providers/worker.d.ts +106 -0
  64. package/dist/providers/worker.js +430 -0
  65. package/dist/providers/workflow.d.ts +10 -0
  66. package/dist/providers/workflow.js +103 -0
  67. package/dist/reconciler/apply.d.ts +10 -0
  68. package/dist/reconciler/apply.js +114 -0
  69. package/dist/reconciler/fake-provider.d.ts +48 -0
  70. package/dist/reconciler/fake-provider.js +83 -0
  71. package/dist/reconciler/plan.d.ts +6 -0
  72. package/dist/reconciler/plan.js +124 -0
  73. package/dist/reconciler/topo.d.ts +10 -0
  74. package/dist/reconciler/topo.js +53 -0
  75. package/dist/reconciler/types.d.ts +54 -0
  76. package/dist/reconciler/types.js +8 -0
  77. package/package.json +61 -0
@@ -0,0 +1,913 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { createHash } from 'node:crypto';
3
+ import { generateDispatcher } from "../canary/dispatcher-template.js";
4
+ export class LowerError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'LowerError';
8
+ }
9
+ }
10
+ const DEFAULT_COMPATIBILITY_DATE = '2025-01-01';
11
+ export async function lower(resources, options) {
12
+ const tables = {
13
+ configMaps: new Map(),
14
+ secrets: new Map(),
15
+ r2Buckets: new Map(),
16
+ kvNamespaces: new Map(),
17
+ hyperdrives: new Map(),
18
+ d1Databases: new Map(),
19
+ queues: new Map(),
20
+ vectorizes: new Map(),
21
+ serviceTargets: new Map(),
22
+ };
23
+ const deployments = [];
24
+ const rollouts = [];
25
+ const statefulSets = [];
26
+ const dispatchNamespaces = [];
27
+ const services = [];
28
+ const cronJobs = [];
29
+ const jobs = [];
30
+ const dnsRecords = [];
31
+ const logpushJobs = [];
32
+ for (const r of resources) {
33
+ const label = labelOf(r);
34
+ switch (r.kind) {
35
+ case 'Namespace':
36
+ break;
37
+ case 'ConfigMap':
38
+ tables.configMaps.set(label, r);
39
+ break;
40
+ case 'Secret':
41
+ tables.secrets.set(label, r);
42
+ break;
43
+ case 'R2Bucket':
44
+ tables.r2Buckets.set(label, r);
45
+ break;
46
+ case 'KVNamespace':
47
+ tables.kvNamespaces.set(label, r);
48
+ break;
49
+ case 'DispatchNamespace':
50
+ dispatchNamespaces.push(r);
51
+ break;
52
+ case 'Service':
53
+ services.push(r);
54
+ break;
55
+ case 'Deployment':
56
+ deployments.push(r);
57
+ break;
58
+ case 'Rollout':
59
+ rollouts.push(r);
60
+ break;
61
+ case 'StatefulSet':
62
+ statefulSets.push(r);
63
+ break;
64
+ case 'CronJob':
65
+ cronJobs.push(r);
66
+ break;
67
+ case 'Hyperdrive':
68
+ tables.hyperdrives.set(label, r);
69
+ break;
70
+ case 'D1Database':
71
+ tables.d1Databases.set(label, r);
72
+ break;
73
+ case 'Queue':
74
+ tables.queues.set(label, r);
75
+ break;
76
+ case 'Vectorize':
77
+ tables.vectorizes.set(label, r);
78
+ break;
79
+ case 'DNSRecord':
80
+ dnsRecords.push(r);
81
+ break;
82
+ case 'Job':
83
+ jobs.push(r);
84
+ break;
85
+ case 'LogpushJob':
86
+ logpushJobs.push(r);
87
+ break;
88
+ }
89
+ }
90
+ const desired = [];
91
+ const warnings = [];
92
+ // Pre-pass: build the Service → target Worker map so volume serviceRef resolution
93
+ // works regardless of resource declaration order in the manifest.
94
+ for (const s of services) {
95
+ const target = findServiceTarget(s, deployments, rollouts);
96
+ if (target !== null) {
97
+ const ns = s.metadata.namespace ?? 'default';
98
+ tables.serviceTargets.set(`${ns}/${s.metadata.name}`, target);
99
+ }
100
+ }
101
+ for (const b of tables.r2Buckets.values())
102
+ desired.push(lowerR2Bucket(b));
103
+ for (const kv of tables.kvNamespaces.values())
104
+ desired.push(lowerKVNamespace(kv));
105
+ for (const dn of dispatchNamespaces)
106
+ desired.push(lowerDispatchNamespace(dn));
107
+ for (const h of tables.hyperdrives.values())
108
+ desired.push(lowerHyperdrive(h, tables));
109
+ for (const d of tables.d1Databases.values())
110
+ desired.push(lowerD1Database(d));
111
+ for (const q of tables.queues.values()) {
112
+ desired.push(lowerQueue(q));
113
+ }
114
+ for (const v of tables.vectorizes.values())
115
+ desired.push(lowerVectorize(v));
116
+ for (const r of dnsRecords)
117
+ desired.push(lowerDNSRecord(r));
118
+ for (const j of logpushJobs)
119
+ desired.push(lowerLogpushJob(j));
120
+ for (const d of deployments) {
121
+ for (const w of await lowerDeployment(d, tables, options)) {
122
+ desired.push(w);
123
+ }
124
+ }
125
+ const emittedStateKvs = new Set();
126
+ for (const r of rollouts) {
127
+ for (const d of await lowerRollout(r, tables, warnings, emittedStateKvs, options)) {
128
+ desired.push(d);
129
+ }
130
+ }
131
+ for (const c of cronJobs) {
132
+ desired.push(await lowerCronJob(c, tables, options));
133
+ }
134
+ for (const s of statefulSets) {
135
+ desired.push(await lowerStatefulSet(s, tables, options));
136
+ }
137
+ for (const j of jobs) {
138
+ for (const d of await lowerJob(j, tables, options)) {
139
+ desired.push(d);
140
+ }
141
+ }
142
+ for (const s of services) {
143
+ const out = lowerService(s, deployments, rollouts, warnings);
144
+ if (out !== null)
145
+ desired.push(out);
146
+ }
147
+ return { desired, warnings };
148
+ }
149
+ function lowerVectorize(v) {
150
+ const ns = v.metadata.namespace ?? 'default';
151
+ const name = v.metadata.name;
152
+ return {
153
+ resourceType: 'Vectorize',
154
+ ref: refOf(v),
155
+ label: `${ns}/${name}`,
156
+ properties: {
157
+ indexName: `k1c-${ns}-${name}`,
158
+ dimensions: v.spec.dimensions,
159
+ metric: v.spec.metric,
160
+ ...(v.spec.description !== undefined ? { description: v.spec.description } : {}),
161
+ },
162
+ };
163
+ }
164
+ function lowerLogpushJob(l) {
165
+ const ns = l.metadata.namespace ?? 'default';
166
+ const name = l.metadata.name;
167
+ const scope = l.spec.zoneId !== undefined ? { zoneId: l.spec.zoneId } : { accountId: l.spec.accountId };
168
+ return {
169
+ resourceType: 'LogpushJob',
170
+ ref: refOf(l),
171
+ label: `${ns}/${name}`,
172
+ properties: {
173
+ jobName: `k1c-${ns}-${name}`,
174
+ scope,
175
+ dataset: l.spec.dataset,
176
+ destinationConf: l.spec.destinationConf,
177
+ ...(l.spec.enabled !== undefined ? { enabled: l.spec.enabled } : {}),
178
+ ...(l.spec.filter !== undefined ? { filter: l.spec.filter } : {}),
179
+ },
180
+ };
181
+ }
182
+ function lowerDNSRecord(r) {
183
+ const ns = r.metadata.namespace ?? 'default';
184
+ const name = r.metadata.name;
185
+ return {
186
+ resourceType: 'DNSRecord',
187
+ ref: refOf(r),
188
+ label: `${ns}/${name}`,
189
+ properties: {
190
+ zoneId: r.spec.zoneId,
191
+ type: r.spec.type,
192
+ name: r.spec.name,
193
+ content: r.spec.content,
194
+ ...(r.spec.ttl !== undefined ? { ttl: r.spec.ttl } : {}),
195
+ ...(r.spec.proxied !== undefined ? { proxied: r.spec.proxied } : {}),
196
+ ...(r.spec.priority !== undefined ? { priority: r.spec.priority } : {}),
197
+ },
198
+ };
199
+ }
200
+ async function lowerJob(j, tables, options) {
201
+ const ns = j.metadata.namespace ?? 'default';
202
+ const name = j.metadata.name;
203
+ const containers = j.spec.template.spec.containers;
204
+ if (containers.length !== 1) {
205
+ throw new LowerError(`Job ${ns}/${name}: jobTemplate must have exactly one container in v0.2 (got ${containers.length})`);
206
+ }
207
+ const annotations = j.metadata.annotations ?? {};
208
+ const className = annotations['cloudflare.com/workflow-class'] ??
209
+ `${name.charAt(0).toUpperCase()}${name.slice(1).replace(/-/g, '')}`;
210
+ if (!/^[A-Z][A-Za-z0-9_]*$/.test(className)) {
211
+ throw new LowerError(`Job ${ns}/${name}: derived Workflow class "${className}" is not a valid JS identifier; set \`cloudflare.com/workflow-class\` annotation explicitly`);
212
+ }
213
+ const ref = refOf(j);
214
+ const workers = await buildWorkerDesireds('Job', ref, j.metadata, j.spec.template, tables, options);
215
+ const worker = workers[0];
216
+ const scriptName = `k1c--${ns}--${name}`;
217
+ const workflowName = `k1c-${ns}-${name}`;
218
+ const workflowDesired = {
219
+ resourceType: 'Workflow',
220
+ ref: { ...ref, name: `${name}--workflow` },
221
+ label: `${ns}/${name}`,
222
+ properties: {
223
+ workflowName,
224
+ className,
225
+ scriptName,
226
+ },
227
+ dependsOn: [ref],
228
+ };
229
+ return [worker, workflowDesired];
230
+ }
231
+ async function lowerStatefulSet(s, tables, options) {
232
+ const ns = s.metadata.namespace ?? 'default';
233
+ const name = s.metadata.name;
234
+ const containers = s.spec.template.spec.containers;
235
+ if (containers.length !== 1) {
236
+ throw new LowerError(`StatefulSet ${ns}/${name}: only single-container Pods are supported in v0.2 (got ${containers.length})`);
237
+ }
238
+ const annotations = s.metadata.annotations ?? {};
239
+ const className = annotations['cloudflare.com/durable-object-class'] ??
240
+ `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
241
+ if (!/^[A-Z][A-Za-z0-9_]*$/.test(className)) {
242
+ throw new LowerError(`StatefulSet ${ns}/${name}: derived Durable Object class "${className}" is not a valid JS identifier; set \`cloudflare.com/durable-object-class\` annotation explicitly`);
243
+ }
244
+ const ref = refOf(s);
245
+ const workers = await buildWorkerDesireds('StatefulSet', ref, s.metadata, s.spec.template, tables, options);
246
+ const worker = workers[0];
247
+ return {
248
+ ...worker,
249
+ properties: { ...worker.properties, durableObjectClasses: [className] },
250
+ };
251
+ }
252
+ async function lowerCronJob(c, tables, options) {
253
+ const ns = c.metadata.namespace ?? 'default';
254
+ const name = c.metadata.name;
255
+ const containers = c.spec.jobTemplate.spec.template.spec.containers;
256
+ if (containers.length !== 1) {
257
+ throw new LowerError(`CronJob ${ns}/${name}: jobTemplate must have exactly one container in v0.2 (got ${containers.length})`);
258
+ }
259
+ const ref = refOf(c);
260
+ const workers = await buildWorkerDesireds('CronJob', ref, c.metadata, c.spec.jobTemplate.spec.template, tables, options);
261
+ const worker = workers[0];
262
+ // Suspend semantics: keep the script but clear all schedules.
263
+ const cronSchedules = c.spec.suspend === true ? [] : [c.spec.schedule];
264
+ return {
265
+ ...worker,
266
+ properties: { ...worker.properties, cronSchedules },
267
+ };
268
+ }
269
+ function lowerService(s, deployments, rollouts, warnings) {
270
+ const ns = s.metadata.namespace ?? 'default';
271
+ const name = s.metadata.name;
272
+ const ref = refOf(s);
273
+ const type = s.spec.type ?? 'ClusterIP';
274
+ if (type === 'ClusterIP') {
275
+ // ClusterIP services do not produce a Cloudflare resource. They are a name → Worker
276
+ // mapping consumed by `volumes[].serviceRef` in other Pods (handled by the pre-pass
277
+ // that populates tables.serviceTargets). If no workload matches, warn so the user
278
+ // knows the binding will fail.
279
+ if (findServiceTarget(s, deployments, rollouts) === null) {
280
+ warnings.push({
281
+ ref,
282
+ message: `Service ${ns}/${name}: type=ClusterIP has no matching Deployment / Rollout for selector ${JSON.stringify(s.spec.selector)} (no workers will be reachable via this service)`,
283
+ });
284
+ }
285
+ return null;
286
+ }
287
+ const annotations = s.metadata.annotations ?? {};
288
+ const zoneId = annotations['cloudflare.com/zone-id'];
289
+ const hostname = annotations['cloudflare.com/hostname'];
290
+ if (!zoneId || !hostname) {
291
+ throw new LowerError(`Service ${ns}/${name}: type=LoadBalancer requires both \`cloudflare.com/zone-id\` and \`cloudflare.com/hostname\` annotations`);
292
+ }
293
+ // Match the selector against Deployment / Rollout in the same namespace.
294
+ const target = findWorkloadBySelector(s.spec.selector, ns, deployments, rollouts);
295
+ if (target === null) {
296
+ throw new LowerError(`Service ${ns}/${name}: no Deployment or Rollout in namespace "${ns}" matches selector ${JSON.stringify(s.spec.selector)}`);
297
+ }
298
+ const targetScriptName = `k1c--${ns}--${target.name}`;
299
+ return {
300
+ resourceType: 'CustomDomain',
301
+ ref,
302
+ label: hostname,
303
+ properties: {
304
+ hostname,
305
+ service: targetScriptName,
306
+ zoneId,
307
+ environment: annotations['cloudflare.com/environment'] ?? 'production',
308
+ },
309
+ dependsOn: [
310
+ {
311
+ apiVersion: target.apiVersion,
312
+ kind: target.kind,
313
+ namespace: ns,
314
+ name: target.name,
315
+ },
316
+ ],
317
+ };
318
+ }
319
+ function findWorkloadBySelector(selector, namespace, deployments, rollouts) {
320
+ for (const d of deployments) {
321
+ if ((d.metadata.namespace ?? 'default') !== namespace)
322
+ continue;
323
+ if (isSubset(selector, d.spec.selector.matchLabels)) {
324
+ return { apiVersion: d.apiVersion, kind: 'Deployment', name: d.metadata.name };
325
+ }
326
+ }
327
+ for (const r of rollouts) {
328
+ if ((r.metadata.namespace ?? 'default') !== namespace)
329
+ continue;
330
+ if (isSubset(selector, r.spec.selector.matchLabels)) {
331
+ return { apiVersion: r.apiVersion, kind: 'Rollout', name: r.metadata.name };
332
+ }
333
+ }
334
+ return null;
335
+ }
336
+ /**
337
+ * Resolves a Service to the script name of its primary target Worker, or null when
338
+ * no Deployment / Rollout in the same namespace matches the Service selector.
339
+ */
340
+ function findServiceTarget(s, deployments, rollouts) {
341
+ const ns = s.metadata.namespace ?? 'default';
342
+ const match = findWorkloadBySelector(s.spec.selector, ns, deployments, rollouts);
343
+ if (match === null)
344
+ return null;
345
+ return `k1c--${ns}--${match.name}`;
346
+ }
347
+ function isSubset(small, large) {
348
+ for (const [k, v] of Object.entries(small)) {
349
+ if (large[k] !== v)
350
+ return false;
351
+ }
352
+ return true;
353
+ }
354
+ async function defaultReadFile(path) {
355
+ const fs = await import('node:fs/promises');
356
+ return fs.readFile(path);
357
+ }
358
+ async function hashEntrypoint(props, options) {
359
+ let bytes;
360
+ if (props.entrypointContent !== undefined) {
361
+ bytes = new TextEncoder().encode(props.entrypointContent);
362
+ }
363
+ else {
364
+ const reader = options?.readFile ?? defaultReadFile;
365
+ bytes = await reader(props.entrypoint);
366
+ }
367
+ return createHash('sha256').update(bytes).digest('hex');
368
+ }
369
+ function lowerDispatchNamespace(dn) {
370
+ const ns = dn.metadata.namespace ?? 'default';
371
+ const name = dn.metadata.name;
372
+ return {
373
+ resourceType: 'DispatchNamespace',
374
+ ref: refOf(dn),
375
+ label: `${ns}/${name}`,
376
+ properties: { namespaceName: `k1c-${ns}-${name}` },
377
+ };
378
+ }
379
+ function labelOf(r) {
380
+ return `${r.metadata.namespace ?? 'default'}/${r.metadata.name}`;
381
+ }
382
+ function refOf(r) {
383
+ return {
384
+ apiVersion: r.apiVersion,
385
+ kind: r.kind,
386
+ namespace: r.metadata.namespace ?? 'default',
387
+ name: r.metadata.name,
388
+ };
389
+ }
390
+ function lowerR2Bucket(b) {
391
+ const ns = b.metadata.namespace ?? 'default';
392
+ const name = b.metadata.name;
393
+ const properties = {
394
+ bucketName: `k1c-${ns}-${name}`,
395
+ ...(b.spec.location !== undefined ? { location: b.spec.location } : {}),
396
+ ...(b.spec.storageClass !== undefined ? { storageClass: b.spec.storageClass } : {}),
397
+ };
398
+ return {
399
+ resourceType: 'R2Bucket',
400
+ ref: refOf(b),
401
+ label: `${ns}/${name}`,
402
+ properties,
403
+ };
404
+ }
405
+ function lowerHyperdrive(h, tables) {
406
+ const ns = h.metadata.namespace ?? 'default';
407
+ const name = h.metadata.name;
408
+ const sRef = h.spec.origin.passwordSecretRef;
409
+ const sec = tables.secrets.get(`${ns}/${sRef.name}`);
410
+ if (!sec) {
411
+ throw new LowerError(`Hyperdrive ${ns}/${name}: Secret "${sRef.name}" referenced by passwordSecretRef not found in namespace "${ns}"`);
412
+ }
413
+ const password = secretValue(sec, sRef.key);
414
+ if (password === undefined) {
415
+ throw new LowerError(`Hyperdrive ${ns}/${name}: Secret "${sRef.name}" has no key "${sRef.key}"`);
416
+ }
417
+ const cfgName = `k1c-${ns}-${name}`;
418
+ return {
419
+ resourceType: 'Hyperdrive',
420
+ ref: refOf(h),
421
+ label: `${ns}/${name}`,
422
+ properties: {
423
+ name: cfgName,
424
+ origin: {
425
+ scheme: h.spec.origin.scheme,
426
+ host: h.spec.origin.host,
427
+ port: h.spec.origin.port,
428
+ database: h.spec.origin.database,
429
+ user: h.spec.origin.user,
430
+ password,
431
+ },
432
+ ...(h.spec.caching !== undefined ? { caching: h.spec.caching } : {}),
433
+ ...(h.spec.originConnectionLimit !== undefined
434
+ ? { originConnectionLimit: h.spec.originConnectionLimit }
435
+ : {}),
436
+ },
437
+ dependsOn: [refOf(sec)],
438
+ };
439
+ }
440
+ function lowerD1Database(d) {
441
+ const ns = d.metadata.namespace ?? 'default';
442
+ const name = d.metadata.name;
443
+ return {
444
+ resourceType: 'D1Database',
445
+ ref: refOf(d),
446
+ label: `${ns}/${name}`,
447
+ properties: {
448
+ databaseName: `k1c-${ns}-${name}`,
449
+ ...(d.spec?.primaryLocationHint !== undefined
450
+ ? { primaryLocationHint: d.spec.primaryLocationHint }
451
+ : {}),
452
+ },
453
+ };
454
+ }
455
+ function lowerQueue(q) {
456
+ const ns = q.metadata.namespace ?? 'default';
457
+ const name = q.metadata.name;
458
+ const consumer = q.spec?.consumer;
459
+ return {
460
+ resourceType: 'Queue',
461
+ ref: refOf(q),
462
+ label: `${ns}/${name}`,
463
+ properties: {
464
+ queueName: `k1c-${ns}-${name}`,
465
+ ...(consumer !== undefined
466
+ ? { consumerWorkerName: `k1c--${ns}--${consumer.workerName}` }
467
+ : {}),
468
+ },
469
+ ...(consumer !== undefined
470
+ ? {
471
+ dependsOn: [
472
+ {
473
+ apiVersion: 'apps/v1',
474
+ kind: 'Deployment',
475
+ namespace: ns,
476
+ name: consumer.workerName,
477
+ },
478
+ ],
479
+ }
480
+ : {}),
481
+ };
482
+ }
483
+ function lowerKVNamespace(kv) {
484
+ const ns = kv.metadata.namespace ?? 'default';
485
+ const name = kv.metadata.name;
486
+ return {
487
+ resourceType: 'KVNamespace',
488
+ ref: refOf(kv),
489
+ label: `${ns}/${name}`,
490
+ properties: { title: kv.spec.title ?? `k1c/${ns}/${name}` },
491
+ };
492
+ }
493
+ async function lowerDeployment(d, tables, options) {
494
+ return buildWorkerDesireds('Deployment', refOf(d), d.metadata, d.spec.template, tables, options);
495
+ }
496
+ async function lowerRollout(r, tables, warnings, emittedStateKvs, options) {
497
+ const ns = r.metadata.namespace ?? 'default';
498
+ const name = r.metadata.name;
499
+ const ref = refOf(r);
500
+ const annotations = r.metadata.annotations ?? {};
501
+ const dispatchAnno = annotations['cloudflare.com/dispatch-namespace'];
502
+ if (dispatchAnno !== undefined) {
503
+ return lowerCanaryRollout(r, dispatchAnno, tables, warnings, emittedStateKvs, options);
504
+ }
505
+ if ('canary' in r.spec.strategy) {
506
+ warnings.push({
507
+ ref,
508
+ message: `Rollout ${ns}/${name}: canary strategy is not yet implemented in v0.1; deploying new version at 100% (treating as immediate cutover)`,
509
+ });
510
+ }
511
+ else {
512
+ const bg = r.spec.strategy.blueGreen;
513
+ if (bg.autoPromotionEnabled === false) {
514
+ warnings.push({
515
+ ref,
516
+ message: `Rollout ${ns}/${name}: blueGreen with autoPromotionEnabled=false is not yet implemented in v0.1; deploying new version at 100%`,
517
+ });
518
+ }
519
+ }
520
+ return buildWorkerDesireds('Rollout', ref, r.metadata, r.spec.template, tables, options);
521
+ }
522
+ async function lowerCanaryRollout(r, dispatchAnno, tables, warnings, emittedStateKvs, options) {
523
+ const ns = r.metadata.namespace ?? 'default';
524
+ const name = r.metadata.name;
525
+ const ref = refOf(r);
526
+ const dispatchNsCFName = `k1c-${ns}-${dispatchAnno}`;
527
+ const stableScriptName = `k1c--${ns}--${name}--stable`;
528
+ const canaryScriptName = `k1c--${ns}--${name}--canary`;
529
+ const dispatcherScriptName = `k1c--${ns}--${name}`;
530
+ const stateKvK8sName = `rollout-state-${dispatchAnno}`;
531
+ const stateKvCFTitle = `k1c/rollout-state/${dispatchAnno}`;
532
+ const dispatchNsRef = {
533
+ apiVersion: 'cloudflare.k1c.io/v1alpha1',
534
+ kind: 'DispatchNamespace',
535
+ namespace: ns,
536
+ name: dispatchAnno,
537
+ };
538
+ const stateKvRef = {
539
+ apiVersion: 'cloudflare.k1c.io/v1alpha1',
540
+ kind: 'KVNamespace',
541
+ namespace: ns,
542
+ name: stateKvK8sName,
543
+ };
544
+ const results = [];
545
+ // Auto-emit the rollout-state KV (shared across all rollouts in the same dispatch namespace).
546
+ if (!emittedStateKvs.has(stateKvCFTitle)) {
547
+ emittedStateKvs.add(stateKvCFTitle);
548
+ results.push({
549
+ resourceType: 'KVNamespace',
550
+ ref: stateKvRef,
551
+ label: `${ns}/${stateKvK8sName}`,
552
+ properties: { title: stateKvCFTitle },
553
+ });
554
+ }
555
+ // Canary path is single-container in v0.1.6. Multi-container Rollout-with-dispatch
556
+ // would need per-container canary lifecycles, deferred to a future ADR.
557
+ if (r.spec.template.spec.containers.length !== 1) {
558
+ throw new LowerError(`Rollout ${ns}/${name}: canary Rollouts (with cloudflare.com/dispatch-namespace) currently support a single container only; got ${r.spec.template.spec.containers.length}`);
559
+ }
560
+ // Stable Worker = the user's code, deployed into the dispatch namespace under <name>--stable.
561
+ const userWorkers = await buildWorkerDesireds('Rollout', ref, r.metadata, r.spec.template, tables, options);
562
+ const userWorker = userWorkers[0];
563
+ const stableRef = { ...ref, name: `${name}--stable` };
564
+ const stableProperties = {
565
+ ...userWorker.properties,
566
+ scriptName: stableScriptName,
567
+ dispatchNamespace: dispatchNsCFName,
568
+ };
569
+ const stableDeps = [...(userWorker.dependsOn ?? []), dispatchNsRef];
570
+ results.push({
571
+ resourceType: 'Worker',
572
+ ref: stableRef,
573
+ label: `${ns}/${name}--stable`,
574
+ properties: stableProperties,
575
+ dependsOn: stableDeps,
576
+ });
577
+ // Dispatcher Worker = generated by k1c, deployed top-level. Routes via env.NAMESPACE.get(...)
578
+ // based on the weight stored in env.STATE under the rollout key.
579
+ const dispatcherSource = generateDispatcher({
580
+ rolloutKey: `rollout/${ns}/${name}`,
581
+ stableName: stableScriptName,
582
+ canaryName: canaryScriptName,
583
+ });
584
+ const dispatcherBaseProperties = {
585
+ scriptName: dispatcherScriptName,
586
+ entrypoint: '<k1c-generated:dispatcher>',
587
+ entrypointContent: dispatcherSource,
588
+ compatibilityDate: r.metadata.annotations?.['cloudflare.com/compatibility-date'] ??
589
+ DEFAULT_COMPATIBILITY_DATE,
590
+ bindings: [
591
+ { type: 'dispatch_namespace', name: 'NAMESPACE', dispatchNamespace: dispatchNsCFName },
592
+ { type: 'kv_namespace', name: 'STATE', namespaceId: `<resolved-at-apply:${stateKvK8sName}>` },
593
+ ],
594
+ };
595
+ const dispatcherProperties = {
596
+ ...dispatcherBaseProperties,
597
+ entrypointHash: await hashEntrypoint(dispatcherBaseProperties, options),
598
+ };
599
+ results.push({
600
+ resourceType: 'Worker',
601
+ ref,
602
+ label: `${ns}/${name}`,
603
+ properties: dispatcherProperties,
604
+ dependsOn: [dispatchNsRef, stateKvRef, stableRef],
605
+ });
606
+ // Note: the canary script itself is not emitted at lower time — its lifecycle (create
607
+ // / update / delete) is owned by the v0.1.2-ε state machine on apply, which compares
608
+ // current code against the stored "stable hash" to decide when to spawn a candidate.
609
+ if ('canary' in r.spec.strategy) {
610
+ warnings.push({
611
+ ref,
612
+ message: `Rollout ${ns}/${name}: canary state machine is not yet implemented (v0.1.2-ε); dispatcher routes 100% to stable until rollout-state KV is populated`,
613
+ });
614
+ }
615
+ else if (r.spec.strategy.blueGreen.autoPromotionEnabled === false) {
616
+ warnings.push({
617
+ ref,
618
+ message: `Rollout ${ns}/${name}: manual blueGreen promotion is not yet implemented; dispatcher routes 100% to stable`,
619
+ });
620
+ }
621
+ return results;
622
+ }
623
+ /**
624
+ * Lowers a Pod template into one Worker per container. The first container is the
625
+ * "primary" front-door and keeps the unsuffixed script name (`k1c--<ns>--<name>`); any
626
+ * additional container becomes a sidecar Worker named `k1c--<ns>--<name>--<container>`.
627
+ *
628
+ * When the Pod has multiple containers, every Worker gets `service` bindings to all of
629
+ * its siblings, addressable inside the Worker as `env.<container-name>.fetch(req)`.
630
+ * This preserves the k8s "containers in a Pod talk to each other" mental model on top
631
+ * of Cloudflare's flat Worker namespace.
632
+ */
633
+ async function buildWorkerDesireds(kind, ref, meta, template, tables, options) {
634
+ const ns = meta.namespace ?? 'default';
635
+ const name = meta.name;
636
+ const containers = template.spec.containers;
637
+ if (containers.length === 0) {
638
+ throw new LowerError(`${kind} ${ns}/${name}: at least one container is required`);
639
+ }
640
+ const baseScriptName = `k1c--${ns}--${name}`;
641
+ const scriptNames = containers.map((c, i) => i === 0 ? baseScriptName : `${baseScriptName}--${c.name}`);
642
+ const results = [];
643
+ for (let i = 0; i < containers.length; i += 1) {
644
+ const container = containers[i];
645
+ const scriptName = scriptNames[i];
646
+ const containerRef = i === 0 ? ref : { ...ref, name: `${name}--${container.name}` };
647
+ const containerLabel = i === 0 ? `${ns}/${name}` : `${ns}/${name}--${container.name}`;
648
+ const built = await buildContainerProperties(kind, ns, name, scriptName, container, template, meta.annotations ?? {}, tables);
649
+ // Auto-wire sibling service bindings for multi-container Pods.
650
+ let bindings = built.bindings;
651
+ if (containers.length > 1) {
652
+ const siblings = [];
653
+ for (let j = 0; j < containers.length; j += 1) {
654
+ if (j === i)
655
+ continue;
656
+ siblings.push({
657
+ type: 'service',
658
+ name: containers[j].name,
659
+ service: scriptNames[j],
660
+ });
661
+ }
662
+ bindings = [...bindings, ...siblings];
663
+ }
664
+ const baseProperties = {
665
+ scriptName,
666
+ entrypoint: container.image,
667
+ compatibilityDate: built.compatibilityDate,
668
+ ...(built.compatibilityFlags !== undefined
669
+ ? { compatibilityFlags: built.compatibilityFlags }
670
+ : {}),
671
+ ...(Object.keys(built.vars).length > 0 ? { vars: built.vars } : {}),
672
+ ...(Object.keys(built.secrets).length > 0 ? { secrets: built.secrets } : {}),
673
+ ...(bindings.length > 0 ? { bindings } : {}),
674
+ ...(built.observability !== undefined
675
+ ? { observability: built.observability }
676
+ : {}),
677
+ ...(built.placement !== undefined ? { placement: built.placement } : {}),
678
+ };
679
+ const properties = {
680
+ ...baseProperties,
681
+ entrypointHash: await hashEntrypoint(baseProperties, options),
682
+ };
683
+ results.push({
684
+ resourceType: 'Worker',
685
+ ref: containerRef,
686
+ label: containerLabel,
687
+ properties,
688
+ ...(built.dependsOn.length > 0 ? { dependsOn: built.dependsOn } : {}),
689
+ });
690
+ }
691
+ return results;
692
+ }
693
+ async function buildContainerProperties(kind, ns, name, scriptName, container, template, annotations, tables) {
694
+ const dependsOn = [];
695
+ const vars = {};
696
+ const secrets = {};
697
+ const ctxLabel = `${kind} ${ns}/${name}/${container.name}`;
698
+ for (const env of container.env ?? []) {
699
+ if (env.value !== undefined) {
700
+ vars[env.name] = env.value;
701
+ continue;
702
+ }
703
+ const valueFrom = env.valueFrom;
704
+ if (!valueFrom) {
705
+ throw new LowerError(`${ctxLabel}: env "${env.name}" has neither value nor valueFrom`);
706
+ }
707
+ if (valueFrom.configMapKeyRef) {
708
+ const cmRef = valueFrom.configMapKeyRef;
709
+ const cm = tables.configMaps.get(`${ns}/${cmRef.name}`);
710
+ if (!cm) {
711
+ throw new LowerError(`${ctxLabel}: ConfigMap "${cmRef.name}" not found in namespace "${ns}" (env ${env.name})`);
712
+ }
713
+ const value = cm.data?.[cmRef.key];
714
+ if (value === undefined) {
715
+ throw new LowerError(`${ctxLabel}: ConfigMap "${cmRef.name}" has no key "${cmRef.key}"`);
716
+ }
717
+ vars[env.name] = value;
718
+ pushUnique(dependsOn, refOf(cm));
719
+ }
720
+ else if (valueFrom.secretKeyRef) {
721
+ const sRef = valueFrom.secretKeyRef;
722
+ const sec = tables.secrets.get(`${ns}/${sRef.name}`);
723
+ if (!sec) {
724
+ throw new LowerError(`${ctxLabel}: Secret "${sRef.name}" not found in namespace "${ns}" (env ${env.name})`);
725
+ }
726
+ const value = secretValue(sec, sRef.key);
727
+ if (value === undefined) {
728
+ throw new LowerError(`${ctxLabel}: Secret "${sRef.name}" has no key "${sRef.key}"`);
729
+ }
730
+ secrets[env.name] = value;
731
+ pushUnique(dependsOn, refOf(sec));
732
+ }
733
+ else {
734
+ throw new LowerError(`${ctxLabel}: env "${env.name}" valueFrom must specify configMapKeyRef or secretKeyRef`);
735
+ }
736
+ }
737
+ const bindings = [];
738
+ const volumes = template.spec.volumes ?? [];
739
+ const volumesByName = new Map(volumes.map((v) => [v.name, v]));
740
+ for (const mount of container.volumeMounts ?? []) {
741
+ const vol = volumesByName.get(mount.name);
742
+ if (!vol) {
743
+ throw new LowerError(`${ctxLabel}: volumeMount "${mount.name}" has no matching volume`);
744
+ }
745
+ if (vol.r2BucketRef) {
746
+ const b = tables.r2Buckets.get(`${ns}/${vol.r2BucketRef.name}`);
747
+ if (!b) {
748
+ throw new LowerError(`${ctxLabel}: R2Bucket "${vol.r2BucketRef.name}" not found in namespace "${ns}"`);
749
+ }
750
+ bindings.push({
751
+ type: 'r2_bucket',
752
+ name: mount.mountPath,
753
+ bucketName: `k1c-${ns}-${b.metadata.name}`,
754
+ });
755
+ pushUnique(dependsOn, refOf(b));
756
+ }
757
+ else if (vol.kvNamespaceRef) {
758
+ const kv = tables.kvNamespaces.get(`${ns}/${vol.kvNamespaceRef.name}`);
759
+ if (!kv) {
760
+ throw new LowerError(`${ctxLabel}: KVNamespace "${vol.kvNamespaceRef.name}" not found in namespace "${ns}"`);
761
+ }
762
+ bindings.push({
763
+ type: 'kv_namespace',
764
+ name: mount.mountPath,
765
+ namespaceId: `<resolved-at-apply:${kv.metadata.name}>`,
766
+ });
767
+ pushUnique(dependsOn, refOf(kv));
768
+ }
769
+ else if (vol.serviceRef) {
770
+ const targetScriptName = tables.serviceTargets.get(`${ns}/${vol.serviceRef.name}`);
771
+ if (targetScriptName === undefined) {
772
+ throw new LowerError(`${ctxLabel}: Service "${vol.serviceRef.name}" not found (or has no matching workload) in namespace "${ns}"`);
773
+ }
774
+ bindings.push({
775
+ type: 'service',
776
+ name: mount.mountPath,
777
+ service: targetScriptName,
778
+ });
779
+ pushUnique(dependsOn, {
780
+ apiVersion: 'v1',
781
+ kind: 'Service',
782
+ namespace: ns,
783
+ name: vol.serviceRef.name,
784
+ });
785
+ }
786
+ else if (vol.hyperdriveRef) {
787
+ const h = tables.hyperdrives.get(`${ns}/${vol.hyperdriveRef.name}`);
788
+ if (!h) {
789
+ throw new LowerError(`${ctxLabel}: Hyperdrive "${vol.hyperdriveRef.name}" not found in namespace "${ns}"`);
790
+ }
791
+ bindings.push({
792
+ type: 'hyperdrive',
793
+ name: mount.mountPath,
794
+ hyperdriveId: `<resolved-at-apply:hyperdrive:${h.metadata.name}>`,
795
+ });
796
+ pushUnique(dependsOn, refOf(h));
797
+ }
798
+ else if (vol.d1DatabaseRef) {
799
+ const d = tables.d1Databases.get(`${ns}/${vol.d1DatabaseRef.name}`);
800
+ if (!d) {
801
+ throw new LowerError(`${ctxLabel}: D1Database "${vol.d1DatabaseRef.name}" not found in namespace "${ns}"`);
802
+ }
803
+ bindings.push({
804
+ type: 'd1',
805
+ name: mount.mountPath,
806
+ databaseId: `<resolved-at-apply:d1:${d.metadata.name}>`,
807
+ });
808
+ pushUnique(dependsOn, refOf(d));
809
+ }
810
+ else if (vol.queueRef) {
811
+ const q = tables.queues.get(`${ns}/${vol.queueRef.name}`);
812
+ if (!q) {
813
+ throw new LowerError(`${ctxLabel}: Queue "${vol.queueRef.name}" not found in namespace "${ns}"`);
814
+ }
815
+ bindings.push({
816
+ type: 'queue',
817
+ name: mount.mountPath,
818
+ queueName: `k1c-${ns}-${q.metadata.name}`,
819
+ });
820
+ pushUnique(dependsOn, refOf(q));
821
+ }
822
+ else if (vol.vectorizeRef) {
823
+ const v = tables.vectorizes.get(`${ns}/${vol.vectorizeRef.name}`);
824
+ if (!v) {
825
+ throw new LowerError(`${ctxLabel}: Vectorize "${vol.vectorizeRef.name}" not found in namespace "${ns}"`);
826
+ }
827
+ bindings.push({
828
+ type: 'vectorize',
829
+ name: mount.mountPath,
830
+ indexName: `k1c-${ns}-${v.metadata.name}`,
831
+ });
832
+ pushUnique(dependsOn, refOf(v));
833
+ }
834
+ else if (vol.analyticsEngineRef) {
835
+ bindings.push({
836
+ type: 'analytics_engine',
837
+ name: mount.mountPath,
838
+ dataset: vol.analyticsEngineRef.dataset,
839
+ });
840
+ }
841
+ else {
842
+ throw new LowerError(`${ctxLabel}: volume "${vol.name}" has no recognised reference (r2BucketRef, kvNamespaceRef, serviceRef, hyperdriveRef, d1DatabaseRef, queueRef, vectorizeRef, or analyticsEngineRef)`);
843
+ }
844
+ }
845
+ // Pod-level annotations for the no-config bindings: AI, Browser Rendering,
846
+ // Version Metadata. Value `enabled` uses the default JS binding name; any other
847
+ // string overrides it.
848
+ const aiAnno = annotations['cloudflare.com/ai'];
849
+ if (aiAnno !== undefined) {
850
+ bindings.push({ type: 'ai', name: aiAnno === 'enabled' ? 'AI' : aiAnno });
851
+ }
852
+ const browserAnno = annotations['cloudflare.com/browser-rendering'];
853
+ if (browserAnno !== undefined) {
854
+ bindings.push({
855
+ type: 'browser',
856
+ name: browserAnno === 'enabled' ? 'BROWSER' : browserAnno,
857
+ });
858
+ }
859
+ const vmAnno = annotations['cloudflare.com/version-metadata'];
860
+ if (vmAnno !== undefined) {
861
+ bindings.push({
862
+ type: 'version_metadata',
863
+ name: vmAnno === 'enabled' ? 'CF_VERSION' : vmAnno,
864
+ });
865
+ }
866
+ // Annotations are pod-level: every container in the Pod inherits compatibility-date,
867
+ // observability, smart-placement, etc. This matches kubectl semantics.
868
+ const flagsAnno = annotations['cloudflare.com/compatibility-flags'];
869
+ const compatibilityFlags = flagsAnno
870
+ ? flagsAnno
871
+ .split(',')
872
+ .map((s) => s.trim())
873
+ .filter((s) => s.length > 0)
874
+ : undefined;
875
+ // scriptName is parameter-only so the function compiles cleanly without referencing it
876
+ // outside the result; the caller wires it into WorkerProperties.
877
+ void scriptName;
878
+ return {
879
+ vars,
880
+ secrets,
881
+ bindings,
882
+ dependsOn,
883
+ compatibilityDate: annotations['cloudflare.com/compatibility-date'] ?? DEFAULT_COMPATIBILITY_DATE,
884
+ compatibilityFlags,
885
+ observability: annotations['cloudflare.com/observability'] === 'enabled'
886
+ ? { enabled: true }
887
+ : undefined,
888
+ placement: annotations['cloudflare.com/smart-placement'] === 'smart'
889
+ ? { mode: 'smart' }
890
+ : undefined,
891
+ };
892
+ }
893
+ function secretValue(sec, key) {
894
+ const fromString = sec.stringData?.[key];
895
+ if (fromString !== undefined)
896
+ return fromString;
897
+ const fromData = sec.data?.[key];
898
+ if (fromData !== undefined)
899
+ return Buffer.from(fromData, 'base64').toString('utf-8');
900
+ return undefined;
901
+ }
902
+ function pushUnique(list, ref) {
903
+ for (const existing of list) {
904
+ if (existing.apiVersion === ref.apiVersion &&
905
+ existing.kind === ref.kind &&
906
+ existing.namespace === ref.namespace &&
907
+ existing.name === ref.name) {
908
+ return;
909
+ }
910
+ }
911
+ list.push(ref);
912
+ }
913
+ //# sourceMappingURL=lower.js.map