@intentius/chant-lexicon-fly 0.100.0 → 0.102.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/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/codegen/package.d.ts +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/composites/fly-deploy.d.ts +2 -0
- package/dist/composites/fly-deploy.d.ts.map +1 -1
- package/dist/composites/fly-site.d.ts +2 -2
- package/dist/generated/index.d.ts +41 -41
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/integrity.json +5 -5
- package/dist/manifest.json +1 -1
- package/dist/okf/types/Secret.md +1 -0
- package/dist/op/activities/flaps.d.ts.map +1 -1
- package/dist/op/activities/fly-apply.d.ts +87 -8
- package/dist/op/activities/fly-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/machines-contract.d.ts.map +1 -1
- package/dist/op/activities/machines-fake.d.ts +10 -1
- package/dist/op/activities/machines-fake.d.ts.map +1 -1
- package/dist/op/activities/machines-local.d.ts.map +1 -1
- package/dist/op/activities/progress.d.ts +9 -0
- package/dist/op/activities/progress.d.ts.map +1 -0
- package/dist/op/activities/sprite-config.d.ts.map +1 -1
- package/dist/op/activities/sprite-fs.d.ts.map +1 -1
- package/dist/op/activities/sprite-service-converge.d.ts.map +1 -1
- package/dist/op/activities/sprite-services.d.ts.map +1 -1
- package/dist/op/activities/sprites-emulator.d.ts.map +1 -1
- package/dist/op/activities/sprites.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +10 -10
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/release-metadata.d.ts.map +1 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-fly-ops.md +8 -0
- package/dist/skills/chant-fly-patterns.md +1 -1
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/package.json +3 -3
- package/src/codegen/docs.ts +16 -1
- package/src/composites/fly-deploy.ts +2 -0
- package/src/describe-resources.test.ts +18 -1
- package/src/describe-resources.ts +1 -1
- package/src/generated/index.d.ts +2 -1
- package/src/generated/index.ts +1 -1
- package/src/op/activities/fly-apply.integration.test.ts +12 -5
- package/src/op/activities/fly-apply.test.ts +483 -2
- package/src/op/activities/fly-apply.ts +229 -45
- package/src/op/activities/index.ts +27 -1
- package/src/op/activities/machines-contract.docker.integration.test.ts +22 -1
- package/src/op/activities/machines-contract.ts +5 -1
- package/src/op/activities/machines-fake.ts +69 -2
- package/src/op/activities/machines-local-cli.ts +3 -0
- package/src/op/activities/machines-local.test.ts +48 -12
- package/src/op/activities/machines-local.ts +28 -5
- package/src/op/activities/progress.ts +10 -0
- package/src/op/activities/sprite-config.ts +10 -9
- package/src/op/activities/sprite-fs.ts +3 -2
- package/src/op/activities/sprite-progress.test.ts +52 -0
- package/src/op/activities/sprite-service-converge.ts +2 -1
- package/src/op/activities/sprite-services.ts +5 -4
- package/src/op/activities/sprites.ts +6 -5
- package/src/serializer.test.ts +17 -2
- package/src/serializer.ts +16 -7
- package/src/skills/chant-fly-ops.md +8 -0
- package/src/skills/chant-fly-patterns.md +1 -1
- package/src/spec/parse.ts +25 -2
- package/src/teardown.test.ts +22 -3
- package/src/teardown.ts +1 -1
package/src/serializer.ts
CHANGED
|
@@ -56,6 +56,11 @@ interface FlapsRequest {
|
|
|
56
56
|
* honors this flag by skipping the drift/diff read and always POSTing.
|
|
57
57
|
*/
|
|
58
58
|
applyOnly?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* A Secret declared without a value. Its value is set outside chant, so the
|
|
61
|
+
* applier never posts it and only checks that the app has it.
|
|
62
|
+
*/
|
|
63
|
+
mustExist?: boolean;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
/**
|
|
@@ -260,22 +265,26 @@ export const flySerializer: Serializer = {
|
|
|
260
265
|
if (entityType === SECRET_ENTITY_TYPE) {
|
|
261
266
|
// Apply-only (D7): name is the URL segment; `value` is the only body
|
|
262
267
|
// field. mudflaps returns just a digest, so this never enters a diff.
|
|
268
|
+
// A Secret with no value is set out of band. It is marked
|
|
269
|
+
// `mustExist`, and the applier checks for it instead of posting.
|
|
263
270
|
const secretName = typeof props.name === "string" ? props.name : name;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
};
|
|
271
|
+
const endpoint = `/v1/apps/${app}/secrets/${encodeURIComponent(secretName)}`;
|
|
272
|
+
requests[name] =
|
|
273
|
+
props.value !== undefined
|
|
274
|
+
? { endpoint, method: "POST", body: { value: props.value }, applyOnly: true }
|
|
275
|
+
: { endpoint, method: "POST", body: {}, applyOnly: true, mustExist: true };
|
|
270
276
|
continue;
|
|
271
277
|
}
|
|
272
278
|
|
|
279
|
+
// A Certificate is an ACME certificate. flaps creates those at
|
|
280
|
+
// `.../certificates/acme` and answers a bare POST to `.../certificates`
|
|
281
|
+
// with 404 (#3114).
|
|
273
282
|
const segment =
|
|
274
283
|
entityType === VOLUME_ENTITY_TYPE
|
|
275
284
|
? "volumes"
|
|
276
285
|
: entityType === IP_ENTITY_TYPE
|
|
277
286
|
? "ip_assignments"
|
|
278
|
-
: "certificates";
|
|
287
|
+
: "certificates/acme";
|
|
279
288
|
const body: Record<string, unknown> = {};
|
|
280
289
|
for (const [key, value] of Object.entries(props)) {
|
|
281
290
|
if (key === "app" || value === undefined) continue;
|
|
@@ -46,6 +46,14 @@ Prune is off by default and destructive. It removes resources the plan no longer
|
|
|
46
46
|
|
|
47
47
|
Each prune logs the resource and endpoint it removed, so a prune run is auditable from the Op output.
|
|
48
48
|
|
|
49
|
+
## Destroying a running machine
|
|
50
|
+
|
|
51
|
+
Fly refuses to destroy a machine that is still running: a plain `DELETE` answers 412 `failed_precondition` unless it carries `?force=true`. So prune and `flyDelete` stop a running machine first (under a lease), wait for `state=stopped`, and then destroy it. The stop sends the machine's configured kill signal and honours its kill timeout, so the process can drain. A machine that is already stopped, suspended or failed is destroyed without a stop.
|
|
52
|
+
|
|
53
|
+
If the stop does not reach `stopped` before the wait deadline, the destroy fails with an error naming the app, machine and id, and the machine is left in place. Pass `force: true` to `flyApply` or `flyDelete` to skip the stop and destroy with `?force=true`, which kills the machine at once.
|
|
54
|
+
|
|
55
|
+
The mudflaps emulator does not enforce the 412 yet (INTENTIUS/mudflaps#70), so only the in-memory flaps and a real org exercise this path.
|
|
56
|
+
|
|
49
57
|
## Teardown
|
|
50
58
|
|
|
51
59
|
`flyDelete` is the inverse of `flyApply`: destroy the machines the plan declares (dependents first), then delete the apps. It is idempotent, so an already-absent machine or app is a no-op. The deploy Op's teardown phase uses this to tear the emulator's app down at the end of an offline loop.
|
|
@@ -38,7 +38,7 @@ An IP is assigned if the declared type is not already present, keyed by family (
|
|
|
38
38
|
|
|
39
39
|
## Certificates
|
|
40
40
|
|
|
41
|
-
A
|
|
41
|
+
A `Certificate` is an ACME certificate that Fly issues once the hostname's DNS points at the app. `flyApply` creates it with `POST /v1/apps/{app}/certificates/acme`, if absent, idempotent by hostname. A re-apply for a hostname that already has a certificate is a no-op. Uploading your own certificate and key (Fly's `/certificates/custom`) is not modeled.
|
|
42
42
|
|
|
43
43
|
## Apply-only secrets
|
|
44
44
|
|
package/src/spec/parse.ts
CHANGED
|
@@ -68,6 +68,11 @@ interface ResourceSpec {
|
|
|
68
68
|
typeName: string;
|
|
69
69
|
request: string;
|
|
70
70
|
response: string;
|
|
71
|
+
/**
|
|
72
|
+
* Authoring props the request schema lacks because flaps takes them from the
|
|
73
|
+
* URL. The serializer moves them into the endpoint.
|
|
74
|
+
*/
|
|
75
|
+
pathProps?: ParsedProperty[];
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
const RESOURCES: ResourceSpec[] = [
|
|
@@ -76,7 +81,22 @@ const RESOURCES: ResourceSpec[] = [
|
|
|
76
81
|
{ typeName: `Fly::${SERVICE}::Volume`, request: "CreateVolumeRequest", response: "Volume" },
|
|
77
82
|
{ typeName: `Fly::${SERVICE}::IPAddress`, request: "assignIPRequest", response: "IPAssignment" },
|
|
78
83
|
{ typeName: `Fly::${SERVICE}::Certificate`, request: "createAcmeCertificateRequest", response: "CertificateDetail" },
|
|
79
|
-
{
|
|
84
|
+
{
|
|
85
|
+
typeName: `Fly::${SERVICE}::Secret`,
|
|
86
|
+
request: "SetAppSecretRequest",
|
|
87
|
+
response: "AppSecret",
|
|
88
|
+
pathProps: [
|
|
89
|
+
{
|
|
90
|
+
name: "name",
|
|
91
|
+
tsType: "string",
|
|
92
|
+
required: false,
|
|
93
|
+
description:
|
|
94
|
+
"The secret's name on the app. Defaults to the declaration's export name. " +
|
|
95
|
+
"A Secret with no value is never sent: apply checks that the app already has it.",
|
|
96
|
+
constraints: {},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
80
100
|
];
|
|
81
101
|
|
|
82
102
|
const REF_PREFIX = "#/components/schemas/";
|
|
@@ -117,11 +137,14 @@ export function parseFlyOpenAPI(data: string | Buffer): FlyParseResult[] {
|
|
|
117
137
|
constraints: coreExtractConstraints(prop as JsonSchemaProperty),
|
|
118
138
|
});
|
|
119
139
|
}
|
|
140
|
+
for (const extra of rspec.pathProps ?? []) {
|
|
141
|
+
if (!(extra.name in reqProps)) properties.push(extra);
|
|
142
|
+
}
|
|
120
143
|
|
|
121
144
|
// Attributes = response props not present in the request schema.
|
|
122
145
|
const attributes: Array<{ name: string; tsType: string }> = [];
|
|
123
146
|
for (const [name, prop] of Object.entries(res?.properties ?? {})) {
|
|
124
|
-
if (name
|
|
147
|
+
if (properties.some((p) => p.name === name)) continue;
|
|
125
148
|
attributes.push({ name, tsType: resolve(prop) });
|
|
126
149
|
}
|
|
127
150
|
|
package/src/teardown.test.ts
CHANGED
|
@@ -48,6 +48,8 @@ const OTHER_STACK = { "managed-by": "chant", "chant-stack": "blog", "chant-env":
|
|
|
48
48
|
* A stateful scripted flaps: `apps` maps app name to its machines; deletes
|
|
49
49
|
* mutate the state, so the app-boundary re-check after a destroy sees what a
|
|
50
50
|
* real flaps would. An app mapped to the string "boom" fails its machine list.
|
|
51
|
+
* Like flaps, a bare DELETE on a machine that is not stopped answers 412
|
|
52
|
+
* (#3115), and a stop moves it to `stopped`.
|
|
51
53
|
*/
|
|
52
54
|
function scriptedFlaps(apps: Record<string, FakeMachine[] | "boom">): { http: FlyHttp; log: string[] } {
|
|
53
55
|
const log: string[] = [];
|
|
@@ -62,10 +64,22 @@ function scriptedFlaps(apps: Record<string, FakeMachine[] | "boom">): { http: Fl
|
|
|
62
64
|
if (method === "GET" && /\/machines\/[^/]+\/wait\?/.test(url)) {
|
|
63
65
|
return { status: 200, text: JSON.stringify({ ok: true }) };
|
|
64
66
|
}
|
|
65
|
-
let m = url.match(/\/v1\/apps\/([^/?]+)\/machines\/([^/?]+)$/);
|
|
67
|
+
let m = url.match(/\/v1\/apps\/([^/?]+)\/machines\/([^/?]+)\/stop$/);
|
|
68
|
+
if (m && method === "POST") {
|
|
69
|
+
const held = apps[m[1]];
|
|
70
|
+
if (Array.isArray(held)) for (const mm of held) if (mm.id === m[2]) mm.state = "stopped";
|
|
71
|
+
return { status: 200, text: "{}" };
|
|
72
|
+
}
|
|
73
|
+
m = url.match(/\/v1\/apps\/([^/?]+)\/machines\/([^/?]+)(\?force=true)?$/);
|
|
66
74
|
if (m && method === "DELETE") {
|
|
67
75
|
const held = apps[m[1]];
|
|
68
|
-
if (Array.isArray(held))
|
|
76
|
+
if (Array.isArray(held)) {
|
|
77
|
+
const target = held.find((mm) => mm.id === m![2]);
|
|
78
|
+
if (target && target.state !== "stopped" && !m[3]) {
|
|
79
|
+
return { status: 412, text: JSON.stringify({ error: "failed_precondition: unable to destroy machine, not currently stopped" }) };
|
|
80
|
+
}
|
|
81
|
+
apps[m[1]] = held.filter((mm) => mm.id !== m![2]);
|
|
82
|
+
}
|
|
69
83
|
return { status: 200, text: "{}" };
|
|
70
84
|
}
|
|
71
85
|
m = url.match(/\/v1\/apps\/([^/?]+)\/machines$/);
|
|
@@ -147,7 +161,7 @@ describe("teardownOwned — marker machines + the app boundary (#743)", () => {
|
|
|
147
161
|
});
|
|
148
162
|
|
|
149
163
|
describe("executeTeardown — machines first, apps last", () => {
|
|
150
|
-
test("destroys the machines (
|
|
164
|
+
test("destroys the machines (stop → destroy → wait, each under a lease), then deletes the app", async () => {
|
|
151
165
|
const state: Record<string, FakeMachine[] | "boom"> = {
|
|
152
166
|
mine: [machine("web", MINE), machine("worker", MINE)],
|
|
153
167
|
};
|
|
@@ -176,6 +190,11 @@ describe("executeTeardown — machines first, apps last", () => {
|
|
|
176
190
|
`DELETE ${ENDPOINT}/v1/apps/mine/machines/m-worker`,
|
|
177
191
|
`DELETE ${ENDPOINT}/v1/apps/mine`,
|
|
178
192
|
]);
|
|
193
|
+
// Started machines are stopped before the destroy: flaps refuses a bare DELETE otherwise (#3115).
|
|
194
|
+
expect(log.filter((l) => l.endsWith("/stop"))).toEqual([
|
|
195
|
+
`POST ${ENDPOINT}/v1/apps/mine/machines/m-web/stop`,
|
|
196
|
+
`POST ${ENDPOINT}/v1/apps/mine/machines/m-worker/stop`,
|
|
197
|
+
]);
|
|
179
198
|
expect(state.mine).toBeUndefined();
|
|
180
199
|
});
|
|
181
200
|
|
package/src/teardown.ts
CHANGED
|
@@ -220,7 +220,7 @@ async function destroyMachineCandidate(
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
try {
|
|
223
|
-
await destroyMachine(ctx, app, live.id, http, undefined, wait ?? {});
|
|
223
|
+
await destroyMachine(ctx, app, live.id, http, undefined, { ...(wait ?? {}), state: live.state, name: live.name });
|
|
224
224
|
} catch (err) {
|
|
225
225
|
return { ...base, outcome: "failed", detail: err instanceof Error ? err.message : String(err) };
|
|
226
226
|
}
|