@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.
Files changed (70) hide show
  1. package/dist/codegen/docs.d.ts.map +1 -1
  2. package/dist/codegen/naming.d.ts.map +1 -1
  3. package/dist/codegen/package.d.ts +1 -1
  4. package/dist/components/capability-plugin.d.ts.map +1 -1
  5. package/dist/composites/fly-deploy.d.ts +2 -0
  6. package/dist/composites/fly-deploy.d.ts.map +1 -1
  7. package/dist/composites/fly-site.d.ts +2 -2
  8. package/dist/generated/index.d.ts +41 -41
  9. package/dist/generated/index.d.ts.map +1 -1
  10. package/dist/import/generator.d.ts.map +1 -1
  11. package/dist/import/parser.d.ts.map +1 -1
  12. package/dist/integrity.json +5 -5
  13. package/dist/manifest.json +1 -1
  14. package/dist/okf/types/Secret.md +1 -0
  15. package/dist/op/activities/flaps.d.ts.map +1 -1
  16. package/dist/op/activities/fly-apply.d.ts +87 -8
  17. package/dist/op/activities/fly-apply.d.ts.map +1 -1
  18. package/dist/op/activities/index.d.ts +2 -2
  19. package/dist/op/activities/index.d.ts.map +1 -1
  20. package/dist/op/activities/machines-contract.d.ts.map +1 -1
  21. package/dist/op/activities/machines-fake.d.ts +10 -1
  22. package/dist/op/activities/machines-fake.d.ts.map +1 -1
  23. package/dist/op/activities/machines-local.d.ts.map +1 -1
  24. package/dist/op/activities/progress.d.ts +9 -0
  25. package/dist/op/activities/progress.d.ts.map +1 -0
  26. package/dist/op/activities/sprite-config.d.ts.map +1 -1
  27. package/dist/op/activities/sprite-fs.d.ts.map +1 -1
  28. package/dist/op/activities/sprite-service-converge.d.ts.map +1 -1
  29. package/dist/op/activities/sprite-services.d.ts.map +1 -1
  30. package/dist/op/activities/sprites-emulator.d.ts.map +1 -1
  31. package/dist/op/activities/sprites.d.ts.map +1 -1
  32. package/dist/op/activity-contracts.d.ts +10 -10
  33. package/dist/op/builders.d.ts.map +1 -1
  34. package/dist/release-metadata.d.ts.map +1 -1
  35. package/dist/serializer.d.ts.map +1 -1
  36. package/dist/skills/chant-fly-ops.md +8 -0
  37. package/dist/skills/chant-fly-patterns.md +1 -1
  38. package/dist/spec/parse.d.ts.map +1 -1
  39. package/dist/types/index.d.ts +2 -1
  40. package/package.json +3 -3
  41. package/src/codegen/docs.ts +16 -1
  42. package/src/composites/fly-deploy.ts +2 -0
  43. package/src/describe-resources.test.ts +18 -1
  44. package/src/describe-resources.ts +1 -1
  45. package/src/generated/index.d.ts +2 -1
  46. package/src/generated/index.ts +1 -1
  47. package/src/op/activities/fly-apply.integration.test.ts +12 -5
  48. package/src/op/activities/fly-apply.test.ts +483 -2
  49. package/src/op/activities/fly-apply.ts +229 -45
  50. package/src/op/activities/index.ts +27 -1
  51. package/src/op/activities/machines-contract.docker.integration.test.ts +22 -1
  52. package/src/op/activities/machines-contract.ts +5 -1
  53. package/src/op/activities/machines-fake.ts +69 -2
  54. package/src/op/activities/machines-local-cli.ts +3 -0
  55. package/src/op/activities/machines-local.test.ts +48 -12
  56. package/src/op/activities/machines-local.ts +28 -5
  57. package/src/op/activities/progress.ts +10 -0
  58. package/src/op/activities/sprite-config.ts +10 -9
  59. package/src/op/activities/sprite-fs.ts +3 -2
  60. package/src/op/activities/sprite-progress.test.ts +52 -0
  61. package/src/op/activities/sprite-service-converge.ts +2 -1
  62. package/src/op/activities/sprite-services.ts +5 -4
  63. package/src/op/activities/sprites.ts +6 -5
  64. package/src/serializer.test.ts +17 -2
  65. package/src/serializer.ts +16 -7
  66. package/src/skills/chant-fly-ops.md +8 -0
  67. package/src/skills/chant-fly-patterns.md +1 -1
  68. package/src/spec/parse.ts +25 -2
  69. package/src/teardown.test.ts +22 -3
  70. 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
- requests[name] = {
265
- endpoint: `/v1/apps/${app}/secrets/${encodeURIComponent(secretName)}`,
266
- method: "POST",
267
- body: props.value !== undefined ? { value: props.value } : {},
268
- applyOnly: true,
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 certificate is created if absent, idempotent by hostname. A re-apply for a hostname that already has a certificate is a no-op.
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
- { typeName: `Fly::${SERVICE}::Secret`, request: "SetAppSecretRequest", response: "AppSecret" },
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 in reqProps) continue;
147
+ if (properties.some((p) => p.name === name)) continue;
125
148
  attributes.push({ name, tsType: resolve(prop) });
126
149
  }
127
150
 
@@ -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)) apps[m[1]] = held.filter((mm) => mm.id !== m![2]);
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 (lease → destroy → wait), then deletes the app", async () => {
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
  }