@mizchi/k1c 0.2.0 → 0.4.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.
- package/README.md +9 -6
- package/dist/cli/main.js +21 -1
- package/dist/ingress/router-template.d.ts +30 -0
- package/dist/ingress/router-template.js +50 -0
- package/dist/manifest/lower.js +223 -1
- package/dist/manifest/schemas.d.ts +3257 -145
- package/dist/manifest/schemas.js +62 -0
- package/dist/manifest/types.d.ts +75 -1
- package/dist/providers/access-application.d.ts +53 -0
- package/dist/providers/access-application.js +183 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/worker-route.d.ts +10 -0
- package/dist/providers/worker-route.js +115 -0
- package/dist/providers/worker.d.ts +8 -0
- package/dist/providers/worker.js +22 -0
- package/dist/reconciler/plan.js +44 -1
- package/package.json +1 -1
package/dist/manifest/schemas.js
CHANGED
|
@@ -32,6 +32,8 @@ const volumeSchema = z.object({
|
|
|
32
32
|
queueRef: z.object({ name: z.string() }).optional(),
|
|
33
33
|
vectorizeRef: z.object({ name: z.string() }).optional(),
|
|
34
34
|
analyticsEngineRef: z.object({ dataset: z.string() }).optional(),
|
|
35
|
+
mtlsCertificateRef: z.object({ certificateId: z.string() }).optional(),
|
|
36
|
+
pipelinesRef: z.object({ pipelineId: z.string() }).optional(),
|
|
35
37
|
});
|
|
36
38
|
const containerSchema = z.object({
|
|
37
39
|
name: z.string(),
|
|
@@ -286,6 +288,64 @@ export const logpushJobSchema = z.object({
|
|
|
286
288
|
message: 'LogpushJob.spec must specify exactly one of zoneId / accountId',
|
|
287
289
|
}),
|
|
288
290
|
});
|
|
291
|
+
const ingressBackendSchema = z.object({
|
|
292
|
+
service: z.object({
|
|
293
|
+
name: z.string().min(1),
|
|
294
|
+
port: z
|
|
295
|
+
.object({
|
|
296
|
+
number: z.number().int().positive().optional(),
|
|
297
|
+
name: z.string().optional(),
|
|
298
|
+
})
|
|
299
|
+
.optional(),
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
const ingressPathSchema = z.object({
|
|
303
|
+
path: z.string().min(1),
|
|
304
|
+
pathType: z.enum(['Prefix', 'Exact', 'ImplementationSpecific']),
|
|
305
|
+
backend: ingressBackendSchema,
|
|
306
|
+
});
|
|
307
|
+
const ingressRuleSchema = z.object({
|
|
308
|
+
host: z.string().min(1).optional(),
|
|
309
|
+
http: z.object({ paths: z.array(ingressPathSchema).min(1) }),
|
|
310
|
+
});
|
|
311
|
+
const accessRuleSchema = z.union([
|
|
312
|
+
z.object({ email: z.object({ email: z.string().min(1) }) }),
|
|
313
|
+
z.object({ emailDomain: z.object({ domain: z.string().min(1) }) }),
|
|
314
|
+
z.object({ everyone: z.object({}).strict() }),
|
|
315
|
+
z.object({ ip: z.object({ ip: z.string().min(1) }) }),
|
|
316
|
+
z.object({ country: z.object({ code: z.string().min(2) }) }),
|
|
317
|
+
z.object({ serviceToken: z.object({ tokenId: z.string().min(1) }) }),
|
|
318
|
+
z.object({ anyValidServiceToken: z.object({}).strict() }),
|
|
319
|
+
]);
|
|
320
|
+
const accessAppPolicySchema = z.object({
|
|
321
|
+
name: z.string().min(1),
|
|
322
|
+
decision: z.enum(['allow', 'deny', 'bypass', 'non_identity']),
|
|
323
|
+
include: z.array(accessRuleSchema).min(1),
|
|
324
|
+
exclude: z.array(accessRuleSchema).optional(),
|
|
325
|
+
require: z.array(accessRuleSchema).optional(),
|
|
326
|
+
sessionDuration: z.string().optional(),
|
|
327
|
+
});
|
|
328
|
+
export const accessApplicationSchema = z.object({
|
|
329
|
+
apiVersion: z.literal('cloudflare.k1c.io/v1alpha1'),
|
|
330
|
+
kind: z.literal('AccessApplication'),
|
|
331
|
+
metadata: objectMetaSchema,
|
|
332
|
+
spec: z.object({
|
|
333
|
+
domain: z.string().min(1),
|
|
334
|
+
sessionDuration: z.string().optional(),
|
|
335
|
+
autoRedirectToIdentity: z.boolean().optional(),
|
|
336
|
+
allowedIdps: z.array(z.string()).optional(),
|
|
337
|
+
policies: z.array(accessAppPolicySchema).min(1),
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
340
|
+
export const ingressSchema = z.object({
|
|
341
|
+
apiVersion: z.literal('networking.k8s.io/v1'),
|
|
342
|
+
kind: z.literal('Ingress'),
|
|
343
|
+
metadata: objectMetaSchema,
|
|
344
|
+
spec: z.object({
|
|
345
|
+
rules: z.array(ingressRuleSchema).min(1),
|
|
346
|
+
defaultBackend: ingressBackendSchema.optional(),
|
|
347
|
+
}),
|
|
348
|
+
});
|
|
289
349
|
export const k1cResourceSchema = z.discriminatedUnion('kind', [
|
|
290
350
|
deploymentSchema,
|
|
291
351
|
rolloutSchema,
|
|
@@ -305,5 +365,7 @@ export const k1cResourceSchema = z.discriminatedUnion('kind', [
|
|
|
305
365
|
vectorizeSchema,
|
|
306
366
|
dnsRecordSchema,
|
|
307
367
|
logpushJobSchema,
|
|
368
|
+
ingressSchema,
|
|
369
|
+
accessApplicationSchema,
|
|
308
370
|
]);
|
|
309
371
|
//# sourceMappingURL=schemas.js.map
|
package/dist/manifest/types.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ export interface Volume {
|
|
|
61
61
|
readonly analyticsEngineRef?: {
|
|
62
62
|
readonly dataset: string;
|
|
63
63
|
};
|
|
64
|
+
readonly mtlsCertificateRef?: {
|
|
65
|
+
readonly certificateId: string;
|
|
66
|
+
};
|
|
67
|
+
readonly pipelinesRef?: {
|
|
68
|
+
readonly pipelineId: string;
|
|
69
|
+
};
|
|
64
70
|
}
|
|
65
71
|
export interface PodTemplateSpec {
|
|
66
72
|
readonly metadata?: ObjectMeta;
|
|
@@ -233,7 +239,75 @@ export interface LogpushJobSpec {
|
|
|
233
239
|
readonly filter?: string;
|
|
234
240
|
}
|
|
235
241
|
export type LogpushJob = BaseResource<'LogpushJob', 'cloudflare.k1c.io/v1alpha1', LogpushJobSpec>;
|
|
236
|
-
export type
|
|
242
|
+
export type IngressPathType = 'Prefix' | 'Exact' | 'ImplementationSpecific';
|
|
243
|
+
export interface IngressBackend {
|
|
244
|
+
readonly service: {
|
|
245
|
+
readonly name: string;
|
|
246
|
+
readonly port?: {
|
|
247
|
+
readonly number?: number;
|
|
248
|
+
readonly name?: string;
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
export interface IngressPath {
|
|
253
|
+
readonly path: string;
|
|
254
|
+
readonly pathType: IngressPathType;
|
|
255
|
+
readonly backend: IngressBackend;
|
|
256
|
+
}
|
|
257
|
+
export interface IngressRule {
|
|
258
|
+
readonly host?: string;
|
|
259
|
+
readonly http: {
|
|
260
|
+
readonly paths: ReadonlyArray<IngressPath>;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export interface IngressSpec {
|
|
264
|
+
readonly rules: ReadonlyArray<IngressRule>;
|
|
265
|
+
readonly defaultBackend?: IngressBackend;
|
|
266
|
+
}
|
|
267
|
+
export type Ingress = BaseResource<'Ingress', 'networking.k8s.io/v1', IngressSpec>;
|
|
268
|
+
export type AccessDecision = 'allow' | 'deny' | 'bypass' | 'non_identity';
|
|
269
|
+
export type AccessRule = {
|
|
270
|
+
readonly email: {
|
|
271
|
+
readonly email: string;
|
|
272
|
+
};
|
|
273
|
+
} | {
|
|
274
|
+
readonly emailDomain: {
|
|
275
|
+
readonly domain: string;
|
|
276
|
+
};
|
|
277
|
+
} | {
|
|
278
|
+
readonly everyone: Readonly<Record<string, never>>;
|
|
279
|
+
} | {
|
|
280
|
+
readonly ip: {
|
|
281
|
+
readonly ip: string;
|
|
282
|
+
};
|
|
283
|
+
} | {
|
|
284
|
+
readonly country: {
|
|
285
|
+
readonly code: string;
|
|
286
|
+
};
|
|
287
|
+
} | {
|
|
288
|
+
readonly serviceToken: {
|
|
289
|
+
readonly tokenId: string;
|
|
290
|
+
};
|
|
291
|
+
} | {
|
|
292
|
+
readonly anyValidServiceToken: Readonly<Record<string, never>>;
|
|
293
|
+
};
|
|
294
|
+
export interface AccessAppPolicy {
|
|
295
|
+
readonly name: string;
|
|
296
|
+
readonly decision: AccessDecision;
|
|
297
|
+
readonly include: ReadonlyArray<AccessRule>;
|
|
298
|
+
readonly exclude?: ReadonlyArray<AccessRule>;
|
|
299
|
+
readonly require?: ReadonlyArray<AccessRule>;
|
|
300
|
+
readonly sessionDuration?: string;
|
|
301
|
+
}
|
|
302
|
+
export interface AccessApplicationSpec {
|
|
303
|
+
readonly domain: string;
|
|
304
|
+
readonly sessionDuration?: string;
|
|
305
|
+
readonly autoRedirectToIdentity?: boolean;
|
|
306
|
+
readonly allowedIdps?: ReadonlyArray<string>;
|
|
307
|
+
readonly policies: ReadonlyArray<AccessAppPolicy>;
|
|
308
|
+
}
|
|
309
|
+
export type AccessApplication = BaseResource<'AccessApplication', 'cloudflare.k1c.io/v1alpha1', AccessApplicationSpec>;
|
|
310
|
+
export type K1cResource = Deployment | Rollout | StatefulSet | CronJob | Job | ConfigMapResource | SecretResource | NamespaceResource | ServiceResource | R2Bucket | KVNamespace | DispatchNamespace | Hyperdrive | D1Database | Queue | Vectorize | DNSRecord | LogpushJob | Ingress | AccessApplication;
|
|
237
311
|
export type ResourceKind = K1cResource['kind'];
|
|
238
312
|
export interface ResourceRef {
|
|
239
313
|
readonly apiVersion: string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CloudflareResourceProvider } from './types.ts';
|
|
3
|
+
export type AccessDecisionWire = 'allow' | 'deny' | 'bypass' | 'non_identity';
|
|
4
|
+
/**
|
|
5
|
+
* SDK-shaped (snake_case) Access rule. The lowering layer translates the camelCase
|
|
6
|
+
* manifest form into this once and the provider then ferries it across the API
|
|
7
|
+
* verbatim. Keeping a flat wire shape here makes the discriminated union match
|
|
8
|
+
* Cloudflare's response without a second translation step.
|
|
9
|
+
*/
|
|
10
|
+
export type AccessRuleWire = {
|
|
11
|
+
readonly email: {
|
|
12
|
+
readonly email: string;
|
|
13
|
+
};
|
|
14
|
+
} | {
|
|
15
|
+
readonly email_domain: {
|
|
16
|
+
readonly domain: string;
|
|
17
|
+
};
|
|
18
|
+
} | {
|
|
19
|
+
readonly everyone: Readonly<Record<string, never>>;
|
|
20
|
+
} | {
|
|
21
|
+
readonly ip: {
|
|
22
|
+
readonly ip: string;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
readonly country: {
|
|
26
|
+
readonly country_code: string;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
readonly service_token: {
|
|
30
|
+
readonly token_id: string;
|
|
31
|
+
};
|
|
32
|
+
} | {
|
|
33
|
+
readonly any_valid_service_token: Readonly<Record<string, never>>;
|
|
34
|
+
};
|
|
35
|
+
export interface AccessAppPolicyWire {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly decision: AccessDecisionWire;
|
|
38
|
+
readonly include: ReadonlyArray<AccessRuleWire>;
|
|
39
|
+
readonly exclude?: ReadonlyArray<AccessRuleWire>;
|
|
40
|
+
readonly require?: ReadonlyArray<AccessRuleWire>;
|
|
41
|
+
readonly session_duration?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface AccessApplicationProperties {
|
|
44
|
+
readonly appName: string;
|
|
45
|
+
readonly domain: string;
|
|
46
|
+
readonly sessionDuration?: string;
|
|
47
|
+
readonly autoRedirectToIdentity?: boolean;
|
|
48
|
+
readonly allowedIdps?: ReadonlyArray<string>;
|
|
49
|
+
readonly policies: ReadonlyArray<AccessAppPolicyWire>;
|
|
50
|
+
}
|
|
51
|
+
export declare const accessApplicationSchema: z.ZodType<AccessApplicationProperties>;
|
|
52
|
+
export declare const accessApplicationProvider: CloudflareResourceProvider<AccessApplicationProperties>;
|
|
53
|
+
//# sourceMappingURL=access-application.d.ts.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NotFound } from "./types.js";
|
|
3
|
+
import { toProviderError } from "./errors.js";
|
|
4
|
+
const accessRuleWireSchema = z.union([
|
|
5
|
+
z.object({ email: z.object({ email: z.string() }) }),
|
|
6
|
+
z.object({ email_domain: z.object({ domain: z.string() }) }),
|
|
7
|
+
z.object({ everyone: z.object({}).strict() }),
|
|
8
|
+
z.object({ ip: z.object({ ip: z.string() }) }),
|
|
9
|
+
z.object({ country: z.object({ country_code: z.string() }) }),
|
|
10
|
+
z.object({ service_token: z.object({ token_id: z.string() }) }),
|
|
11
|
+
z.object({ any_valid_service_token: z.object({}).strict() }),
|
|
12
|
+
]);
|
|
13
|
+
const accessAppPolicyWireSchema = z.object({
|
|
14
|
+
name: z.string(),
|
|
15
|
+
decision: z.enum(['allow', 'deny', 'bypass', 'non_identity']),
|
|
16
|
+
include: z.array(accessRuleWireSchema),
|
|
17
|
+
exclude: z.array(accessRuleWireSchema).optional(),
|
|
18
|
+
require: z.array(accessRuleWireSchema).optional(),
|
|
19
|
+
session_duration: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
export const accessApplicationSchema = z.object({
|
|
22
|
+
appName: z.string(),
|
|
23
|
+
domain: z.string(),
|
|
24
|
+
sessionDuration: z.string().optional(),
|
|
25
|
+
autoRedirectToIdentity: z.boolean().optional(),
|
|
26
|
+
allowedIdps: z.array(z.string()).optional(),
|
|
27
|
+
policies: z.array(accessAppPolicyWireSchema),
|
|
28
|
+
});
|
|
29
|
+
const NAME_PREFIX = 'k1c-';
|
|
30
|
+
/**
|
|
31
|
+
* Cloudflare Access Applications carry no per-app comment field, so ownership is
|
|
32
|
+
* inferred from the application name starting with the `k1c-` prefix. This is
|
|
33
|
+
* the same approach used by other prefix-named resources.
|
|
34
|
+
*/
|
|
35
|
+
function parseLabel(name) {
|
|
36
|
+
if (typeof name !== 'string' || !name.startsWith(NAME_PREFIX))
|
|
37
|
+
return null;
|
|
38
|
+
const rest = name.slice(NAME_PREFIX.length);
|
|
39
|
+
const dash = rest.indexOf('-');
|
|
40
|
+
if (dash <= 0 || dash === rest.length - 1)
|
|
41
|
+
return null;
|
|
42
|
+
return `${rest.slice(0, dash)}/${rest.slice(dash + 1)}`;
|
|
43
|
+
}
|
|
44
|
+
function buildBody(props) {
|
|
45
|
+
return {
|
|
46
|
+
name: props.appName,
|
|
47
|
+
domain: props.domain,
|
|
48
|
+
type: 'self_hosted',
|
|
49
|
+
...(props.sessionDuration !== undefined ? { session_duration: props.sessionDuration } : {}),
|
|
50
|
+
...(props.autoRedirectToIdentity !== undefined
|
|
51
|
+
? { auto_redirect_to_identity: props.autoRedirectToIdentity }
|
|
52
|
+
: {}),
|
|
53
|
+
...(props.allowedIdps !== undefined ? { allowed_idps: [...props.allowedIdps] } : {}),
|
|
54
|
+
policies: props.policies.map((p) => ({
|
|
55
|
+
name: p.name,
|
|
56
|
+
decision: p.decision,
|
|
57
|
+
include: [...p.include],
|
|
58
|
+
...(p.exclude !== undefined ? { exclude: [...p.exclude] } : {}),
|
|
59
|
+
...(p.require !== undefined ? { require: [...p.require] } : {}),
|
|
60
|
+
...(p.session_duration !== undefined ? { session_duration: p.session_duration } : {}),
|
|
61
|
+
})),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export const accessApplicationProvider = {
|
|
65
|
+
resourceType: 'AccessApplication',
|
|
66
|
+
schema: accessApplicationSchema,
|
|
67
|
+
async *list(ctx) {
|
|
68
|
+
let iter;
|
|
69
|
+
try {
|
|
70
|
+
iter = ctx.cloudflare.zeroTrust.access.applications.list({ account_id: ctx.accountId });
|
|
71
|
+
}
|
|
72
|
+
catch (raw) {
|
|
73
|
+
throw toProviderError(raw);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
for await (const app of iter) {
|
|
77
|
+
const a = app;
|
|
78
|
+
if (!a.id)
|
|
79
|
+
continue;
|
|
80
|
+
const label = parseLabel(a.name);
|
|
81
|
+
if (label === null)
|
|
82
|
+
continue;
|
|
83
|
+
yield { nativeId: a.id, label };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (raw) {
|
|
87
|
+
throw toProviderError(raw);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async read(ctx, nativeId) {
|
|
91
|
+
try {
|
|
92
|
+
const app = (await ctx.cloudflare.zeroTrust.access.applications.get(nativeId, {
|
|
93
|
+
account_id: ctx.accountId,
|
|
94
|
+
}));
|
|
95
|
+
if (!app.name || !app.domain)
|
|
96
|
+
return NotFound;
|
|
97
|
+
// Reading back the policies is best-effort: Cloudflare returns a richer shape
|
|
98
|
+
// than we store, so we narrow back to the wire types we know how to emit.
|
|
99
|
+
// Unknown rule types are dropped from the read result, which means a manual
|
|
100
|
+
// edit in the dashboard may produce a perpetual "drift" — accepted for v0.4.
|
|
101
|
+
const policies = [];
|
|
102
|
+
for (const raw of app.policies ?? []) {
|
|
103
|
+
const p = raw;
|
|
104
|
+
if (!p.name || !p.decision || !p.include)
|
|
105
|
+
continue;
|
|
106
|
+
policies.push({
|
|
107
|
+
name: p.name,
|
|
108
|
+
decision: p.decision,
|
|
109
|
+
include: p.include,
|
|
110
|
+
...(p.exclude !== undefined
|
|
111
|
+
? { exclude: p.exclude }
|
|
112
|
+
: {}),
|
|
113
|
+
...(p.require !== undefined
|
|
114
|
+
? { require: p.require }
|
|
115
|
+
: {}),
|
|
116
|
+
...(p.session_duration !== undefined ? { session_duration: p.session_duration } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
appName: app.name,
|
|
121
|
+
domain: app.domain,
|
|
122
|
+
...(app.session_duration !== undefined
|
|
123
|
+
? { sessionDuration: app.session_duration }
|
|
124
|
+
: {}),
|
|
125
|
+
...(app.auto_redirect_to_identity !== undefined
|
|
126
|
+
? { autoRedirectToIdentity: app.auto_redirect_to_identity }
|
|
127
|
+
: {}),
|
|
128
|
+
...(app.allowed_idps !== undefined ? { allowedIdps: [...app.allowed_idps] } : {}),
|
|
129
|
+
policies,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (raw) {
|
|
133
|
+
const err = toProviderError(raw);
|
|
134
|
+
if (err.code === 'NotFound')
|
|
135
|
+
return NotFound;
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async create(ctx, _label, desired) {
|
|
140
|
+
try {
|
|
141
|
+
const app = (await ctx.cloudflare.zeroTrust.access.applications.create({
|
|
142
|
+
account_id: ctx.accountId,
|
|
143
|
+
...buildBody(desired),
|
|
144
|
+
}));
|
|
145
|
+
return {
|
|
146
|
+
kind: 'sync',
|
|
147
|
+
nativeId: app.id ?? desired.appName,
|
|
148
|
+
properties: desired,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (raw) {
|
|
152
|
+
throw toProviderError(raw);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
async update(ctx, nativeId, _prior, desired) {
|
|
156
|
+
try {
|
|
157
|
+
const app = (await ctx.cloudflare.zeroTrust.access.applications.update(nativeId, {
|
|
158
|
+
account_id: ctx.accountId,
|
|
159
|
+
...buildBody(desired),
|
|
160
|
+
}));
|
|
161
|
+
return {
|
|
162
|
+
kind: 'sync',
|
|
163
|
+
nativeId: app.id ?? nativeId,
|
|
164
|
+
properties: desired,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (raw) {
|
|
168
|
+
throw toProviderError(raw);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
async delete(ctx, nativeId) {
|
|
172
|
+
try {
|
|
173
|
+
await ctx.cloudflare.zeroTrust.access.applications.delete(nativeId, {
|
|
174
|
+
account_id: ctx.accountId,
|
|
175
|
+
});
|
|
176
|
+
return { kind: 'sync' };
|
|
177
|
+
}
|
|
178
|
+
catch (raw) {
|
|
179
|
+
throw toProviderError(raw);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=access-application.js.map
|
package/dist/providers/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { vectorizeProvider } from "./vectorize.js";
|
|
|
13
13
|
import { dnsRecordProvider } from "./dns-record.js";
|
|
14
14
|
import { workflowProvider } from "./workflow.js";
|
|
15
15
|
import { logpushJobProvider } from "./logpush-job.js";
|
|
16
|
+
import { workerRouteProvider } from "./worker-route.js";
|
|
17
|
+
import { accessApplicationProvider } from "./access-application.js";
|
|
16
18
|
export function createDefaultRegistry() {
|
|
17
19
|
const r = new ProviderRegistry();
|
|
18
20
|
r.register(workerProvider);
|
|
@@ -29,6 +31,8 @@ export function createDefaultRegistry() {
|
|
|
29
31
|
r.register(dnsRecordProvider);
|
|
30
32
|
r.register(workflowProvider);
|
|
31
33
|
r.register(logpushJobProvider);
|
|
34
|
+
r.register(workerRouteProvider);
|
|
35
|
+
r.register(accessApplicationProvider);
|
|
32
36
|
return r;
|
|
33
37
|
}
|
|
34
38
|
export { ProviderRegistry } from "./registry.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CloudflareResourceProvider } from './types.ts';
|
|
3
|
+
export interface WorkerRouteProperties {
|
|
4
|
+
readonly zoneId: string;
|
|
5
|
+
readonly pattern: string;
|
|
6
|
+
readonly scriptName: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const workerRouteSchema: z.ZodType<WorkerRouteProperties>;
|
|
9
|
+
export declare const workerRouteProvider: CloudflareResourceProvider<WorkerRouteProperties>;
|
|
10
|
+
//# sourceMappingURL=worker-route.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NotFound } from "./types.js";
|
|
3
|
+
import { toProviderError } from "./errors.js";
|
|
4
|
+
export const workerRouteSchema = z.object({
|
|
5
|
+
zoneId: z.string(),
|
|
6
|
+
pattern: z.string(),
|
|
7
|
+
scriptName: z.string(),
|
|
8
|
+
});
|
|
9
|
+
const SCRIPT_PREFIX = 'k1c--';
|
|
10
|
+
/**
|
|
11
|
+
* Cloudflare Workers Routes carry no per-route comment field, so ownership is
|
|
12
|
+
* inferred from the bound `script` starting with the k1c naming prefix. This
|
|
13
|
+
* is the same approach used by the CustomDomain provider.
|
|
14
|
+
*/
|
|
15
|
+
function isManaged(script) {
|
|
16
|
+
return typeof script === 'string' && script.startsWith(SCRIPT_PREFIX);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Routes are keyed on `pattern` for the purpose of label matching — the SDK
|
|
20
|
+
* surface lets us list by zone, and within a zone the pattern is unique.
|
|
21
|
+
*/
|
|
22
|
+
function labelFromPattern(pattern) {
|
|
23
|
+
if (typeof pattern !== 'string' || pattern.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
return pattern;
|
|
26
|
+
}
|
|
27
|
+
export const workerRouteProvider = {
|
|
28
|
+
resourceType: 'WorkerRoute',
|
|
29
|
+
schema: workerRouteSchema,
|
|
30
|
+
async *list(ctx) {
|
|
31
|
+
if (ctx.zoneId === undefined) {
|
|
32
|
+
// Routes are zone-scoped; without a zone we cannot enumerate. Yield nothing.
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let iter;
|
|
36
|
+
try {
|
|
37
|
+
iter = ctx.cloudflare.workers.routes.list({ zone_id: ctx.zoneId });
|
|
38
|
+
}
|
|
39
|
+
catch (raw) {
|
|
40
|
+
throw toProviderError(raw);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
for await (const r of iter) {
|
|
44
|
+
if (!isManaged(r.script))
|
|
45
|
+
continue;
|
|
46
|
+
const label = labelFromPattern(r.pattern);
|
|
47
|
+
if (label === null || !r.id)
|
|
48
|
+
continue;
|
|
49
|
+
yield { nativeId: r.id, label };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (raw) {
|
|
53
|
+
throw toProviderError(raw);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async read(ctx, nativeId) {
|
|
57
|
+
if (ctx.zoneId === undefined)
|
|
58
|
+
return NotFound;
|
|
59
|
+
try {
|
|
60
|
+
const r = await ctx.cloudflare.workers.routes.get(nativeId, { zone_id: ctx.zoneId });
|
|
61
|
+
if (!r.pattern || !r.script)
|
|
62
|
+
return NotFound;
|
|
63
|
+
return { zoneId: ctx.zoneId, pattern: r.pattern, scriptName: r.script };
|
|
64
|
+
}
|
|
65
|
+
catch (raw) {
|
|
66
|
+
const err = toProviderError(raw);
|
|
67
|
+
if (err.code === 'NotFound')
|
|
68
|
+
return NotFound;
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async create(_ctx, _label, desired) {
|
|
73
|
+
try {
|
|
74
|
+
const r = await _ctx.cloudflare.workers.routes.create({
|
|
75
|
+
zone_id: desired.zoneId,
|
|
76
|
+
pattern: desired.pattern,
|
|
77
|
+
script: desired.scriptName,
|
|
78
|
+
});
|
|
79
|
+
return { kind: 'sync', nativeId: r.id, properties: desired };
|
|
80
|
+
}
|
|
81
|
+
catch (raw) {
|
|
82
|
+
throw toProviderError(raw);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async update(ctx, nativeId, _prior, desired) {
|
|
86
|
+
try {
|
|
87
|
+
const r = await ctx.cloudflare.workers.routes.update(nativeId, {
|
|
88
|
+
zone_id: desired.zoneId,
|
|
89
|
+
pattern: desired.pattern,
|
|
90
|
+
script: desired.scriptName,
|
|
91
|
+
});
|
|
92
|
+
return { kind: 'sync', nativeId: r.id, properties: desired };
|
|
93
|
+
}
|
|
94
|
+
catch (raw) {
|
|
95
|
+
throw toProviderError(raw);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
async delete(ctx, nativeId) {
|
|
99
|
+
if (ctx.zoneId === undefined) {
|
|
100
|
+
throw {
|
|
101
|
+
code: 'InvalidRequest',
|
|
102
|
+
recoverable: false,
|
|
103
|
+
message: 'WorkerRoute delete requires zoneId in ProviderContext',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await ctx.cloudflare.workers.routes.delete(nativeId, { zone_id: ctx.zoneId });
|
|
108
|
+
return { kind: 'sync' };
|
|
109
|
+
}
|
|
110
|
+
catch (raw) {
|
|
111
|
+
throw toProviderError(raw);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=worker-route.js.map
|
|
@@ -100,6 +100,14 @@ export type WorkerBinding = {
|
|
|
100
100
|
readonly type: 'analytics_engine';
|
|
101
101
|
readonly name: string;
|
|
102
102
|
readonly dataset: string;
|
|
103
|
+
} | {
|
|
104
|
+
readonly type: 'mtls_certificate';
|
|
105
|
+
readonly name: string;
|
|
106
|
+
readonly certificateId: string;
|
|
107
|
+
} | {
|
|
108
|
+
readonly type: 'pipelines';
|
|
109
|
+
readonly name: string;
|
|
110
|
+
readonly pipeline: string;
|
|
103
111
|
};
|
|
104
112
|
export declare const workerSchema: z.ZodType<WorkerProperties>;
|
|
105
113
|
export declare const workerProvider: CloudflareResourceProvider<WorkerProperties>;
|
package/dist/providers/worker.js
CHANGED
|
@@ -64,6 +64,16 @@ export const workerSchema = z.object({
|
|
|
64
64
|
name: z.string(),
|
|
65
65
|
dataset: z.string(),
|
|
66
66
|
}),
|
|
67
|
+
z.object({
|
|
68
|
+
type: z.literal('mtls_certificate'),
|
|
69
|
+
name: z.string(),
|
|
70
|
+
certificateId: z.string(),
|
|
71
|
+
}),
|
|
72
|
+
z.object({
|
|
73
|
+
type: z.literal('pipelines'),
|
|
74
|
+
name: z.string(),
|
|
75
|
+
pipeline: z.string(),
|
|
76
|
+
}),
|
|
67
77
|
]))
|
|
68
78
|
.optional(),
|
|
69
79
|
observability: z.object({ enabled: z.boolean() }).optional(),
|
|
@@ -137,6 +147,12 @@ function buildBindings(props) {
|
|
|
137
147
|
else if (b.type === 'analytics_engine') {
|
|
138
148
|
out.push({ type: 'analytics_engine', name: b.name, dataset: b.dataset });
|
|
139
149
|
}
|
|
150
|
+
else if (b.type === 'mtls_certificate') {
|
|
151
|
+
out.push({ type: 'mtls_certificate', name: b.name, certificate_id: b.certificateId });
|
|
152
|
+
}
|
|
153
|
+
else if (b.type === 'pipelines') {
|
|
154
|
+
out.push({ type: 'pipelines', name: b.name, pipeline: b.pipeline });
|
|
155
|
+
}
|
|
140
156
|
}
|
|
141
157
|
return out;
|
|
142
158
|
}
|
|
@@ -306,6 +322,12 @@ function fromCFBinding(b) {
|
|
|
306
322
|
if (b.type === 'analytics_engine' && b.dataset !== undefined) {
|
|
307
323
|
return { type: 'analytics_engine', name: b.name, dataset: b.dataset };
|
|
308
324
|
}
|
|
325
|
+
if (b.type === 'mtls_certificate' && b.certificate_id !== undefined) {
|
|
326
|
+
return { type: 'mtls_certificate', name: b.name, certificateId: b.certificate_id };
|
|
327
|
+
}
|
|
328
|
+
if (b.type === 'pipelines' && b.pipeline !== undefined) {
|
|
329
|
+
return { type: 'pipelines', name: b.name, pipeline: b.pipeline };
|
|
330
|
+
}
|
|
309
331
|
return null;
|
|
310
332
|
}
|
|
311
333
|
export const workerProvider = {
|
package/dist/reconciler/plan.js
CHANGED
|
@@ -102,9 +102,52 @@ function orderByDependencies(operations, desired) {
|
|
|
102
102
|
});
|
|
103
103
|
const sortedMutating = topoSort(nodes).map((n) => n.op);
|
|
104
104
|
noops.sort((a, b) => a.label.localeCompare(b.label));
|
|
105
|
-
deletes.sort((a, b) =>
|
|
105
|
+
deletes.sort((a, b) => {
|
|
106
|
+
const pa = deletePriority(a.resourceType);
|
|
107
|
+
const pb = deletePriority(b.resourceType);
|
|
108
|
+
if (pa !== pb)
|
|
109
|
+
return pa - pb;
|
|
110
|
+
return a.label.localeCompare(b.label);
|
|
111
|
+
});
|
|
106
112
|
return [...sortedMutating, ...noops, ...deletes];
|
|
107
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Reverse-topological priority for deletes. Smaller priority is deleted first.
|
|
116
|
+
*
|
|
117
|
+
* The reconciler has no per-instance dependency record for resources that are
|
|
118
|
+
* being deleted (they are no longer in `desired`), so we approximate the
|
|
119
|
+
* dependency direction with a static type ordering that mirrors how creates
|
|
120
|
+
* actually flow in `lower.ts`. Workers depend on data services; CustomDomain /
|
|
121
|
+
* Workflow / LogpushJob depend on Workers; DispatchNamespace hosts Workers.
|
|
122
|
+
*
|
|
123
|
+
* Unknown types fall through with a neutral priority so the sort is still stable.
|
|
124
|
+
*/
|
|
125
|
+
function deletePriority(resourceType) {
|
|
126
|
+
switch (resourceType) {
|
|
127
|
+
// Top-level edges — nothing else points at these. Delete first so we do not
|
|
128
|
+
// serve traffic to a Worker we are about to remove.
|
|
129
|
+
case 'CustomDomain':
|
|
130
|
+
case 'WorkerRoute':
|
|
131
|
+
case 'DNSRecord':
|
|
132
|
+
case 'LogpushJob':
|
|
133
|
+
case 'Workflow':
|
|
134
|
+
case 'AccessApplication':
|
|
135
|
+
return 0;
|
|
136
|
+
case 'Worker':
|
|
137
|
+
return 1;
|
|
138
|
+
case 'R2Bucket':
|
|
139
|
+
case 'KVNamespace':
|
|
140
|
+
case 'D1Database':
|
|
141
|
+
case 'Hyperdrive':
|
|
142
|
+
case 'Vectorize':
|
|
143
|
+
case 'Queue':
|
|
144
|
+
return 2;
|
|
145
|
+
case 'DispatchNamespace':
|
|
146
|
+
return 3;
|
|
147
|
+
default:
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
108
151
|
export function propertiesEqual(a, b) {
|
|
109
152
|
return canonicalize(a) === canonicalize(b);
|
|
110
153
|
}
|