@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,48 @@
1
+ import type { ZodSchema } from 'zod';
2
+ import type { CloudflareResourceProvider, CreateResult, DeleteResult, ListedResource, ProviderContext, ProviderError, StatusResult, UpdateResult } from '../providers/types.ts';
3
+ import { NotFound } from '../providers/types.ts';
4
+ export type FakeEvent = {
5
+ readonly op: 'create';
6
+ readonly nativeId: string;
7
+ readonly label: string;
8
+ readonly properties: unknown;
9
+ } | {
10
+ readonly op: 'update';
11
+ readonly nativeId: string;
12
+ readonly properties: unknown;
13
+ } | {
14
+ readonly op: 'delete';
15
+ readonly nativeId: string;
16
+ } | {
17
+ readonly op: 'read';
18
+ readonly nativeId: string;
19
+ } | {
20
+ readonly op: 'list';
21
+ };
22
+ export interface FakeFailure {
23
+ readonly op: 'create' | 'update' | 'delete' | 'read';
24
+ remaining: number;
25
+ readonly error: ProviderError;
26
+ }
27
+ export declare class FakeProvider<P> implements CloudflareResourceProvider<P> {
28
+ #private;
29
+ readonly resourceType: string;
30
+ readonly schema: ZodSchema<P>;
31
+ readonly state: Map<string, {
32
+ label: string;
33
+ properties: P;
34
+ }>;
35
+ readonly events: FakeEvent[];
36
+ readonly failures: FakeFailure[];
37
+ constructor(resourceType: string, schema: ZodSchema<P>);
38
+ seed(nativeId: string, label: string, properties: P): void;
39
+ injectFailure(failure: FakeFailure): void;
40
+ list(_ctx: ProviderContext): AsyncIterable<ListedResource>;
41
+ read(_ctx: ProviderContext, nativeId: string): Promise<P | NotFound>;
42
+ create(_ctx: ProviderContext, label: string, desired: P): Promise<CreateResult>;
43
+ update(_ctx: ProviderContext, nativeId: string, _prior: P, desired: P): Promise<UpdateResult>;
44
+ delete(_ctx: ProviderContext, nativeId: string): Promise<DeleteResult>;
45
+ status(_ctx: ProviderContext, _nativeId: string, _opId: string): Promise<StatusResult>;
46
+ }
47
+ export declare function makeFakeContext(): ProviderContext;
48
+ //# sourceMappingURL=fake-provider.d.ts.map
@@ -0,0 +1,83 @@
1
+ import { NotFound } from "../providers/types.js";
2
+ export class FakeProvider {
3
+ resourceType;
4
+ schema;
5
+ state = new Map();
6
+ events = [];
7
+ failures = [];
8
+ #idCounter = 0;
9
+ constructor(resourceType, schema) {
10
+ this.resourceType = resourceType;
11
+ this.schema = schema;
12
+ }
13
+ seed(nativeId, label, properties) {
14
+ this.state.set(nativeId, { label, properties });
15
+ }
16
+ injectFailure(failure) {
17
+ this.failures.push(failure);
18
+ }
19
+ #consumeFailure(op) {
20
+ const f = this.failures.find((x) => x.op === op && x.remaining > 0);
21
+ if (!f)
22
+ return undefined;
23
+ f.remaining -= 1;
24
+ return f.error;
25
+ }
26
+ async *list(_ctx) {
27
+ this.events.push({ op: 'list' });
28
+ for (const [nativeId, { label }] of this.state) {
29
+ yield { nativeId, label };
30
+ }
31
+ }
32
+ async read(_ctx, nativeId) {
33
+ this.events.push({ op: 'read', nativeId });
34
+ const err = this.#consumeFailure('read');
35
+ if (err)
36
+ throw err;
37
+ const entry = this.state.get(nativeId);
38
+ if (!entry)
39
+ return NotFound;
40
+ return entry.properties;
41
+ }
42
+ async create(_ctx, label, desired) {
43
+ const err = this.#consumeFailure('create');
44
+ if (err)
45
+ throw err;
46
+ const nativeId = `native-${this.resourceType}-${++this.#idCounter}`;
47
+ this.state.set(nativeId, { label, properties: desired });
48
+ this.events.push({ op: 'create', nativeId, label, properties: desired });
49
+ return { kind: 'sync', nativeId, properties: desired };
50
+ }
51
+ async update(_ctx, nativeId, _prior, desired) {
52
+ const err = this.#consumeFailure('update');
53
+ if (err)
54
+ throw err;
55
+ const entry = this.state.get(nativeId);
56
+ const label = entry ? entry.label : 'unknown';
57
+ this.state.set(nativeId, { label, properties: desired });
58
+ this.events.push({ op: 'update', nativeId, properties: desired });
59
+ return { kind: 'sync', nativeId, properties: desired };
60
+ }
61
+ async delete(_ctx, nativeId) {
62
+ const err = this.#consumeFailure('delete');
63
+ if (err)
64
+ throw err;
65
+ this.state.delete(nativeId);
66
+ this.events.push({ op: 'delete', nativeId });
67
+ return { kind: 'sync' };
68
+ }
69
+ async status(_ctx, _nativeId, _opId) {
70
+ return { kind: 'success', properties: null };
71
+ }
72
+ }
73
+ export function makeFakeContext() {
74
+ return {
75
+ cloudflare: {},
76
+ accountId: 'test-account',
77
+ namespace: 'default',
78
+ managedByLabel: 'k1c.io/managed-by=k1c',
79
+ signal: new AbortController().signal,
80
+ readFile: async (path) => new TextEncoder().encode(`// stub for ${path}`),
81
+ };
82
+ }
83
+ //# sourceMappingURL=fake-provider.js.map
@@ -0,0 +1,6 @@
1
+ import type { ProviderContext } from '../providers/types.ts';
2
+ import type { ProviderRegistry } from '../providers/registry.ts';
3
+ import type { DesiredResource, Plan } from './types.ts';
4
+ export declare function plan(desired: ReadonlyArray<DesiredResource>, registry: ProviderRegistry, ctx: ProviderContext): Promise<Plan>;
5
+ export declare function propertiesEqual(a: unknown, b: unknown): boolean;
6
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { NotFound } from "../providers/types.js";
2
+ import { namespaceFromLabel } from "./types.js";
3
+ import { refKey } from "../manifest/types.js";
4
+ import { topoSort } from "./topo.js";
5
+ export async function plan(desired, registry, ctx) {
6
+ const desiredNamespaces = new Set();
7
+ const desiredByType = new Map();
8
+ for (const d of desired) {
9
+ desiredNamespaces.add(d.ref.namespace);
10
+ const arr = desiredByType.get(d.resourceType) ?? [];
11
+ arr.push(d);
12
+ desiredByType.set(d.resourceType, arr);
13
+ }
14
+ const operations = [];
15
+ for (const resourceType of registry.types()) {
16
+ const provider = registry.get(resourceType);
17
+ const desiredOfType = desiredByType.get(resourceType) ?? [];
18
+ const desiredByLabel = new Map();
19
+ for (const d of desiredOfType)
20
+ desiredByLabel.set(d.label, d);
21
+ const actualByLabel = new Map();
22
+ for await (const item of provider.list(ctx)) {
23
+ actualByLabel.set(item.label, item);
24
+ }
25
+ for (const d of desiredOfType) {
26
+ const actual = actualByLabel.get(d.label);
27
+ if (!actual) {
28
+ operations.push({
29
+ kind: 'create',
30
+ resourceType,
31
+ ref: d.ref,
32
+ label: d.label,
33
+ properties: d.properties,
34
+ });
35
+ continue;
36
+ }
37
+ const prior = await provider.read(ctx, actual.nativeId);
38
+ if (prior === NotFound) {
39
+ operations.push({
40
+ kind: 'create',
41
+ resourceType,
42
+ ref: d.ref,
43
+ label: d.label,
44
+ properties: d.properties,
45
+ });
46
+ continue;
47
+ }
48
+ if (propertiesEqual(prior, d.properties)) {
49
+ operations.push({
50
+ kind: 'noop',
51
+ resourceType,
52
+ ref: d.ref,
53
+ label: d.label,
54
+ });
55
+ }
56
+ else {
57
+ operations.push({
58
+ kind: 'update',
59
+ resourceType,
60
+ ref: d.ref,
61
+ label: d.label,
62
+ nativeId: actual.nativeId,
63
+ prior,
64
+ properties: d.properties,
65
+ });
66
+ }
67
+ }
68
+ for (const [label, actual] of actualByLabel) {
69
+ if (desiredByLabel.has(label))
70
+ continue;
71
+ if (!desiredNamespaces.has(namespaceFromLabel(label)))
72
+ continue;
73
+ operations.push({
74
+ kind: 'delete',
75
+ resourceType,
76
+ nativeId: actual.nativeId,
77
+ label,
78
+ });
79
+ }
80
+ }
81
+ return { operations: orderByDependencies(operations, desired) };
82
+ }
83
+ function orderByDependencies(operations, desired) {
84
+ const depsByRef = new Map();
85
+ for (const d of desired) {
86
+ depsByRef.set(refKey(d.ref), (d.dependsOn ?? []).map(refKey));
87
+ }
88
+ const mutating = [];
89
+ const noops = [];
90
+ const deletes = [];
91
+ for (const op of operations) {
92
+ if (op.kind === 'create' || op.kind === 'update')
93
+ mutating.push(op);
94
+ else if (op.kind === 'noop')
95
+ noops.push(op);
96
+ else
97
+ deletes.push(op);
98
+ }
99
+ const nodes = mutating.map((op) => {
100
+ const id = refKey(op.ref);
101
+ return { id, op, dependsOn: depsByRef.get(id) ?? [] };
102
+ });
103
+ const sortedMutating = topoSort(nodes).map((n) => n.op);
104
+ noops.sort((a, b) => a.label.localeCompare(b.label));
105
+ deletes.sort((a, b) => a.label.localeCompare(b.label));
106
+ return [...sortedMutating, ...noops, ...deletes];
107
+ }
108
+ export function propertiesEqual(a, b) {
109
+ return canonicalize(a) === canonicalize(b);
110
+ }
111
+ function canonicalize(value) {
112
+ return JSON.stringify(value, replacer);
113
+ }
114
+ function replacer(_key, value) {
115
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
116
+ const sorted = {};
117
+ const keys = Object.keys(value).sort();
118
+ for (const k of keys)
119
+ sorted[k] = value[k];
120
+ return sorted;
121
+ }
122
+ return value;
123
+ }
124
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1,10 @@
1
+ export interface TopoNode {
2
+ readonly id: string;
3
+ readonly dependsOn?: ReadonlyArray<string>;
4
+ }
5
+ export declare class CycleError extends Error {
6
+ readonly cycle: ReadonlyArray<string>;
7
+ constructor(cycle: ReadonlyArray<string>);
8
+ }
9
+ export declare function topoSort<T extends TopoNode>(nodes: ReadonlyArray<T>): T[];
10
+ //# sourceMappingURL=topo.d.ts.map
@@ -0,0 +1,53 @@
1
+ export class CycleError extends Error {
2
+ cycle;
3
+ constructor(cycle) {
4
+ super(`dependency cycle: ${[...cycle].sort().join(', ')}`);
5
+ this.name = 'CycleError';
6
+ this.cycle = cycle;
7
+ }
8
+ }
9
+ export function topoSort(nodes) {
10
+ const byId = new Map();
11
+ for (const node of nodes)
12
+ byId.set(node.id, node);
13
+ const inDegree = new Map();
14
+ const successors = new Map();
15
+ for (const node of nodes) {
16
+ inDegree.set(node.id, 0);
17
+ successors.set(node.id, []);
18
+ }
19
+ for (const node of nodes) {
20
+ for (const dep of node.dependsOn ?? []) {
21
+ if (!byId.has(dep))
22
+ continue;
23
+ inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1);
24
+ successors.get(dep).push(node.id);
25
+ }
26
+ }
27
+ const ready = [];
28
+ for (const [id, deg] of inDegree) {
29
+ if (deg === 0)
30
+ ready.push(id);
31
+ }
32
+ ready.sort();
33
+ const result = [];
34
+ while (ready.length > 0) {
35
+ const id = ready.shift();
36
+ result.push(byId.get(id));
37
+ const succs = [...(successors.get(id) ?? [])].sort();
38
+ for (const s of succs) {
39
+ const newDeg = (inDegree.get(s) ?? 0) - 1;
40
+ inDegree.set(s, newDeg);
41
+ if (newDeg === 0)
42
+ ready.push(s);
43
+ }
44
+ ready.sort();
45
+ }
46
+ if (result.length < nodes.length) {
47
+ const resolved = new Set(result.map((n) => n.id));
48
+ const remaining = nodes.filter((n) => !resolved.has(n.id)).map((n) => n.id);
49
+ throw new CycleError(remaining);
50
+ }
51
+ return result;
52
+ }
53
+ //# sourceMappingURL=topo.js.map
@@ -0,0 +1,54 @@
1
+ import type { ResourceRef } from '../manifest/types.ts';
2
+ import type { ProviderError } from '../providers/types.ts';
3
+ export interface DesiredResource<P = unknown> {
4
+ readonly resourceType: string;
5
+ readonly ref: ResourceRef;
6
+ readonly label: string;
7
+ readonly properties: P;
8
+ readonly dependsOn?: ReadonlyArray<ResourceRef>;
9
+ }
10
+ export type Operation = {
11
+ readonly kind: 'create';
12
+ readonly resourceType: string;
13
+ readonly ref: ResourceRef;
14
+ readonly label: string;
15
+ readonly properties: unknown;
16
+ } | {
17
+ readonly kind: 'update';
18
+ readonly resourceType: string;
19
+ readonly ref: ResourceRef;
20
+ readonly label: string;
21
+ readonly nativeId: string;
22
+ readonly prior: unknown;
23
+ readonly properties: unknown;
24
+ } | {
25
+ readonly kind: 'delete';
26
+ readonly resourceType: string;
27
+ readonly nativeId: string;
28
+ readonly label: string;
29
+ } | {
30
+ readonly kind: 'noop';
31
+ readonly resourceType: string;
32
+ readonly ref: ResourceRef;
33
+ readonly label: string;
34
+ };
35
+ export type OperationKind = Operation['kind'];
36
+ export interface Plan {
37
+ readonly operations: ReadonlyArray<Operation>;
38
+ }
39
+ export type OperationStatus = 'succeeded' | 'skipped' | 'failed';
40
+ export interface OperationResult {
41
+ readonly op: Operation;
42
+ readonly status: OperationStatus;
43
+ readonly nativeId?: string;
44
+ readonly error?: ProviderError;
45
+ }
46
+ export interface ApplyReport {
47
+ readonly results: ReadonlyArray<OperationResult>;
48
+ readonly succeeded: number;
49
+ readonly skipped: number;
50
+ readonly failed: number;
51
+ }
52
+ export declare function labelOf(ref: ResourceRef): string;
53
+ export declare function namespaceFromLabel(label: string): string;
54
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,8 @@
1
+ export function labelOf(ref) {
2
+ return `${ref.namespace}/${ref.name}`;
3
+ }
4
+ export function namespaceFromLabel(label) {
5
+ const idx = label.indexOf('/');
6
+ return idx === -1 ? 'default' : label.slice(0, idx);
7
+ }
8
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@mizchi/k1c",
3
+ "version": "0.1.0",
4
+ "description": "Experimental kubectl-style apply tool for Cloudflare",
5
+ "keywords": [
6
+ "cloudflare",
7
+ "workers",
8
+ "iac",
9
+ "kubectl",
10
+ "kubernetes",
11
+ "manifest"
12
+ ],
13
+ "homepage": "https://github.com/mizchi/k1c",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/mizchi/k1c.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/mizchi/k1c/issues"
20
+ },
21
+ "license": "MIT",
22
+ "author": "mizchi",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=24"
26
+ },
27
+ "packageManager": "pnpm@10.0.0",
28
+ "bin": {
29
+ "k1c": "./dist/cli/main.js"
30
+ },
31
+ "files": [
32
+ "dist/**/*.js",
33
+ "dist/**/*.d.ts",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "secretlint": "secretlint --secretlintignore .secretlintignore '**/*'",
46
+ "k1c": "node --experimental-strip-types --disable-warning=ExperimentalWarning src/cli/main.ts",
47
+ "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
48
+ },
49
+ "dependencies": {
50
+ "cloudflare": "^4.0.0",
51
+ "yaml": "^2.6.0",
52
+ "zod": "^3.23.0"
53
+ },
54
+ "devDependencies": {
55
+ "@secretlint/secretlint-rule-preset-recommend": "^13.0.0",
56
+ "@types/node": "^22.10.0",
57
+ "secretlint": "^13.0.0",
58
+ "typescript": "^5.7.0",
59
+ "vitest": "^2.1.0"
60
+ }
61
+ }