@b00dea/cockpit 0.1.0 → 0.1.1
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/CHANGELOG.md +99 -0
- package/README.md +3 -0
- package/cockpit.schema.json +73 -3
- package/dist/autonomy.d.ts +3 -2
- package/dist/autonomy.js +30 -13
- package/dist/browser/commands.d.ts +57 -2
- package/dist/browser/commands.js +114 -10
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +4 -1
- package/dist/browser/surfaces.d.ts +7 -7
- package/dist/browser/surfaces.js +5 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-main.d.ts +40 -3
- package/dist/cli-main.js +241 -62
- package/dist/command-table.d.ts +19 -11
- package/dist/command-table.js +190 -38
- package/dist/config/load.d.ts +16 -4
- package/dist/config/load.js +25 -3
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/diagnostics.js +10 -1
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.js +6 -2
- package/dist/environment/application/doctor.d.ts +12 -7
- package/dist/environment/application/doctor.js +20 -8
- package/dist/environment/application/guard.d.ts +12 -6
- package/dist/environment/application/guard.js +87 -32
- package/dist/environment/application/health.d.ts +14 -5
- package/dist/environment/application/health.js +8 -2
- package/dist/environment/application/lifecycle-context.d.ts +10 -3
- package/dist/environment/application/lifecycle-state.js +4 -3
- package/dist/environment/application/lifecycle.d.ts +2 -1
- package/dist/environment/application/lifecycle.js +11 -3
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +10 -10
- package/dist/environment/claims.js +3 -11
- package/dist/environment/compose-topology.d.ts +15 -7
- package/dist/environment/compose-topology.js +100 -23
- package/dist/environment/contract.d.ts +42 -9
- package/dist/environment/contract.js +33 -0
- package/dist/environment/executable.d.ts +12 -0
- package/dist/environment/executable.js +46 -23
- package/dist/environment/generated-application/check.js +2 -0
- package/dist/environment/generated-application/prepare.js +2 -0
- package/dist/environment/generated-application/shared.d.ts +10 -2
- package/dist/environment/generated-application/shared.js +6 -1
- package/dist/environment/generated-application/surface-outputs.js +23 -12
- package/dist/environment/generated-application/surfaces.js +16 -11
- package/dist/environment/generated-application/sync.js +19 -4
- package/dist/environment/git-observation.d.ts +25 -6
- package/dist/environment/git-observation.js +37 -28
- package/dist/environment/health-composition.d.ts +3 -2
- package/dist/environment/health-composition.js +19 -2
- package/dist/environment/init-application.d.ts +39 -14
- package/dist/environment/init-application.js +39 -10
- package/dist/environment/lifecycle-composition.d.ts +4 -2
- package/dist/environment/lifecycle-composition.js +84 -17
- package/dist/environment/lifecycle-stage.d.ts +8 -3
- package/dist/environment/lifecycle-stage.js +11 -1
- package/dist/environment/logs.d.ts +1 -1
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +1 -1
- package/dist/environment/observations.js +2 -0
- package/dist/environment/operator-application.d.ts +35 -6
- package/dist/environment/operator-application.js +42 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +8 -3
- package/dist/environment/read-composition.js +53 -8
- package/dist/environment/readiness-composition.d.ts +20 -1
- package/dist/environment/readiness-composition.js +23 -2
- package/dist/environment/reclaim.d.ts +56 -19
- package/dist/environment/reclaim.js +147 -42
- package/dist/environment/runtime.d.ts +64 -18
- package/dist/environment/runtime.js +73 -3
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +48 -9
- package/dist/environment/state.js +142 -2
- package/dist/environment/tool-registration.js +3 -7
- package/dist/environment-exec/execute.d.ts +6 -3
- package/dist/environment-exec/execute.js +4 -0
- package/dist/fix-catalogue.d.ts +1 -1
- package/dist/generated-environment/adopt.js +4 -8
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +20 -3
- package/dist/guards/evaluator.js +39 -7
- package/dist/guards/local-identity.d.ts +6 -2
- package/dist/guards/local-identity.js +10 -4
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +1 -1
- package/dist/guards/policy-pin.js +14 -12
- package/dist/init/command.d.ts +4 -3
- package/dist/init/command.js +36 -16
- package/dist/init/derive.d.ts +8 -6
- package/dist/init/derive.js +140 -36
- package/dist/init/draft.d.ts +13 -11
- package/dist/init/draft.js +54 -31
- package/dist/init/render.d.ts +18 -6
- package/dist/init/render.js +31 -9
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema.d.ts +42 -75
- package/dist/schema.js +62 -11
- package/dist/surfaces/harness-registry.d.ts +80 -0
- package/dist/surfaces/harness-registry.js +29 -0
- package/dist/surfaces/mcp.d.ts +67 -17
- package/dist/surfaces/mcp.js +194 -31
- package/dist/surfaces/projection.d.ts +7 -7
- package/dist/surfaces/projection.js +41 -6
- package/dist/surfaces/render.d.ts +5 -44
- package/dist/surfaces/render.js +5 -2
- package/dist/team-metadata.js +2 -1
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +4 -97
- package/dist/work-orders/application.js +94 -254
- package/dist/work-orders/composition.d.ts +35 -58
- package/dist/work-orders/composition.js +216 -24
- package/dist/work-orders/contract.d.ts +22 -6
- package/dist/work-orders/contract.js +34 -16
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +54 -5
- package/dist/work-orders/store.js +118 -24
- package/docs/README.md +6 -3
- package/docs/capability-matrix.md +17 -9
- package/docs/cli-reference.md +209 -1
- package/docs/command-list.md +12 -1
- package/docs/concepts.md +6 -4
- package/docs/configuration-essentials.md +7 -2
- package/docs/configuration-reference.md +18 -3
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +194 -6
- package/docs/faq.md +5 -3
- package/docs/getting-started.md +109 -25
- package/docs/harness-surfaces.md +69 -2
- package/docs/safety-and-autonomy.md +56 -12
- package/docs/troubleshooting.md +29 -8
- package/docs/tutorial.md +3 -2
- package/docs/work-and-browser.md +180 -14
- package/libexec/browser-operation.cjs +86 -2
- package/package.json +2 -2
- package/surfaces/skills/cockpit-browser.skill.md +7 -2
- package/surfaces/skills/cockpit-explain.skill.md +1 -1
- package/surfaces/skills/cockpit-fix.skill.md +1 -1
- package/surfaces/skills/cockpit-setup.skill.md +3 -3
- package/surfaces/skills/cockpit-work.skill.md +1 -1
- package/surfaces/skills/procedures/explain.md +9 -4
- package/surfaces/skills/procedures/fix.md +25 -11
- package/surfaces/skills/procedures/setup.md +113 -40
- package/surfaces/skills/procedures/work.md +45 -16
|
@@ -265,39 +265,43 @@ function selectionRow(bytes) {
|
|
|
265
265
|
// Ownership is label-based: a resource without this stack's label is never removed, so listing the
|
|
266
266
|
// rest of a shared daemon only cost time. Listing and inspecting every container, image and volume
|
|
267
267
|
// on a daemon with a hundred unrelated containers made each `down` observation take seconds, and
|
|
268
|
-
// `container ls --size` alone ran past the step timeout.
|
|
269
|
-
// refuses when a declared volume name belongs to something this stack does not own, and an
|
|
268
|
+
// `container ls --size` alone ran past the step timeout. A stack's volumes stay unfiltered because a
|
|
269
|
+
// reset refuses when a declared volume name belongs to something this stack does not own, and an
|
|
270
270
|
// unlabelled volume of that name would otherwise be invisible; their inspection is still scoped.
|
|
271
|
-
|
|
271
|
+
// The machine-wide listing has no such name to check, so it lists cockpit-labelled volumes alone.
|
|
272
|
+
function runtimeCommand(kind, selection, owner) {
|
|
272
273
|
if (kind === 'build-cache')
|
|
273
274
|
return ['--host', selection.endpoint, 'builder', 'du', '--format', 'json'];
|
|
274
|
-
const
|
|
275
|
-
const owned = ['--filter', `label=${REQUIRED_LABELS.stack}=${stackId}`];
|
|
275
|
+
const owned = ['--filter', owner === null ? `label=${REQUIRED_LABELS.stack}` : `label=${REQUIRED_LABELS.stack}=${owner.stackId}`];
|
|
276
276
|
const command = kind === 'container' ? ['container', 'ls', '--all', '--no-trunc', ...owned]
|
|
277
|
-
: kind === 'volume' ? ['volume', 'ls'
|
|
278
|
-
|
|
277
|
+
: kind === 'volume' ? ['volume', 'ls', ...owner === null ? owned : []]
|
|
278
|
+
: kind === 'image' ? ['image', 'ls', '--all', '--no-trunc', ...owned] : [kind, 'ls', '--no-trunc', ...owned];
|
|
279
|
+
return ['--host', selection.endpoint, ...command, '--format', discoveryFormat(kind, owner)];
|
|
279
280
|
}
|
|
280
|
-
function discoveryFormat(kind) {
|
|
281
|
+
function discoveryFormat(kind, owner) {
|
|
281
282
|
const id = kind === 'volume' ? '{{json .Name}}' : '{{json .ID}}';
|
|
282
283
|
const name = kind === 'container' ? '{{json .Names}}' : kind === 'image' ? '{{json .Repository}}' : '{{json .Name}}';
|
|
283
284
|
const label = (key) => `{{json (.Label "${key}")}}`;
|
|
285
|
+
const labels = kind === 'image' ? '{}' : `{${(owner === null ? MACHINE_LABELS : listedLabels(kind)).map((key) => `"${key}":${label(key)}`).join(',')}}`;
|
|
286
|
+
// The machine-wide listing attributes each resource by its labels alone and reports whether a container runs.
|
|
287
|
+
if (owner === null)
|
|
288
|
+
return `{"id":${id},"name":${name},"state":${kind === 'container' ? '{{json .State}}' : '""'},"labels":${labels}}`;
|
|
284
289
|
// Without `--size` a container prints a misleading "0B (virtual ...)", so its size is unmeasured.
|
|
285
290
|
const bytes = kind === 'network' || kind === 'container' ? 'null' : '{{json .Size}}';
|
|
286
|
-
const labels = kind === 'image' ? '{}' : `{${listedLabels(kind).map((key) => `"${key}":${label(key)}`).join(',')}}`;
|
|
287
291
|
return `{"id":${id},"name":${name},"labels":${labels},"referenced":false,"bytes":${bytes}}`;
|
|
288
292
|
}
|
|
289
293
|
function listedLabels(kind) {
|
|
290
294
|
return [...Object.values(REQUIRED_LABELS), ...kind === 'volume' ? Object.values(COMPOSE_LABELS) : []];
|
|
291
295
|
}
|
|
292
|
-
|
|
296
|
+
/** The labels Docker printed, refused when any is not `allowed` or its value is too long or holds a NUL; empty values are dropped. */
|
|
297
|
+
function dockerLabels(value, allowed, maxLength = 512) {
|
|
293
298
|
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
294
299
|
return undefined;
|
|
295
300
|
const labels = {};
|
|
296
|
-
|
|
297
|
-
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
301
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
298
302
|
return undefined;
|
|
299
303
|
for (const [key, labelValue] of Object.entries(value)) {
|
|
300
|
-
if (typeof labelValue !== 'string' || labelValue.length >
|
|
304
|
+
if (typeof labelValue !== 'string' || labelValue.length > maxLength || labelValue.includes('\0'))
|
|
301
305
|
return undefined;
|
|
302
306
|
if (labelValue !== '')
|
|
303
307
|
labels[key] = labelValue;
|
|
@@ -345,7 +349,7 @@ function dockerResource(value, kind) {
|
|
|
345
349
|
const name = row.name;
|
|
346
350
|
const labels = kind === 'image'
|
|
347
351
|
? row.labels !== null && typeof row.labels === 'object' && !Array.isArray(row.labels) && exactKeys(row.labels, []) ? {} : undefined
|
|
348
|
-
: dockerLabels(row.labels, kind);
|
|
352
|
+
: dockerLabels(row.labels, listedLabels(kind));
|
|
349
353
|
const bytes = dockerBytes(row.bytes, kind === 'container');
|
|
350
354
|
const referenced = kind === 'image' ? typeof row.referenced === 'boolean' ? row.referenced : undefined : row.referenced;
|
|
351
355
|
return typeof id === 'string' && typeof name === 'string' && labels !== undefined && bytes !== undefined && typeof referenced === 'boolean'
|
|
@@ -500,17 +504,8 @@ async function selectDocker(input, executable, env, execute) {
|
|
|
500
504
|
return undefined;
|
|
501
505
|
return { value: { context: input.context, endpoint: row.endpoint }, startedAt: outcome.receipt.startedAt, finishedAt: outcome.receipt.finishedAt };
|
|
502
506
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
stackId: runtime.stackId, cockpitId: runtime.cockpitId, resourceKind: kind,
|
|
506
|
-
dockerContext: runtime.selection.context, dockerEndpoint: runtime.selection.endpoint,
|
|
507
|
-
}, [], OUTPUT_LIMIT);
|
|
508
|
-
if (step === undefined)
|
|
509
|
-
return undefined;
|
|
510
|
-
const outcome = cleanOutput(await execute(step));
|
|
511
|
-
const text = outcome === undefined ? undefined : utf8(outcome.stdout);
|
|
512
|
-
if (outcome === undefined || text === undefined)
|
|
513
|
-
return undefined;
|
|
507
|
+
/** One stack's listed resources of one kind, or nothing when any row cannot be read. */
|
|
508
|
+
function stackRows(text, kind) {
|
|
514
509
|
const resources = [];
|
|
515
510
|
for (const line of text.split('\n')) {
|
|
516
511
|
if (line.length === 0)
|
|
@@ -538,7 +533,37 @@ async function listResources(runtime, kind, execute) {
|
|
|
538
533
|
}
|
|
539
534
|
if (resources.length > MAX_RESOURCES || new Set(resources.map(({ id }) => id)).size !== resources.length)
|
|
540
535
|
return undefined;
|
|
541
|
-
return
|
|
536
|
+
return resources.sort((a, b) => lexical(a.id, b.id));
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Lists every kind at once on the context's daemon, reading the selection before and after so the
|
|
540
|
+
* listings describe one daemon. A step that fails, writes to stderr or prints a row `parse` rejects
|
|
541
|
+
* fails the whole listing closed.
|
|
542
|
+
*/
|
|
543
|
+
async function listOnSelectedDaemon(input, executable, env, execute, owner, kinds, parse) {
|
|
544
|
+
const planned = await selectDocker(input, executable, env, execute);
|
|
545
|
+
if (planned === undefined)
|
|
546
|
+
return undefined;
|
|
547
|
+
const selection = planned.value;
|
|
548
|
+
const listed = await Promise.all(kinds.map(async (kind) => {
|
|
549
|
+
const step = exactStep(`${owner === null ? 'stacks' : 'runtime'}:discover:${kind}`, [executable.canonicalPath, ...runtimeCommand(kind, selection, owner)], executable, input.cwd, env, input.timeoutMs, [], {
|
|
550
|
+
...owner === null ? {} : { stackId: owner.stackId, cockpitId: owner.cockpitId },
|
|
551
|
+
resourceKind: kind, dockerContext: selection.context, dockerEndpoint: selection.endpoint,
|
|
552
|
+
}, [], OUTPUT_LIMIT);
|
|
553
|
+
const outcome = step === undefined ? undefined : cleanOutput(await execute(step));
|
|
554
|
+
const text = outcome === undefined ? undefined : utf8(outcome.stdout);
|
|
555
|
+
const rows = text === undefined ? undefined : parse(text, kind);
|
|
556
|
+
return rows === undefined ? undefined : { value: rows, startedAt: outcome.receipt.startedAt, finishedAt: outcome.receipt.finishedAt };
|
|
557
|
+
}));
|
|
558
|
+
if (listed.some((value) => value === undefined))
|
|
559
|
+
return undefined;
|
|
560
|
+
const lists = listed;
|
|
561
|
+
const listedAt = lists.map(({ finishedAt }) => finishedAt).sort(lexical).at(-1);
|
|
562
|
+
const reselected = await selectDocker(input, executable, env, execute);
|
|
563
|
+
if (reselected === undefined || !sameSelection(reselected.value, selection)
|
|
564
|
+
|| Date.parse(reselected.startedAt) < Date.parse(listedAt))
|
|
565
|
+
return undefined;
|
|
566
|
+
return { selection, lists, listedAt, reselected };
|
|
542
567
|
}
|
|
543
568
|
/** Runs one kind's inspection steps; an oversized object names its chunk instead of failing anonymously. */
|
|
544
569
|
async function runInspections(steps, kind, execute) {
|
|
@@ -569,7 +594,7 @@ async function inspectImages(runtime, images, execute) {
|
|
|
569
594
|
return undefined;
|
|
570
595
|
for (const [position, record] of rows.entries()) {
|
|
571
596
|
const resourceId = ids[position];
|
|
572
|
-
const observedLabels = exactKeys(record, ['id', 'labels']) ? dockerLabels(record.labels) : undefined;
|
|
597
|
+
const observedLabels = exactKeys(record, ['id', 'labels']) ? dockerLabels(record.labels, listedLabels()) : undefined;
|
|
573
598
|
if (record.id !== resourceId || observedLabels === undefined || labels.has(resourceId))
|
|
574
599
|
return undefined;
|
|
575
600
|
labels.set(resourceId, observedLabels);
|
|
@@ -678,23 +703,14 @@ function sameSelection(left, right) {
|
|
|
678
703
|
async function discoverReclaimRound(input, executable, env, execute) {
|
|
679
704
|
// The selection is read before and after the listings, and again after the inspections, so the
|
|
680
705
|
// context's endpoint cannot have moved to another daemon while this stack was being read.
|
|
681
|
-
const
|
|
682
|
-
if (
|
|
706
|
+
const listed = await listOnSelectedDaemon(input, executable, env, execute, { stackId: input.stackId, cockpitId: input.cockpitId }, DISCOVERY_KINDS, stackRows);
|
|
707
|
+
if (listed === undefined || Date.parse(listed.reselected.finishedAt) - Date.parse(listed.listedAt) > 30_000)
|
|
683
708
|
return undefined;
|
|
709
|
+
const { lists, listedAt, selection } = listed;
|
|
684
710
|
const runtime = {
|
|
685
711
|
executable, cwd: input.cwd, env, timeoutMs: input.timeoutMs,
|
|
686
|
-
stackId: input.stackId, cockpitId: input.cockpitId, selection
|
|
712
|
+
stackId: input.stackId, cockpitId: input.cockpitId, selection,
|
|
687
713
|
};
|
|
688
|
-
const listed = await Promise.all(DISCOVERY_KINDS.map((kind) => listResources(runtime, kind, execute)));
|
|
689
|
-
if (listed.some((value) => value === undefined))
|
|
690
|
-
return undefined;
|
|
691
|
-
const lists = listed;
|
|
692
|
-
const listedAt = lists.map(({ finishedAt }) => finishedAt).sort(lexical).at(-1);
|
|
693
|
-
const current = await selectDocker(input, executable, env, execute);
|
|
694
|
-
if (current === undefined || !sameSelection(current.value, planned.value)
|
|
695
|
-
|| Date.parse(current.startedAt) < Date.parse(listedAt)
|
|
696
|
-
|| Date.parse(current.finishedAt) - Date.parse(listedAt) > 30_000)
|
|
697
|
-
return undefined;
|
|
698
714
|
const [containers, networks, volumes, images, buildCache] = lists.map(({ value }) => value);
|
|
699
715
|
const inspect = boundedExecutor(execute, MAX_CONCURRENT_INSPECTIONS);
|
|
700
716
|
const inspected = await Promise.all([
|
|
@@ -718,10 +734,10 @@ async function discoverReclaimRound(input, executable, env, execute) {
|
|
|
718
734
|
if (Date.parse(finishedAt) - Date.parse(startedAt) > 30_000)
|
|
719
735
|
return undefined;
|
|
720
736
|
const confirmed = await selectDocker(input, executable, env, execute);
|
|
721
|
-
if (confirmed === undefined || !sameSelection(confirmed.value,
|
|
737
|
+
if (confirmed === undefined || !sameSelection(confirmed.value, selection)
|
|
722
738
|
|| Date.parse(confirmed.startedAt) < Date.parse(finishedAt))
|
|
723
739
|
return undefined;
|
|
724
|
-
return { observedAt: finishedAt, producer: 'runtime-resource-discovery', resources, startedAt, stackId: input.stackId, selection
|
|
740
|
+
return { observedAt: finishedAt, producer: 'runtime-resource-discovery', resources, startedAt, stackId: input.stackId, selection };
|
|
725
741
|
}
|
|
726
742
|
/**
|
|
727
743
|
* Lists and inspects this stack's runtime resources on the selected Docker context. Discovery
|
|
@@ -741,6 +757,95 @@ export async function discoverReclaim(input, execute) {
|
|
|
741
757
|
return await discoverReclaimRound(input, executable, env, execute)
|
|
742
758
|
?? await discoverReclaimRound(input, executable, env, execute);
|
|
743
759
|
}
|
|
760
|
+
const MACHINE_KINDS = ['container', 'network', 'volume'];
|
|
761
|
+
const COMPOSE_WORKING_DIR = 'com.docker.compose.project.working_dir';
|
|
762
|
+
const MACHINE_LABELS = [...Object.values(REQUIRED_LABELS), COMPOSE_LABELS.project, COMPOSE_WORKING_DIR];
|
|
763
|
+
const RUNNING_STATES = new Set(['running', 'restarting', 'paused']);
|
|
764
|
+
// Each removal names its resources in argv, which a step bounds; a chunk also keeps the ids Docker
|
|
765
|
+
// echoes on stdout within the step's default output cap.
|
|
766
|
+
const REMOVAL_CHUNK = 128;
|
|
767
|
+
function machineRows(text, kind) {
|
|
768
|
+
const resources = [];
|
|
769
|
+
let unattributed = 0;
|
|
770
|
+
for (const line of text.split('\n')) {
|
|
771
|
+
if (line.length === 0)
|
|
772
|
+
continue;
|
|
773
|
+
// Unparseable docker output is unproven; the discovery fails closed.
|
|
774
|
+
const row = tryParseJson(line);
|
|
775
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row) || !exactKeys(row, ['id', 'name', 'state', 'labels']))
|
|
776
|
+
return undefined;
|
|
777
|
+
const { id, name, state, labels: listed } = row;
|
|
778
|
+
// A working directory is a path, so its label may run far past a stack's own label bound.
|
|
779
|
+
const labels = listed !== null && typeof listed === 'object' && exactKeys(listed, MACHINE_LABELS) ? dockerLabels(listed, MACHINE_LABELS, 4_096) : undefined;
|
|
780
|
+
if (typeof id !== 'string' || !RESOURCE_ID.test(id) || typeof name !== 'string' || name.length === 0 || name.length > 512
|
|
781
|
+
|| typeof state !== 'string' || state.length > 64 || labels === undefined)
|
|
782
|
+
return undefined;
|
|
783
|
+
const stack = labels[REQUIRED_LABELS.stack] ?? '';
|
|
784
|
+
const owner = labels[REQUIRED_LABELS.owner] ?? '';
|
|
785
|
+
if (!STACK_ID.test(stack) || !COCKPIT_ID.test(owner) || !SHA256.test(labels[REQUIRED_LABELS.config] ?? '')) {
|
|
786
|
+
unattributed += 1;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const workingDir = labels[COMPOSE_WORKING_DIR];
|
|
790
|
+
resources.push({
|
|
791
|
+
kind, id, name, cockpitId: owner, stackId: stack,
|
|
792
|
+
running: kind === 'container' && RUNNING_STATES.has(state),
|
|
793
|
+
composeProject: labels[COMPOSE_LABELS.project] ?? null,
|
|
794
|
+
workingDir: kind === 'container' && workingDir !== undefined && isAbsolute(workingDir) ? normalize(workingDir) : null,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return { resources, unattributed };
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Lists every container, network and volume on the selected daemon that carries cockpit's stack
|
|
801
|
+
* label, whichever cockpit or checkout started it, for the machine-wide stack inventory. Ownership
|
|
802
|
+
* comes from the labels alone; nothing is attributed by its name. The selection is read before and
|
|
803
|
+
* after the listings, as `discoverReclaim` does, so the listings describe one daemon.
|
|
804
|
+
*/
|
|
805
|
+
export async function discoverMachineStacks(input, execute) {
|
|
806
|
+
const executable = resolvedDockerExecutable(input.executable);
|
|
807
|
+
const env = safeEnvironment(input.env);
|
|
808
|
+
if (executable === undefined || env === undefined || !DOCKER_SELECTOR.test(input.context)
|
|
809
|
+
|| input.host !== undefined && (admitLocalDockerEndpoint(input.host) === undefined || input.context !== 'default')
|
|
810
|
+
|| !validExecutionInput(executable, input.cwd, input.env, input.timeoutMs))
|
|
811
|
+
return undefined;
|
|
812
|
+
const listed = await listOnSelectedDaemon(input, executable, env, execute, null, MACHINE_KINDS, machineRows);
|
|
813
|
+
if (listed === undefined)
|
|
814
|
+
return undefined;
|
|
815
|
+
const resources = listed.lists.flatMap(({ value }) => value.resources);
|
|
816
|
+
if (resources.length > MAX_RESOURCES)
|
|
817
|
+
return undefined;
|
|
818
|
+
return {
|
|
819
|
+
selection: listed.selection, observedAt: listed.listedAt,
|
|
820
|
+
resources: resources.sort((left, right) => lexical(`${left.kind}:${left.id}`, `${right.kind}:${right.id}`)),
|
|
821
|
+
unattributed: listed.lists.reduce((sum, { value }) => sum + value.unattributed, 0),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* The Docker removals prune runs for one kind of one orphaned stack's resources, as the machine
|
|
826
|
+
* discovery attributed them: one client per chunk rather than one per resource. Undefined when the
|
|
827
|
+
* resources are not all of one kind and one stack, or a step cannot be built.
|
|
828
|
+
*/
|
|
829
|
+
export function machineStackRemovalSteps(input, selection, resources) {
|
|
830
|
+
const executable = resolvedDockerExecutable(input.executable);
|
|
831
|
+
const env = safeEnvironment(input.env);
|
|
832
|
+
const [first] = resources;
|
|
833
|
+
if (first === undefined || executable === undefined || env === undefined || admitLocalDockerEndpoint(selection.endpoint) === undefined
|
|
834
|
+
|| resources.some(({ kind, stackId, cockpitId, id }) => kind !== first.kind || stackId !== first.stackId || cockpitId !== first.cockpitId || !RESOURCE_ID.test(id))
|
|
835
|
+
|| !validExecutionInput(executable, input.cwd, input.env, input.timeoutMs))
|
|
836
|
+
return undefined;
|
|
837
|
+
const command = first.kind === 'container' ? ['container', 'rm', '--force'] : [first.kind, 'rm'];
|
|
838
|
+
const removals = [];
|
|
839
|
+
for (let start = 0; start < resources.length; start += REMOVAL_CHUNK) {
|
|
840
|
+
const chunk = resources.slice(start, start + REMOVAL_CHUNK);
|
|
841
|
+
const ids = chunk.map(({ id }) => id);
|
|
842
|
+
const step = exactStep(`stacks:prune:${first.kind}:${digestValue(ids).slice(0, 16)}`, [executable.canonicalPath, '--host', selection.endpoint, ...command, '--', ...ids], executable, input.cwd, env, input.timeoutMs, [], { stackId: first.stackId, cockpitId: first.cockpitId, resourceKind: first.kind, resourceCount: String(ids.length) });
|
|
843
|
+
if (step === undefined)
|
|
844
|
+
return undefined;
|
|
845
|
+
removals.push({ step, resources: chunk });
|
|
846
|
+
}
|
|
847
|
+
return removals;
|
|
848
|
+
}
|
|
744
849
|
function refusal(code, message, target, fix) {
|
|
745
850
|
return { kind: 'refusal', diagnostics: [diagnostic(code, message, target, fix)] };
|
|
746
851
|
}
|
|
@@ -86,8 +86,8 @@ export declare function observeRuntimeExecution(input: {
|
|
|
86
86
|
readonly plan: ExecutionPlan;
|
|
87
87
|
readonly outcome: ExecuteProbeOutcome;
|
|
88
88
|
}): readonly EnvironmentObservation[];
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
type RuntimeOperation = 'validate' | 'up' | 'down';
|
|
90
|
+
interface RuntimeAdapterInput {
|
|
91
91
|
runtime: {
|
|
92
92
|
name: 'docker';
|
|
93
93
|
version: string;
|
|
@@ -108,7 +108,7 @@ export interface RuntimeAdapterInput {
|
|
|
108
108
|
coldStartLimitMillis?: number;
|
|
109
109
|
expectedMountSources?: readonly string[];
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
interface RuntimePlanResult {
|
|
112
112
|
runtime: 'docker';
|
|
113
113
|
operation: RuntimeOperation;
|
|
114
114
|
projectName: string;
|
|
@@ -120,7 +120,7 @@ export interface ComposeArtifact {
|
|
|
120
120
|
sha256: string;
|
|
121
121
|
}
|
|
122
122
|
/** Each `[[sibling]] var` and the absolute sibling directory it names. */
|
|
123
|
-
|
|
123
|
+
type SiblingDirectoryVariables = Readonly<Record<string, string>>;
|
|
124
124
|
export type RuntimeAdapterAuthority =
|
|
125
125
|
/** A stack's own Compose files, which interpolate the lease, the sibling directories and their own defaults. */
|
|
126
126
|
Readonly<{
|
|
@@ -133,7 +133,7 @@ Readonly<{
|
|
|
133
133
|
/** The numeric core of a version Docker or Compose printed, or undefined when it has none. */
|
|
134
134
|
export declare function observedVersionCore(value: string): string | undefined;
|
|
135
135
|
export declare function classifyResolvedRuntimeLiterals(values: readonly string[]): 'resolved' | 'unresolved' | 'unsafe';
|
|
136
|
-
|
|
136
|
+
type MachineFloorPlanInput = {
|
|
137
137
|
readonly runtimeExecutable: ResolvedExecutable;
|
|
138
138
|
readonly diskExecutable: ResolvedExecutable;
|
|
139
139
|
readonly cwd: string;
|
|
@@ -146,7 +146,7 @@ export type MachineFloorPlanInput = {
|
|
|
146
146
|
readonly timeoutMs: number;
|
|
147
147
|
readonly authority?: MachineFloorAuthority;
|
|
148
148
|
};
|
|
149
|
-
|
|
149
|
+
interface MachineFloorThresholds {
|
|
150
150
|
readonly memoryBytes: number;
|
|
151
151
|
readonly cpuMillis: number;
|
|
152
152
|
readonly freeDiskBytes: number;
|
|
@@ -172,14 +172,14 @@ export declare function issueMachineFloorAuthority(input: {
|
|
|
172
172
|
checkout: ConsumedSetupCheckoutObservation;
|
|
173
173
|
}> | undefined;
|
|
174
174
|
export declare function isMachineFloorEvidence(value: unknown): value is MachineFloorEvidence;
|
|
175
|
-
|
|
175
|
+
interface DockerRuntimeObservationInput {
|
|
176
176
|
readonly executable: ResolvedExecutable;
|
|
177
177
|
readonly cwd: string;
|
|
178
178
|
readonly env: Readonly<Record<string, string>>;
|
|
179
179
|
readonly timeoutMs: number;
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
type RuntimeIdentity = 'docker-desktop' | 'orbstack' | 'colima' | 'rancher-desktop' | 'docker-engine';
|
|
182
|
+
interface DockerInfoIdentityInput {
|
|
183
183
|
readonly operatingSystem: string;
|
|
184
184
|
readonly name: string;
|
|
185
185
|
readonly osType: string;
|
|
@@ -196,7 +196,7 @@ export interface DockerInfoIdentityInput {
|
|
|
196
196
|
* been probed on a host here.
|
|
197
197
|
*/
|
|
198
198
|
export declare function runtimeIdentity(input: DockerInfoIdentityInput): RuntimeIdentity | null;
|
|
199
|
-
|
|
199
|
+
type DockerRuntimeObservation = Readonly<{
|
|
200
200
|
kind: 'complete';
|
|
201
201
|
runtime: {
|
|
202
202
|
readonly name: 'docker';
|
|
@@ -223,7 +223,7 @@ export declare function issueRuntimeMeasurementProbeAuthority(observation: Docke
|
|
|
223
223
|
export declare function runtimeMeasurementProbeObservation(authority: RuntimeMeasurementProbeAuthority, executableSha256: string): Extract<DockerRuntimeObservation, {
|
|
224
224
|
readonly kind: 'complete';
|
|
225
225
|
}> | undefined;
|
|
226
|
-
|
|
226
|
+
type RuntimeMeasurementAdmission = Readonly<{
|
|
227
227
|
kind: 'admitted';
|
|
228
228
|
runtimeIdentity: string;
|
|
229
229
|
measurement: string;
|
|
@@ -239,7 +239,7 @@ export declare function versionAtLeast(actual: string, floor: string): boolean;
|
|
|
239
239
|
export declare function admitRuntimeMeasurement(observation: DockerRuntimeObservation, measurements: LocalRuntimeMeasurements): RuntimeMeasurementAdmission;
|
|
240
240
|
export declare function observeRuntimeMeasurement(admission: RuntimeMeasurementAdmission, stackId: StackId): EnvironmentObservation;
|
|
241
241
|
export declare function observeDockerRuntime(rawInput: DockerRuntimeObservationInput, execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>): Promise<DockerRuntimeObservation>;
|
|
242
|
-
|
|
242
|
+
type MachineFloorMeasurement = {
|
|
243
243
|
readonly kind: 'complete';
|
|
244
244
|
readonly evidence: MachineFloorEvidence;
|
|
245
245
|
} | MachineFloorRefusal | {
|
|
@@ -255,7 +255,7 @@ export interface RuntimeObservationWindow {
|
|
|
255
255
|
readonly startedAt: string;
|
|
256
256
|
readonly expiresAt: string;
|
|
257
257
|
}
|
|
258
|
-
|
|
258
|
+
interface RuntimeTopologyService {
|
|
259
259
|
readonly id: string;
|
|
260
260
|
readonly dependsOn: readonly string[];
|
|
261
261
|
readonly bindMountSources: readonly string[];
|
|
@@ -278,7 +278,7 @@ export interface RuntimeTopologyEvidence {
|
|
|
278
278
|
readonly observationWindow: RuntimeObservationWindow;
|
|
279
279
|
readonly services: readonly RuntimeTopologyService[];
|
|
280
280
|
}
|
|
281
|
-
|
|
281
|
+
type RuntimeTopologyObservation = {
|
|
282
282
|
readonly kind: 'complete';
|
|
283
283
|
readonly step: ExecutionStep;
|
|
284
284
|
readonly topology: RuntimeTopologyEvidence;
|
|
@@ -287,6 +287,7 @@ export type RuntimeTopologyObservation = {
|
|
|
287
287
|
readonly kind: 'unavailable';
|
|
288
288
|
readonly reason: string;
|
|
289
289
|
readonly mounts?: RuntimeMountMismatch;
|
|
290
|
+
readonly undefinedProfiles?: readonly string[];
|
|
290
291
|
};
|
|
291
292
|
export declare const OBSERVATION_WINDOW_HEADROOM_MS = 300000;
|
|
292
293
|
/**
|
|
@@ -309,7 +310,7 @@ export type RuntimeMountMismatch = Readonly<{
|
|
|
309
310
|
export declare function isRuntimeTopologyEvidence(value: unknown): value is RuntimeTopologyEvidence;
|
|
310
311
|
export declare function resolveRuntimeServiceOrder(topology: RuntimeTopologyEvidence): readonly string[];
|
|
311
312
|
export declare function projectRuntimeExecutionEnvironment(topology: RuntimeTopologyEvidence, namespace?: StateNamespace): Readonly<Record<string, string>> | undefined;
|
|
312
|
-
|
|
313
|
+
type RuntimeContainerIdSlot = Readonly<{
|
|
313
314
|
kind: 'runtime-container-id';
|
|
314
315
|
service: string;
|
|
315
316
|
}>;
|
|
@@ -328,7 +329,7 @@ export interface RuntimeLifecycleExecutionSemantics {
|
|
|
328
329
|
readonly filesystemChecks: ExecutionStep['filesystemChecks'];
|
|
329
330
|
readonly compensation: readonly RuntimeLifecycleExecutionSemantics[];
|
|
330
331
|
}
|
|
331
|
-
|
|
332
|
+
interface RuntimeLifecycleServiceSemantics {
|
|
332
333
|
readonly id: string;
|
|
333
334
|
readonly start: RuntimeLifecycleExecutionSemantics;
|
|
334
335
|
readonly status: RuntimeLifecycleExecutionSemantics;
|
|
@@ -416,6 +417,8 @@ export declare function startRuntimeService(input: {
|
|
|
416
417
|
readonly topology: RuntimeTopologyEvidence;
|
|
417
418
|
readonly service: string;
|
|
418
419
|
readonly execute: Execute;
|
|
420
|
+
/** Build the service's image and replace its container, as `rebuild` does after its code changed. */
|
|
421
|
+
readonly rebuild?: true;
|
|
419
422
|
}): Promise<RuntimeServiceOutcome>;
|
|
420
423
|
/**
|
|
421
424
|
* Re-reads a service an earlier run started: stale when Compose no longer has the recorded
|
|
@@ -427,6 +430,49 @@ export declare function revalidateRuntimeService(input: {
|
|
|
427
430
|
readonly expectedContainerId: string;
|
|
428
431
|
readonly execute: Execute;
|
|
429
432
|
}): Promise<RuntimeServiceOutcome>;
|
|
433
|
+
/**
|
|
434
|
+
* Restarts one service's container in place with `compose restart`, then waits for it as `up` does
|
|
435
|
+
* and inspects it, so the outcome is the same healthy-or-failed evidence a start gives.
|
|
436
|
+
*/
|
|
437
|
+
export declare function restartRuntimeService(input: {
|
|
438
|
+
readonly topology: RuntimeTopologyEvidence;
|
|
439
|
+
readonly service: string;
|
|
440
|
+
readonly execute: Execute;
|
|
441
|
+
}): Promise<RuntimeServiceOutcome>;
|
|
442
|
+
/** One service of the stack as Compose reports it; `state` is `absent` when it has no container. */
|
|
443
|
+
export interface RuntimeServiceState {
|
|
444
|
+
readonly service: string;
|
|
445
|
+
readonly state: string;
|
|
446
|
+
readonly health: string | null;
|
|
447
|
+
readonly status: string | null;
|
|
448
|
+
readonly exitCode: number | null;
|
|
449
|
+
}
|
|
450
|
+
/** Lists every service of the stack with its container's state and health through `compose ps`. */
|
|
451
|
+
export declare function observeRuntimeServiceStates(input: {
|
|
452
|
+
readonly topology: RuntimeTopologyEvidence;
|
|
453
|
+
readonly execute: Execute;
|
|
454
|
+
}): Promise<Readonly<{
|
|
455
|
+
kind: 'complete';
|
|
456
|
+
services: readonly RuntimeServiceState[];
|
|
457
|
+
}> | Readonly<{
|
|
458
|
+
kind: 'unavailable';
|
|
459
|
+
reason: string;
|
|
460
|
+
}>>;
|
|
461
|
+
/**
|
|
462
|
+
* The foreground command that runs `argv` inside one service's container, under the stack's own
|
|
463
|
+
* project, files and profiles, so the caller never names the derived project.
|
|
464
|
+
*/
|
|
465
|
+
export declare function planRuntimeServiceExec(input: {
|
|
466
|
+
readonly topology: RuntimeTopologyEvidence;
|
|
467
|
+
readonly service: string;
|
|
468
|
+
readonly argv: readonly [string, ...string[]];
|
|
469
|
+
readonly tty: boolean;
|
|
470
|
+
}): Readonly<{
|
|
471
|
+
executable: ResolvedExecutable;
|
|
472
|
+
argv: readonly string[];
|
|
473
|
+
cwd: string;
|
|
474
|
+
env: Readonly<Record<string, string>>;
|
|
475
|
+
}> | undefined;
|
|
430
476
|
/** The observation as healthy-service evidence, or undefined when the service is not running healthy. */
|
|
431
477
|
export declare function admitRuntimeServiceHealth(observation: RuntimeServiceObservation): RuntimeServiceEvidence | undefined;
|
|
432
478
|
export declare function isRuntimeServiceEvidence(value: unknown): value is RuntimeServiceEvidence;
|
|
@@ -467,14 +513,14 @@ export interface RuntimeServiceFailureLogContext {
|
|
|
467
513
|
readonly filesystemChecks: ExecutionStep['filesystemChecks'];
|
|
468
514
|
}
|
|
469
515
|
export declare function consumeRuntimeServiceFailureForLogs(evidence: RuntimeServiceFailureEvidence): RuntimeServiceFailureLogContext | undefined;
|
|
470
|
-
|
|
516
|
+
interface RuntimeServiceFailureContext {
|
|
471
517
|
readonly observation: RuntimeServiceObservation;
|
|
472
518
|
readonly executable: ResolvedExecutable;
|
|
473
519
|
readonly cwd: string;
|
|
474
520
|
readonly env: Readonly<Record<string, string>>;
|
|
475
521
|
}
|
|
476
522
|
export declare function consumeRuntimeServiceObservationForFailureLogs(value: RuntimeServiceObservation): RuntimeServiceFailureContext | undefined;
|
|
477
|
-
|
|
523
|
+
type RuntimeStopOutcome =
|
|
478
524
|
/** Compose stopped the stack and a fresh `ps` found nothing running. */
|
|
479
525
|
{
|
|
480
526
|
readonly kind: 'stopped';
|
|
@@ -1278,6 +1278,12 @@ function parseTopology(stdout, stackId, configSha256, activeProfiles) {
|
|
|
1278
1278
|
profiles.set(name, selected);
|
|
1279
1279
|
}
|
|
1280
1280
|
const active = new Set(activeProfiles);
|
|
1281
|
+
// Compose reports only the services of active profiles, so an active profile no reported
|
|
1282
|
+
// service carries is one no service declares; Compose itself accepts it silently.
|
|
1283
|
+
const declaredProfiles = new Set([...profiles.values()].flat());
|
|
1284
|
+
const undefinedProfiles = activeProfiles.filter((profile) => !declaredProfiles.has(profile));
|
|
1285
|
+
if (undefinedProfiles.length > 0)
|
|
1286
|
+
return { undefinedProfiles };
|
|
1281
1287
|
const names = declaredNames.filter((name) => {
|
|
1282
1288
|
const serviceProfiles = profiles.get(name) ?? [];
|
|
1283
1289
|
return serviceProfiles.length === 0 || serviceProfiles.some((profile) => active.has(profile));
|
|
@@ -1371,6 +1377,12 @@ function admitRuntimeTopology(contract, step, outcome) {
|
|
|
1371
1377
|
const projected = parseTopology(outcome.stdout, contract.stackId, contract.configSha256, contract.runtime.activeProfiles);
|
|
1372
1378
|
if (!projected)
|
|
1373
1379
|
return unavailable();
|
|
1380
|
+
if ('undefinedProfiles' in projected)
|
|
1381
|
+
return {
|
|
1382
|
+
kind: 'unavailable',
|
|
1383
|
+
reason: `no Compose service declares profile ${projected.undefinedProfiles.join(', ')}`,
|
|
1384
|
+
undefinedProfiles: projected.undefinedProfiles,
|
|
1385
|
+
};
|
|
1374
1386
|
const mounts = compareBindSources(projected.services.flatMap(({ bindMountSources }) => bindMountSources), contract.runtime.expectedMountSources);
|
|
1375
1387
|
// The caller can name the sibling or host path behind each difference; a bare refusal here
|
|
1376
1388
|
// left `up` reporting only that readiness authority was unavailable.
|
|
@@ -1486,7 +1498,7 @@ function runtimeServiceLookupSteps(topologyContract, service, issuanceId) {
|
|
|
1486
1498
|
const identity = derivedComposeStep(topologyContract.step, `runtime:docker-compose:identity:${service.id}`, 'ps', ['ps', '--quiet', '--all', service.id], 128, issuanceId, MAX_OBSERVATION_WINDOW_MS, undefined, topologyContract.runtime.interpolatedNames);
|
|
1487
1499
|
return status && identity ? { status, identity } : undefined;
|
|
1488
1500
|
}
|
|
1489
|
-
function runtimeServiceInitialSteps(topologyContract, service, issuanceId) {
|
|
1501
|
+
function runtimeServiceInitialSteps(topologyContract, service, issuanceId, rebuild = false) {
|
|
1490
1502
|
// `up --wait` returns only once the service runs and its healthcheck passes, after Compose has
|
|
1491
1503
|
// built or pulled its image, and the declared cold_start_seconds bounds exactly that. The
|
|
1492
1504
|
// ordinary command budget does not: a first `up` was killed and reported "did not become
|
|
@@ -1495,7 +1507,7 @@ function runtimeServiceInitialSteps(topologyContract, service, issuanceId) {
|
|
|
1495
1507
|
// grace; a start without that grace was killed mid-recreate, leaving the replacement container
|
|
1496
1508
|
// created but never started.
|
|
1497
1509
|
const start = derivedComposeStep({ ...topologyContract.step, timeoutMs: COMPOSE_STOP_GRACE_MS
|
|
1498
|
-
+ Math.max(topologyContract.step.timeoutMs, topologyContract.runtime.coldStartLimitMillis) }, `runtime:docker-compose:start:${service.id}`, 'up', ['--progress', 'quiet', 'up', '--detach', '--wait', '--no-deps', service.id], OUTPUT_LIMIT_BYTES, issuanceId, MAX_OBSERVATION_WINDOW_MS, undefined, topologyContract.runtime.interpolatedNames);
|
|
1510
|
+
+ Math.max(topologyContract.step.timeoutMs, topologyContract.runtime.coldStartLimitMillis) }, `runtime:docker-compose:start:${service.id}`, 'up', ['--progress', 'quiet', 'up', '--detach', '--wait', '--no-deps', ...rebuild ? ['--build', '--force-recreate'] : [], service.id], OUTPUT_LIMIT_BYTES, issuanceId, MAX_OBSERVATION_WINDOW_MS, undefined, topologyContract.runtime.interpolatedNames);
|
|
1499
1511
|
// A service that failed its start under a restart policy restarts forever until `down`. The
|
|
1500
1512
|
// failed `up` stops it and keeps the container, so its logs stay readable and the next `up`
|
|
1501
1513
|
// starts it again.
|
|
@@ -1817,7 +1829,7 @@ function runtimeServiceFailure(contract, stage, outcome, previousFinishedAt, con
|
|
|
1817
1829
|
*/
|
|
1818
1830
|
export async function startRuntimeService(input) {
|
|
1819
1831
|
const contract = runtimeServiceContract(input.topology, input.service);
|
|
1820
|
-
const steps = contract && runtimeServiceInitialSteps(contract.topologyContract, contract.service, randomBytes(32).toString('hex'));
|
|
1832
|
+
const steps = contract && runtimeServiceInitialSteps(contract.topologyContract, contract.service, randomBytes(32).toString('hex'), input.rebuild === true);
|
|
1821
1833
|
if (!contract || !steps)
|
|
1822
1834
|
return { kind: 'failure' };
|
|
1823
1835
|
const { projectName } = contract.topologyContract.runtime;
|
|
@@ -1881,6 +1893,64 @@ export async function revalidateRuntimeService(input) {
|
|
|
1881
1893
|
return { kind: 'stale' };
|
|
1882
1894
|
return inspectRuntimeService({ ...contract, containerId: observed.containerId, identityFinishedAt: identity.receipt.finishedAt }, input.execute);
|
|
1883
1895
|
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Restarts one service's container in place with `compose restart`, then waits for it as `up` does
|
|
1898
|
+
* and inspects it, so the outcome is the same healthy-or-failed evidence a start gives.
|
|
1899
|
+
*/
|
|
1900
|
+
export async function restartRuntimeService(input) {
|
|
1901
|
+
const contract = runtimeServiceContract(input.topology, input.service);
|
|
1902
|
+
const step = contract && derivedComposeStep({ ...contract.topologyContract.step, timeoutMs: COMPOSE_STOP_GRACE_MS + contract.topologyContract.step.timeoutMs }, `runtime:docker-compose:restart:${contract.service.id}`, 'restart', ['--progress', 'quiet', 'restart', '--no-deps', contract.service.id], OUTPUT_LIMIT_BYTES, randomBytes(32).toString('hex'), MAX_OBSERVATION_WINDOW_MS, undefined, contract.topologyContract.runtime.interpolatedNames);
|
|
1903
|
+
if (!contract || !step)
|
|
1904
|
+
return { kind: 'failure' };
|
|
1905
|
+
const restarted = await input.execute(step);
|
|
1906
|
+
if (restarted.kind !== 'complete' || restarted.stderr.byteLength !== 0) {
|
|
1907
|
+
return runtimeServiceFailure(contract, 'start', restarted, undefined, null);
|
|
1908
|
+
}
|
|
1909
|
+
return startRuntimeService(input);
|
|
1910
|
+
}
|
|
1911
|
+
/** Lists every service of the stack with its container's state and health through `compose ps`. */
|
|
1912
|
+
export async function observeRuntimeServiceStates(input) {
|
|
1913
|
+
const contract = issuedTopologyEvidence.get(input.topology);
|
|
1914
|
+
const step = contract && derivedComposeStep(contract.step, `runtime:docker-compose:ps:${contract.runtime.projectName}`, 'ps', ['ps', '--format', 'json', '--all'], OUTPUT_LIMIT_BYTES, randomBytes(32).toString('hex'), MAX_OBSERVATION_WINDOW_MS, undefined, contract.runtime.interpolatedNames);
|
|
1915
|
+
if (!contract || !step)
|
|
1916
|
+
return { kind: 'unavailable', reason: 'the stack topology is no longer current' };
|
|
1917
|
+
const outcome = await input.execute(step);
|
|
1918
|
+
if (outcome.kind !== 'complete')
|
|
1919
|
+
return { kind: 'unavailable', reason: `docker compose ps failed (${outcome.code})` };
|
|
1920
|
+
const reported = new Map();
|
|
1921
|
+
try {
|
|
1922
|
+
for (const line of new TextDecoder('utf-8', { fatal: true }).decode(outcome.stdout).split('\n')) {
|
|
1923
|
+
if (line.trim() === '')
|
|
1924
|
+
continue;
|
|
1925
|
+
const row = JSON.parse(line);
|
|
1926
|
+
if (row.Project !== contract.runtime.projectName || typeof row.Service !== 'string' || typeof row.State !== 'string') {
|
|
1927
|
+
return { kind: 'unavailable', reason: 'docker compose ps reported a container outside this stack or without a state' };
|
|
1928
|
+
}
|
|
1929
|
+
reported.set(row.Service, {
|
|
1930
|
+
service: row.Service, state: row.State,
|
|
1931
|
+
health: typeof row.Health === 'string' && row.Health !== '' ? row.Health : null,
|
|
1932
|
+
status: typeof row.Status === 'string' ? row.Status : null,
|
|
1933
|
+
exitCode: typeof row.ExitCode === 'number' ? row.ExitCode : null,
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
catch (error) {
|
|
1938
|
+
return { kind: 'unavailable', reason: unavailableReason('docker compose ps output is not JSON lines', error) };
|
|
1939
|
+
}
|
|
1940
|
+
return { kind: 'complete', services: [...contract.services.keys()].sort().map(service => reported.get(service)
|
|
1941
|
+
?? { service, state: 'absent', health: null, status: null, exitCode: null }) };
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* The foreground command that runs `argv` inside one service's container, under the stack's own
|
|
1945
|
+
* project, files and profiles, so the caller never names the derived project.
|
|
1946
|
+
*/
|
|
1947
|
+
export function planRuntimeServiceExec(input) {
|
|
1948
|
+
const contract = runtimeServiceContract(input.topology, input.service);
|
|
1949
|
+
const step = contract && derivedComposeStep(contract.topologyContract.step, `runtime:docker-compose:exec:${contract.service.id}`, 'exec', ['exec', ...input.tty ? [] : ['--no-tty'], contract.service.id, ...input.argv], OUTPUT_LIMIT_BYTES, randomBytes(32).toString('hex'), MAX_OBSERVATION_WINDOW_MS, undefined, contract.topologyContract.runtime.interpolatedNames);
|
|
1950
|
+
if (!contract || !step)
|
|
1951
|
+
return undefined;
|
|
1952
|
+
return { executable: contract.topologyContract.runtime.runtimeExecutable, argv: step.argv.slice(1), cwd: step.cwd, env: step.env };
|
|
1953
|
+
}
|
|
1884
1954
|
/** The observation as healthy-service evidence, or undefined when the service is not running healthy. */
|
|
1885
1955
|
export function admitRuntimeServiceHealth(observation) {
|
|
1886
1956
|
const contract = issuedRuntimeServiceObservations.get(observation);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SurfaceTarget } from '../surfaces/contract.js';
|
|
2
|
+
import { type EnvironmentDiagnostic } from './contract.js';
|
|
3
|
+
export declare function inside(path: string, root: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The directory the OpenCode grant file names: the served checkout when it lies outside the
|
|
6
|
+
* declared one, which the grant would not otherwise reach.
|
|
7
|
+
*/
|
|
8
|
+
export declare function openCodeGrantDirectory(declared: string, served: string | undefined): string;
|
|
9
|
+
/** Why a directory cannot go into an OpenCode access pattern, or undefined when it can. */
|
|
10
|
+
export declare function openCodePatternUnsafe(directory: string): string | undefined;
|
|
11
|
+
export interface AgentGrantPlan {
|
|
12
|
+
readonly settings: Readonly<{
|
|
13
|
+
bytes: string | null;
|
|
14
|
+
}> | undefined;
|
|
15
|
+
readonly local: readonly Readonly<{
|
|
16
|
+
name: string;
|
|
17
|
+
bytes: string;
|
|
18
|
+
}>[];
|
|
19
|
+
/** What the tools can reach, for a served checkout outside the declared one. */
|
|
20
|
+
readonly lines: readonly string[];
|
|
21
|
+
readonly report: Readonly<{
|
|
22
|
+
granted: readonly SurfaceTarget[];
|
|
23
|
+
ungranted: readonly SurfaceTarget[];
|
|
24
|
+
}> | undefined;
|
|
25
|
+
}
|
|
26
|
+
type Planned = Readonly<{
|
|
27
|
+
kind: 'complete';
|
|
28
|
+
plan: AgentGrantPlan;
|
|
29
|
+
}> | Readonly<{
|
|
30
|
+
kind: 'refusal';
|
|
31
|
+
diagnostic: EnvironmentDiagnostic;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Plans the machine-local grants for one sibling. `served` is the checkout that serves it, or
|
|
35
|
+
* undefined when the sibling is no longer declared; `targets` are the listed tools that hold a
|
|
36
|
+
* committed grant for it. A grant cockpit added earlier for the sibling is taken back when the
|
|
37
|
+
* served checkout no longer needs it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function planAgentGrants(input: Readonly<{
|
|
40
|
+
root: string;
|
|
41
|
+
slug: string;
|
|
42
|
+
declared: string | undefined;
|
|
43
|
+
served: string | undefined;
|
|
44
|
+
targets: readonly SurfaceTarget[];
|
|
45
|
+
}>): Promise<Planned>;
|
|
46
|
+
/** Writes a plan: cockpit's own record first, so a grant it added is never one it forgets. */
|
|
47
|
+
export declare function applyAgentGrants(root: string, plan: AgentGrantPlan): Promise<Readonly<{
|
|
48
|
+
kind: 'complete';
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'refusal';
|
|
51
|
+
diagnostic: EnvironmentDiagnostic;
|
|
52
|
+
}>>;
|
|
53
|
+
export {};
|