ankka-console 0.11.0 → 0.12.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/client/control-plane.d.ts +63 -1
- package/dist/client/control-plane.js +142 -1
- package/dist/client/schemas.d.ts +1312 -2
- package/dist/client/schemas.js +423 -2
- package/dist/context.d.ts +1 -1
- package/dist/context.js +1 -1
- package/dist/extensions/types.d.ts +1 -1
- package/dist/extensions/types.js +22 -0
- package/dist/routes/front.d.ts +62 -2
- package/dist/routes/front.js +37 -8
- package/dist/routes/logs.d.ts +57 -0
- package/dist/routes/machines.d.ts +78 -0
- package/dist/routes/machines.js +78 -0
- package/dist/routes/organization.d.ts +29 -1
- package/dist/routes/organization.js +27 -5
- package/dist/routes/project.d.ts +275 -1
- package/dist/routes/project.js +107 -6
- package/dist/routes/service-history.d.ts +2 -0
- package/dist/routes/service-history.js +3 -0
- package/dist/routes/service-topology.d.ts +57 -0
- package/dist/routes/service.d.ts +57 -0
- package/dist/routes/service.js +63 -4
- package/dist/routes.js +1 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +1 -0
- package/dist/testing/fake-control-plane.d.ts +179 -1
- package/dist/testing/fake-control-plane.js +819 -7
- package/dist/ui/grants.d.ts +3 -0
- package/dist/ui/grants.js +13 -0
- package/dist/ui/shell.d.ts +1 -1
- package/dist/ui/shell.js +9 -2
- package/dist/ui/use-stream.d.ts +114 -0
- package/package.json +1 -1
package/dist/routes/service.js
CHANGED
|
@@ -7,7 +7,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
7
7
|
import { redirect, useLoaderData } from "react-router";
|
|
8
8
|
import { act, applyPrimary, guard, pageData, projectShell, text, useConsoleContext } from "../context.js";
|
|
9
9
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
10
|
-
import { ConsoleForm, Submit, useConsole } from "../ui/console.js";
|
|
10
|
+
import { ConsoleForm, Field, Submit, useConsole } from "../ui/console.js";
|
|
11
|
+
import { Select } from "../ui/primitives/select.js";
|
|
11
12
|
import { Page, SectionTitle, ServiceSections } from "../ui/shell.js";
|
|
12
13
|
import { Shape } from "../ui/shape.js";
|
|
13
14
|
import { runsAs } from "../ui/hosting.js";
|
|
@@ -37,6 +38,7 @@ export async function loader({ params, context }) {
|
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
const serviceOperations = ["pause", "resume", "restart", "expose", "unexpose"];
|
|
41
|
+
const storageOperations = ["storage-credential", "storage-settings"];
|
|
40
42
|
export async function action({ request, params, context }) {
|
|
41
43
|
const ctx = useConsoleContext(context);
|
|
42
44
|
const { projectId, name } = params;
|
|
@@ -47,12 +49,47 @@ export async function action({ request, params, context }) {
|
|
|
47
49
|
await ctx.client.deleteService(projectId, name);
|
|
48
50
|
return redirect(ctx.href(`projects/${encodeURIComponent(projectId)}`));
|
|
49
51
|
}
|
|
52
|
+
const self = ctx.href(`projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(name)}`);
|
|
53
|
+
if (intent === "storage-credential") {
|
|
54
|
+
await ctx.client.reissueStorageCredential(projectId, name);
|
|
55
|
+
return redirect(self);
|
|
56
|
+
}
|
|
57
|
+
if (intent === "storage-settings") {
|
|
58
|
+
await ctx.client.reapplyStorageSettings(projectId, name);
|
|
59
|
+
return redirect(self);
|
|
60
|
+
}
|
|
61
|
+
if (intent === "storage-move") {
|
|
62
|
+
await ctx.client.moveStorage(projectId, name, text(form, "writePauseBound") || undefined);
|
|
63
|
+
return redirect(self);
|
|
64
|
+
}
|
|
65
|
+
if (intent === "hostname-add") {
|
|
66
|
+
await ctx.client.addHostname(projectId, name, text(form, "hostname").trim());
|
|
67
|
+
return redirect(self);
|
|
68
|
+
}
|
|
69
|
+
if (intent === "hostname-remove") {
|
|
70
|
+
await ctx.client.removeHostname(projectId, name, text(form, "hostname"));
|
|
71
|
+
return redirect(self);
|
|
72
|
+
}
|
|
50
73
|
if (!serviceOperations.includes(intent))
|
|
51
74
|
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
52
75
|
await ctx.client.serviceOperation(projectId, name, intent);
|
|
53
|
-
return redirect(
|
|
76
|
+
return redirect(self);
|
|
54
77
|
});
|
|
55
78
|
}
|
|
79
|
+
/** The store a bucket is in, as the operator reported it. */
|
|
80
|
+
const stores = { garage: "Garage", gcs: "Google Cloud Storage" };
|
|
81
|
+
/**
|
|
82
|
+
* What members can do about a bucket the platform made: issue its credential again, reapply the
|
|
83
|
+
* installation's settings to one in Google Cloud Storage, or move one from Garage there. Whether the
|
|
84
|
+
* installation can move at all is the control plane's to say, and its refusal is shown here.
|
|
85
|
+
*/
|
|
86
|
+
function Storage({ s }) {
|
|
87
|
+
const { shows } = useConsole();
|
|
88
|
+
const moveRefusal = useRefusal("storage-move");
|
|
89
|
+
if (!s.bucket)
|
|
90
|
+
return null;
|
|
91
|
+
return (_jsxs("section", { className: "ac-ops", "aria-labelledby": "storage-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "storage-title", children: "Object storage" }) }), _jsx(Operation, { intent: "storage-credential", label: "Issue credential again", operation: "service.storage-credential", entity: s }), s.objectStore === "gcs" ? (_jsx(Operation, { intent: "storage-settings", label: "Reapply bucket settings", operation: "service.storage-settings", entity: s })) : shows("service.storage-move") ? (_jsxs("details", { className: "ac-more", open: moveRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Move to Google Cloud Storage" }), _jsx("p", { children: "Every object is copied while the service goes on writing; then its writes are paused, what changed is copied, every object is checked on both sides, and the service is replaced onto its new bucket. The bucket in Garage is kept." }), _jsxs(ConsoleForm, { intent: "storage-move", className: "ac-form", children: [_jsx(Field, { label: "Write pause bound", name: "writePauseBound", placeholder: "10m", autoComplete: "off", defaultValue: moveRefusal?.values.writePauseBound, hint: "How long writes may be paused, from 1m to 24h. A pause that reaches it fails the move and gives the writes back." }), _jsx(Refused, { intent: "storage-move" }), _jsx("div", { children: _jsx(Submit, { intent: "storage-move", children: "Move bucket" }) })] })] })) : null, _jsx(HostActions, { operation: "service.storage-move", entity: s }), storageOperations.map((op) => (_jsx(Refused, { intent: op }, op)))] }));
|
|
92
|
+
}
|
|
56
93
|
/** A service's bucket, that it has a store of its own, or none — and a phrase while it waits or failed. */
|
|
57
94
|
function objectStorage(s) {
|
|
58
95
|
if (s.objectStorage && /waiting|failed/.test(s.objectStorage))
|
|
@@ -67,12 +104,34 @@ function Operation({ intent, label, operation, danger, entity }) {
|
|
|
67
104
|
const { shows } = useConsole();
|
|
68
105
|
return (_jsxs(_Fragment, { children: [shows(operation) ? (_jsx(ConsoleForm, { intent: intent, children: _jsx(Submit, { intent: intent, danger: danger, children: label }) })) : null, _jsx(HostActions, { operation: operation, entity: entity })] }));
|
|
69
106
|
}
|
|
107
|
+
function record(r) {
|
|
108
|
+
return `${r.kind} ${r.name} → ${r.value}`;
|
|
109
|
+
}
|
|
110
|
+
function hostnameStands(h) {
|
|
111
|
+
return h.reason ? `${h.state}: ${h.reason}` : h.state;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The names a service answers at: the one the platform derives, then each custom hostname with where it
|
|
115
|
+
* stands and the record its owner creates, and the proof record the project's names need. Adding and
|
|
116
|
+
* removing are forms; the control plane's refusal is shown as it says it.
|
|
117
|
+
*/
|
|
118
|
+
function Hostnames({ service: s }) {
|
|
119
|
+
return (_jsxs("section", { className: "ac-card", "aria-labelledby": "hostnames", "data-hostnames": true, children: [_jsx("h2", { id: "hostnames", children: "Hostnames" }), _jsxs("dl", { className: "ac-facts", children: [_jsx("dt", { children: "Address" }), _jsx("dd", { children: s.hostname ? _jsx("a", { href: s.hostname, children: s.hostname }) : s.exposed ? "Exposed; the platform has no address for it yet" : "Not exposed" }), s.customHostnames.map((h) => (_jsxs("div", { "data-custom-hostname": h.hostname, "data-state": h.state, children: [_jsx("dt", { children: h.hostname }), _jsxs("dd", { children: [_jsx("span", { children: hostnameStands(h) }), h.record ? (_jsxs("span", { className: "ac-hint", "data-record": true, children: [" ", "Create ", record(h.record)] })) : null, h.note ? _jsxs("span", { className: "ac-hint", children: [" ", h.note] }) : null] })] }, h.hostname))), s.proofRecord ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Proof record" }), _jsx("dd", { children: _jsxs("code", { "data-proof-record": true, children: [s.proofRecord.kind, " ", s.proofRecord.name, " \"", s.proofRecord.value, "\""] }) })] })) : null] })] }));
|
|
120
|
+
}
|
|
121
|
+
/** Adding and removing a custom hostname, in the inspector with the service's other operations. */
|
|
122
|
+
function HostnameOperations({ service: s }) {
|
|
123
|
+
const { shows } = useConsole();
|
|
124
|
+
const addRefusal = useRefusal("hostname-add");
|
|
125
|
+
if (!shows("service-hostname.add") && !shows("service-hostname.remove"))
|
|
126
|
+
return null;
|
|
127
|
+
return (_jsxs("section", { className: "ac-form", "aria-labelledby": "hostname-operations", "data-hostname-operations": true, children: [_jsx(SectionTitle, { children: _jsx("span", { id: "hostname-operations", children: "Custom hostnames" }) }), shows("service-hostname.add") ? (_jsxs(ConsoleForm, { intent: "hostname-add", className: "ac-inline", children: [_jsx(Field, { label: "Add a custom hostname", name: "hostname", required: true, autoComplete: "off", placeholder: "app.example.com", defaultValue: addRefusal?.values.hostname }), _jsx(Submit, { intent: "hostname-add", primary: true, children: "Add" })] })) : null, _jsx(Refused, { intent: "hostname-add" }), shows("service-hostname.remove") && s.customHostnames.length > 0 ? (_jsxs(ConsoleForm, { intent: "hostname-remove", className: "ac-inline", children: [_jsx(Select, { label: "Remove a custom hostname", id: "hostname-remove", name: "hostname", children: s.customHostnames.map((h) => (_jsx("option", { value: h.hostname, children: h.hostname }, h.hostname))) }), _jsx(Submit, { intent: "hostname-remove", danger: true, children: "Remove" })] })) : null, _jsx(Refused, { intent: "hostname-remove" })] }));
|
|
128
|
+
}
|
|
70
129
|
export default function Service() {
|
|
71
130
|
const data = useLoaderData();
|
|
72
131
|
const { project: p, panels } = data;
|
|
73
132
|
const { status: s, state } = useServiceStream(p.id, data.service.name, data.service);
|
|
74
133
|
const deleteRefused = useRefusal("delete") !== undefined;
|
|
75
|
-
const inspector = (_jsxs(_Fragment, { children: [_jsxs("section", { className: "ac-ops", "aria-labelledby": "operations-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "operations-title", children: "Operations" }) }), s.paused ? _jsx(Operation, { intent: "resume", label: "Resume", operation: "service.resume", entity: s }) : _jsx(Operation, { intent: "pause", label: "Pause", operation: "service.pause", entity: s }), _jsx(Operation, { intent: "restart", label: "Restart", operation: "service.restart", entity: s }), s.exposed ? _jsx(Operation, { intent: "unexpose", label: "Unexpose", operation: "service.unexpose", entity: s }) : _jsx(Operation, { intent: "expose", label: "Expose", operation: "service.expose", entity: s })] }), serviceOperations.map((op) => (_jsx(Refused, { intent: op }, op))), _jsxs("details", { className: "ac-more", open: deleteRefused || undefined, children: [_jsx("summary", { children: "Delete" }), _jsxs("div", { className: "ac-danger-zone", children: [_jsxs("p", { children: ["Deleting ", s.name, " stops it and removes it from the project. Its database is kept, and so is its bucket if it has one: applying a descriptor with this name again brings the service back with its data."] }), _jsx(Operation, { intent: "delete", label: "Delete service", operation: "service.delete", danger: true, entity: s }), _jsx(Refused, { intent: "delete" })] })] })] }));
|
|
76
|
-
return (_jsxs(Page, { inspector: inspector, children: [_jsxs("div", { className: "ac-head", children: [_jsx("h1", { children: s.name }), _jsxs("p", { "aria-live": "polite", className: "ac-actions ac-state-line", children: [_jsx("span", { className: "ac-pill", children: _jsx(Lifecycle, { lifecycle: s.lifecycle, confirmed: s.confirmed }) }), state === "live" ? _jsx("span", { className: "ac-live", children: " Updating as the platform reports" }) : null] }), _jsx(ServiceSections, { projectId: p.id, name: s.name, current: "overview" })] }), s.detail ? _jsx("p", { className: "ac-notice", children: s.detail }) : null, _jsx(Shape, { service: s }), _jsxs("section", { className: "ac-card", "aria-labelledby": "reported", children: [_jsx("h2", { id: "reported", children: "Reported by the cluster" }), _jsxs("dl", { className: "ac-facts", children: [_jsx("dt", { children: "Instances" }), _jsxs("dd", { children: [s.readyInstances, " ready of ", s.desiredInstances] }), _jsx("dt", { children: "Image" }), _jsx("dd", { children: s.image }), _jsx("dt", { children: "Generation" }), _jsx("dd", { children: s.generation }), _jsx("dt", { children: "
|
|
134
|
+
const inspector = (_jsxs(_Fragment, { children: [_jsxs("section", { className: "ac-ops", "aria-labelledby": "operations-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "operations-title", children: "Operations" }) }), s.paused ? _jsx(Operation, { intent: "resume", label: "Resume", operation: "service.resume", entity: s }) : _jsx(Operation, { intent: "pause", label: "Pause", operation: "service.pause", entity: s }), _jsx(Operation, { intent: "restart", label: "Restart", operation: "service.restart", entity: s }), s.exposed ? _jsx(Operation, { intent: "unexpose", label: "Unexpose", operation: "service.unexpose", entity: s }) : _jsx(Operation, { intent: "expose", label: "Expose", operation: "service.expose", entity: s })] }), _jsx(HostnameOperations, { service: s }), serviceOperations.map((op) => (_jsx(Refused, { intent: op }, op))), _jsx(Storage, { s: s }), _jsxs("details", { className: "ac-more", open: deleteRefused || undefined, children: [_jsx("summary", { children: "Delete" }), _jsxs("div", { className: "ac-danger-zone", children: [_jsxs("p", { children: ["Deleting ", s.name, " stops it and removes it from the project. Its database is kept, and so is its bucket if it has one: applying a descriptor with this name again brings the service back with its data."] }), _jsx(Operation, { intent: "delete", label: "Delete service", operation: "service.delete", danger: true, entity: s }), _jsx(Refused, { intent: "delete" })] })] })] }));
|
|
135
|
+
return (_jsxs(Page, { inspector: inspector, children: [_jsxs("div", { className: "ac-head", children: [_jsx("h1", { children: s.name }), _jsxs("p", { "aria-live": "polite", className: "ac-actions ac-state-line", children: [_jsx("span", { className: "ac-pill", children: _jsx(Lifecycle, { lifecycle: s.lifecycle, confirmed: s.confirmed }) }), state === "live" ? _jsx("span", { className: "ac-live", children: " Updating as the platform reports" }) : null] }), _jsx(ServiceSections, { projectId: p.id, name: s.name, current: "overview" })] }), s.detail ? _jsx("p", { className: "ac-notice", children: s.detail }) : null, _jsx(Shape, { service: s }), _jsx(Hostnames, { service: s }), _jsxs("section", { className: "ac-card", "aria-labelledby": "reported", children: [_jsx("h2", { id: "reported", children: "Reported by the cluster" }), _jsxs("dl", { className: "ac-facts", children: [_jsx("dt", { children: "Instances" }), _jsxs("dd", { children: [s.readyInstances, " ready of ", s.desiredInstances] }), _jsx("dt", { children: "Image" }), _jsx("dd", { children: s.image }), _jsx("dt", { children: "Generation" }), _jsx("dd", { children: s.generation }), _jsx("dt", { children: "Database" }), _jsx("dd", { children: s.hosting === "web" ? "None" : (s.database ?? "Nothing reported yet") }), _jsx("dt", { children: "Object storage" }), _jsxs("dd", { children: [objectStorage(s), s.bucketAddress ? _jsxs("span", { className: "ac-hint", "data-bucket-address": true, children: [" ", s.bucketAddress] }) : null] }), s.objectStore ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Object store" }), _jsx("dd", { "data-object-store": s.objectStore, children: stores[s.objectStore] ?? s.objectStore })] })) : null, s.bucketLocation ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Bucket location" }), _jsx("dd", { children: s.bucketLocation })] })) : null, s.softDeleteDays !== undefined && s.softDeleteDays !== null ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Deleted objects" }), _jsxs("dd", { children: ["Recoverable for ", s.softDeleteDays, " days"] })] })) : null, s.storageMove ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Bucket move" }), _jsx("dd", { "data-storage-move": true, children: s.storageMove })] })) : null, s.broker ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Broker" }), _jsx("dd", { children: s.broker })] })) : null, s.grants ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Grants" }), _jsx("dd", { "data-grants": true, children: s.grants })] })) : null, s.crossProjectTopics && s.crossProjectTopics.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Cross-project topics" }), _jsx("dd", { children: _jsx("ul", { className: "ac-topics", children: s.crossProjectTopics.map((t) => (_jsxs("li", { "data-cross-project-topic": `${t.project}/${t.topic}`, "data-granted": t.status === "granted" ? "yes" : "no", children: [_jsx("code", { children: `${t.project}/${t.topic}` }), " ", t.right, " ", t.status === "granted" ? t.status : _jsx("span", { className: "ac-notice", children: t.status })] }, `${t.project}/${t.topic}/${t.right}`))) }) })] })) : null, s.undeclaredTopics && s.undeclaredTopics.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Undeclared topics" }), _jsx("dd", { children: _jsx("ul", { className: "ac-topics", children: s.undeclaredTopics.map((t) => (_jsx("li", { "data-topic": t, children: _jsx("code", { children: t }) }, t))) }) })] })) : null, s.warnings && s.warnings.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Warnings" }), _jsx("dd", { children: _jsx("ul", { className: "ac-topics", children: s.warnings.map((w) => (_jsx("li", { "data-warning": w.kind, children: _jsx("span", { className: "ac-notice", children: w.message }) }, `${w.kind}/${w.component}/${w.topic}`))) }) })] })) : null, s.topicSources && s.topicSources.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Topic sources" }), _jsx("dd", { children: _jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "data-topic-sources": true, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Component" }), _jsx("th", { scope: "col", children: "Topic" }), _jsx("th", { scope: "col", children: "Group" }), _jsx("th", { scope: "col", className: "ac-num", children: "Version" }), _jsx("th", { scope: "col", className: "ac-num", children: "Lag" }), _jsx("th", { scope: "col", children: "Failing" }), _jsx("th", { scope: "col", children: "Retained" })] }) }), _jsx("tbody", { children: s.topicSources.map((t) => (_jsxs("tr", { "data-topic-source": t.component, "data-failing": t.failing ? "yes" : "no", children: [_jsx("td", { children: t.component }), _jsxs("td", { children: [_jsx("code", { children: t.broker ? `${t.topic}@${t.broker}` : t.topic }), t.contract ? ` as ${t.contract}` : ""] }), _jsx("td", { children: _jsx("code", { children: t.group }) }), _jsx("td", { className: "ac-num", children: t.version }), _jsx("td", { className: "ac-num", children: t.lag ?? "—" }), _jsx("td", { children: t.failing ? _jsx("span", { className: "ac-notice", children: t.failing }) : "—" }), _jsx("td", { "data-gap": t.gap ? (t.gap.compacted ? "compacted" : t.gap.gone ? "gone" : "everything") : "unknown", children: t.gap === undefined || t.gap === null ? ("—") : t.gap.compacted ? ("compacted") : !t.gap.gone ? ("everything") : (_jsxs("details", { children: [_jsx("summary", { children: "earlier messages gone" }), _jsx("ul", { className: "ac-topics", children: t.gap.partitions.map((p) => (_jsxs("li", { "data-partition": p.partition, children: ["p", p.partition, " from ", p.beginning, " (", p.earliestRetained ?? "holds nothing", ")"] }, p.partition))) })] })) })] }, t.component))) })] }) }) })] })) : null, s.topicChecks && s.topicChecks.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Topic checks" }), _jsx("dd", { children: _jsx("ul", { className: "ac-topics", children: s.topicChecks.map((c) => (_jsxs("li", { "data-check": c.state, children: [c.component, " ", c.direction, ": ", c.state, c.state === "mismatch" ? ` (${c.stated ?? "none"})` : ""] }, `${c.component}/${c.direction}`))) }) })] })) : null, _jsx("dt", { children: "Runs as" }), _jsx("dd", { children: runsAs(s) }), s.hosting === "web" ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Admits" }), _jsx("dd", { children: ["The internet", ...s.callers.map((c) => (c === "*" ? `Every service in ${p.name}` : c))].join(", ") }), _jsx("dt", { children: "Mounts" }), _jsx("dd", { children: s.mounts.length === 0 ? ("None") : (_jsx("ul", { className: "ac-mounts", children: s.mounts.map((m) => (_jsxs("li", { "data-mount": m.path, children: [_jsx("code", { children: m.path }), " \u2192 ", m.service, m.state && m.state !== "ok" ? ` (${m.state})` : ""] }, m.path))) })) })] })) : null, _jsx("dt", { children: "Stopped by" }), _jsx("dd", { children: s.suspended ? "Its organization is disabled" : s.paused ? "Its members paused it" : "Nobody" }), _jsx("dt", { children: "Report" }), _jsx("dd", { children: s.confirmed ? "Confirmed by the cluster" : "The last known state; the cluster has not confirmed it" })] })] }), _jsx(Panels, { kind: "service", entity: s, loaded: panels })] }));
|
|
77
136
|
}
|
|
78
137
|
export const ErrorBoundary = ConsoleErrorBoundary;
|
package/dist/routes.js
CHANGED
|
@@ -34,6 +34,7 @@ export function consoleRoutes(options = {}) {
|
|
|
34
34
|
page("organizations/:organizationId", "organization"),
|
|
35
35
|
page("organizations/:organizationId/members", "members"),
|
|
36
36
|
page("organizations/:organizationId/tokens", "tokens"),
|
|
37
|
+
page("organizations/:organizationId/machines", "machines"),
|
|
37
38
|
page("organizations/:organizationId/projects/new", "project-new"),
|
|
38
39
|
page("projects/:projectId", "project"),
|
|
39
40
|
page("projects/:projectId/services/apply", "service-apply"),
|
package/dist/runtime.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { SealedFlash, type SessionStore } from "./session/cookie-store.ts";
|
|
|
3
3
|
import { SessionTokenSource, type TokenSource } from "./session/token-source.ts";
|
|
4
4
|
import { type FetchLike } from "./server/transport.ts";
|
|
5
5
|
import type { ConsoleOptions } from "./options.ts";
|
|
6
|
-
import type { DeployTokenCreated } from "./client/schemas.ts";
|
|
6
|
+
import type { DeployTokenCreated, MachineRegistered } from "./client/schemas.ts";
|
|
7
7
|
export interface ConsoleRuntime {
|
|
8
8
|
options: ConsoleOptions;
|
|
9
9
|
mount: string;
|
|
@@ -16,6 +16,8 @@ export interface ConsoleRuntime {
|
|
|
16
16
|
controlPlaneFetch: FetchLike;
|
|
17
17
|
/** A deploy token's secret, carried across one redirect and shown once. */
|
|
18
18
|
tokenFlash: SealedFlash<DeployTokenCreated>;
|
|
19
|
+
/** A registered machine's client secret, carried across one redirect and shown once, as a token's is. */
|
|
20
|
+
machineFlash: SealedFlash<MachineRegistered>;
|
|
19
21
|
pendingLogin: SealedFlash<{
|
|
20
22
|
state: string;
|
|
21
23
|
nonce: string;
|
package/dist/runtime.js
CHANGED
|
@@ -71,6 +71,7 @@ export function createRuntime(options) {
|
|
|
71
71
|
sessionTokens,
|
|
72
72
|
controlPlaneFetch,
|
|
73
73
|
tokenFlash: new SealedFlash({ secret: secret ?? "unused", secure, name: "ankka_console_flash", maxAgeSeconds: 60 }),
|
|
74
|
+
machineFlash: new SealedFlash({ secret: secret ?? "unused", secure, name: "ankka_console_machine_flash", maxAgeSeconds: 60 }),
|
|
74
75
|
pendingLogin: new SealedFlash({ secret: secret ?? "unused", secure, name: "ankka_console_login", maxAgeSeconds: 600 }),
|
|
75
76
|
log: options.log ?? ((line) => process.stdout.write(JSON.stringify(line) + "\n")),
|
|
76
77
|
};
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* A control plane in one process: every route the console uses, over in-memory state, with the
|
|
3
3
|
* control plane's rules as the console can observe them — who sees what, `404` rather than `403`
|
|
4
4
|
* for an outsider, owner-only operations, tombstoned ids, `409` for a non-empty delete, a disabled
|
|
5
|
-
* organization refusing changes, descriptor validation, a deploy token shown once
|
|
5
|
+
* organization refusing changes, descriptor validation, a deploy token shown once, grants made,
|
|
6
|
+
* listed and ended with their effect, and machines registered with a secret shown once.
|
|
6
7
|
*
|
|
7
8
|
* It answers from state rather than from a script, so it cannot run out quietly. What a test needs
|
|
8
9
|
* to steer is explicit: `script()` makes the next answer on one route a given status, `tick()` moves
|
|
@@ -32,6 +33,10 @@ export interface FakeControlPlaneOptions {
|
|
|
32
33
|
signupUrl?: string;
|
|
33
34
|
baseDomain?: string;
|
|
34
35
|
port?: number;
|
|
36
|
+
/** The broker's address outside the cluster; unset, the installation does not expose its broker and a machine's topic grant opens nothing. */
|
|
37
|
+
brokerBootstrap?: string;
|
|
38
|
+
/** The most bytes a second a machine may be given on either side; the control plane's default is 32 MiB. */
|
|
39
|
+
byteRateCeiling?: number;
|
|
35
40
|
}
|
|
36
41
|
type Role = "owner" | "member";
|
|
37
42
|
interface Member {
|
|
@@ -82,8 +87,55 @@ interface Project {
|
|
|
82
87
|
topics?: Map<string, TopicRecord>;
|
|
83
88
|
/** Declared brokers beside the installation's, by name. */
|
|
84
89
|
brokers?: Map<string, BrokerRecord>;
|
|
90
|
+
/** Where the project's new buckets in Google Cloud Storage are made (feature 039). */
|
|
91
|
+
location?: string;
|
|
92
|
+
/** Feature 041: restores asked for, in order, and rehearsals. Restores verify at once here. */
|
|
93
|
+
restores?: {
|
|
94
|
+
name: string;
|
|
95
|
+
line: string;
|
|
96
|
+
moment: string;
|
|
97
|
+
requestedBy?: string;
|
|
98
|
+
requestedAt: string;
|
|
99
|
+
}[];
|
|
100
|
+
rehearsals?: {
|
|
101
|
+
name: string;
|
|
102
|
+
line: string;
|
|
103
|
+
moment: string;
|
|
104
|
+
requestedBy?: string;
|
|
105
|
+
requestedAt: string;
|
|
106
|
+
}[];
|
|
107
|
+
databaseHistory?: {
|
|
108
|
+
kind: string;
|
|
109
|
+
by?: string;
|
|
110
|
+
at: string;
|
|
111
|
+
detail?: string;
|
|
112
|
+
}[];
|
|
113
|
+
credentialGeneration?: number;
|
|
114
|
+
database?: {
|
|
115
|
+
replicas: number;
|
|
116
|
+
synchronous: boolean;
|
|
117
|
+
retentionDays?: number;
|
|
118
|
+
rehearse?: string;
|
|
119
|
+
};
|
|
120
|
+
/** The changes to its topics, newest first. */
|
|
121
|
+
history?: ProjectHistoryRecord[];
|
|
85
122
|
hidden: boolean;
|
|
86
123
|
}
|
|
124
|
+
interface ProjectHistoryRecord {
|
|
125
|
+
kind: string;
|
|
126
|
+
topic: string;
|
|
127
|
+
actor?: {
|
|
128
|
+
subject: string;
|
|
129
|
+
display?: string;
|
|
130
|
+
administrative: boolean;
|
|
131
|
+
};
|
|
132
|
+
at?: string;
|
|
133
|
+
changes: {
|
|
134
|
+
setting: string;
|
|
135
|
+
from: string;
|
|
136
|
+
to: string;
|
|
137
|
+
}[];
|
|
138
|
+
}
|
|
87
139
|
interface BrokerRecord {
|
|
88
140
|
bootstrap: string;
|
|
89
141
|
shape: string;
|
|
@@ -102,7 +154,31 @@ interface TopicSourceRecord {
|
|
|
102
154
|
contract?: string;
|
|
103
155
|
lag?: number;
|
|
104
156
|
failing?: string;
|
|
157
|
+
/** What the broker still holds of the topic, per partition. */
|
|
158
|
+
gap?: {
|
|
159
|
+
partitions: {
|
|
160
|
+
partition: number;
|
|
161
|
+
beginning: number;
|
|
162
|
+
earliestRetained?: string;
|
|
163
|
+
}[];
|
|
164
|
+
compacted?: boolean;
|
|
165
|
+
gone?: boolean;
|
|
166
|
+
readAt?: string;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** A topic's settings, in the words a member types, as the control plane keeps them. */
|
|
170
|
+
interface TopicSettingsRecord {
|
|
171
|
+
retention: string;
|
|
172
|
+
retentionSize: string;
|
|
173
|
+
cleanup: string;
|
|
174
|
+
tombstoneWindow: string;
|
|
175
|
+
minCompactionLag: string;
|
|
176
|
+
maxCompactionLag: string;
|
|
177
|
+
copies?: number;
|
|
178
|
+
minInSync?: number;
|
|
105
179
|
}
|
|
180
|
+
declare const SettingNames: readonly ["retention", "retentionSize", "cleanup", "tombstoneWindow", "minCompactionLag", "maxCompactionLag", "copies", "minInSync"];
|
|
181
|
+
type SettingName = (typeof SettingNames)[number];
|
|
106
182
|
interface TopicRecord {
|
|
107
183
|
partitions: number;
|
|
108
184
|
phase?: string;
|
|
@@ -113,6 +189,11 @@ interface TopicRecord {
|
|
|
113
189
|
fingerprint: string;
|
|
114
190
|
schema: unknown;
|
|
115
191
|
};
|
|
192
|
+
/** Every setting, and the ones the installation supplied; absent for a topic seeded without them. */
|
|
193
|
+
settings?: TopicSettingsRecord;
|
|
194
|
+
defaulted?: SettingName[];
|
|
195
|
+
/** How many copies the broker holds, as a seed says. */
|
|
196
|
+
copiesHeld?: number;
|
|
116
197
|
}
|
|
117
198
|
/** One side a running service takes on a declared topic with a contract, against it; `topic` is the fake's own, for the project's listing. */
|
|
118
199
|
interface TopicCheckRecord {
|
|
@@ -126,6 +207,8 @@ interface TopicCheckRecord {
|
|
|
126
207
|
interface Service {
|
|
127
208
|
name: string;
|
|
128
209
|
projectId: string;
|
|
210
|
+
/** Custom hostnames (feature 045), in the order added; the fake serves each at once. */
|
|
211
|
+
customHostnames?: string[];
|
|
129
212
|
lifecycle: string;
|
|
130
213
|
generation: number;
|
|
131
214
|
image: string;
|
|
@@ -136,6 +219,8 @@ interface Service {
|
|
|
136
219
|
paused: boolean;
|
|
137
220
|
/** What the cluster reports of its database; `null` until it has reported one. */
|
|
138
221
|
database: string | null;
|
|
222
|
+
/** The restore a member switched it to (feature 041); the project database when absent. */
|
|
223
|
+
databaseCluster?: string;
|
|
139
224
|
history: {
|
|
140
225
|
kind: string;
|
|
141
226
|
generation: number;
|
|
@@ -147,6 +232,7 @@ interface Service {
|
|
|
147
232
|
at: string;
|
|
148
233
|
image?: string;
|
|
149
234
|
digest?: string;
|
|
235
|
+
hostname?: string;
|
|
150
236
|
rolledBackTo?: number;
|
|
151
237
|
}[];
|
|
152
238
|
/** The descriptor in force, and the applied ones a rollback can return to, newest first, at most fifty. */
|
|
@@ -181,6 +267,12 @@ interface Service {
|
|
|
181
267
|
ownObjectStore?: boolean;
|
|
182
268
|
/** The descriptor asked that the bucket be reachable from the internet. */
|
|
183
269
|
exposeObjectStorage?: boolean;
|
|
270
|
+
/** The store the bucket is in, `garage` or `gcs`, as the operator reports it (feature 039). */
|
|
271
|
+
objectStore?: string;
|
|
272
|
+
/** Where its bucket in Google Cloud Storage is. */
|
|
273
|
+
bucketLocation?: string;
|
|
274
|
+
/** Where a move of its bucket from Garage is, as the control plane phrases it. */
|
|
275
|
+
storageMove?: string;
|
|
184
276
|
/** What the instances report when asked for the topology; generated from the service when unset. */
|
|
185
277
|
topology?: FakeTopology;
|
|
186
278
|
}
|
|
@@ -216,6 +308,48 @@ interface Token {
|
|
|
216
308
|
expiresAt?: string;
|
|
217
309
|
revoked: boolean;
|
|
218
310
|
}
|
|
311
|
+
/** What one grant opens, flat as the control plane holds it (feature 040). */
|
|
312
|
+
export interface FakeGrantTarget {
|
|
313
|
+
kind: string;
|
|
314
|
+
service?: string;
|
|
315
|
+
method?: string;
|
|
316
|
+
path?: string;
|
|
317
|
+
topic?: string;
|
|
318
|
+
right?: string;
|
|
319
|
+
decrypt?: boolean;
|
|
320
|
+
}
|
|
321
|
+
interface GrantAct {
|
|
322
|
+
by?: string;
|
|
323
|
+
at: string;
|
|
324
|
+
}
|
|
325
|
+
/** A grant a project made; `changes` is what the grantee's side records of it. */
|
|
326
|
+
interface Grant {
|
|
327
|
+
id: string;
|
|
328
|
+
projectId: string;
|
|
329
|
+
grantee: string;
|
|
330
|
+
target: FakeGrantTarget;
|
|
331
|
+
state: string;
|
|
332
|
+
granted: GrantAct;
|
|
333
|
+
answered?: GrantAct;
|
|
334
|
+
ended?: GrantAct;
|
|
335
|
+
changes: {
|
|
336
|
+
change: string;
|
|
337
|
+
by?: string;
|
|
338
|
+
at: string;
|
|
339
|
+
}[];
|
|
340
|
+
}
|
|
341
|
+
interface Machine {
|
|
342
|
+
organizationId: string;
|
|
343
|
+
name: string;
|
|
344
|
+
registeredBy?: string;
|
|
345
|
+
registeredAt: string;
|
|
346
|
+
byteRates?: {
|
|
347
|
+
produceBytesPerSecond: number;
|
|
348
|
+
consumeBytesPerSecond: number;
|
|
349
|
+
requestPercentage: number;
|
|
350
|
+
};
|
|
351
|
+
hidden: boolean;
|
|
352
|
+
}
|
|
219
353
|
export interface FakeControlPlane {
|
|
220
354
|
url: string;
|
|
221
355
|
server: Server;
|
|
@@ -242,11 +376,36 @@ export interface FakeControlPlane {
|
|
|
242
376
|
services: Map<string, Service>;
|
|
243
377
|
tokens: Map<string, Token>;
|
|
244
378
|
tombstones: Set<string>;
|
|
379
|
+
/** Every project's grants, by id. */
|
|
380
|
+
grants: Map<string, Grant>;
|
|
381
|
+
/** Registered machines, by `<organization>/<name>`. */
|
|
382
|
+
machines: Map<string, Machine>;
|
|
245
383
|
};
|
|
246
384
|
seed(seed: FakeSeed): void;
|
|
247
385
|
close(): Promise<void>;
|
|
248
386
|
}
|
|
387
|
+
/** The installation's cloud, as `GET /installation` reports it (feature 044). */
|
|
388
|
+
export interface FakeCloud {
|
|
389
|
+
provider: string;
|
|
390
|
+
account: string;
|
|
391
|
+
location: string;
|
|
392
|
+
kmsKey?: string;
|
|
393
|
+
}
|
|
249
394
|
export interface FakeSeed {
|
|
395
|
+
/** The installation's cloud (feature 044); `null` for an installation that names none, as until seeded. */
|
|
396
|
+
cloud?: FakeCloud | null;
|
|
397
|
+
/** Where the installation keeps new buckets (feature 039); Garage until a suite says otherwise. */
|
|
398
|
+
objectStore?: "garage" | "gcs";
|
|
399
|
+
/** Feature 041: start held, as a control plane whose own database was restored. */
|
|
400
|
+
held?: {
|
|
401
|
+
targetTime: string;
|
|
402
|
+
services?: {
|
|
403
|
+
project: string;
|
|
404
|
+
service: string;
|
|
405
|
+
recordedImage?: string;
|
|
406
|
+
clusterImage?: string;
|
|
407
|
+
}[];
|
|
408
|
+
};
|
|
250
409
|
organizations?: {
|
|
251
410
|
id: string;
|
|
252
411
|
name: string;
|
|
@@ -283,6 +442,9 @@ export interface FakeSeed {
|
|
|
283
442
|
provisionObjectStorage?: boolean;
|
|
284
443
|
exposeObjectStorage?: boolean;
|
|
285
444
|
ownObjectStore?: boolean;
|
|
445
|
+
/** The store its bucket is in, `garage` or `gcs`. */
|
|
446
|
+
objectStore?: string;
|
|
447
|
+
bucketLocation?: string;
|
|
286
448
|
}[];
|
|
287
449
|
/** Project secrets, by name and entry: a value each, never listed. */
|
|
288
450
|
secrets?: {
|
|
@@ -310,6 +472,22 @@ export interface FakeSeed {
|
|
|
310
472
|
name: string;
|
|
311
473
|
schema: unknown;
|
|
312
474
|
};
|
|
475
|
+
/** Its settings, each the installation's shipped default unless given; none for a topic from before settings. */
|
|
476
|
+
settings?: Partial<TopicSettingsRecord>;
|
|
477
|
+
}[];
|
|
478
|
+
/** Grants a project made, as if an owner had made them: accepted within its organization, pending to another, unless `state` says. */
|
|
479
|
+
grants?: {
|
|
480
|
+
projectId: string;
|
|
481
|
+
grantee: string;
|
|
482
|
+
target: FakeGrantTarget;
|
|
483
|
+
state?: string;
|
|
484
|
+
by?: string;
|
|
485
|
+
}[];
|
|
486
|
+
/** Machines registered on an organization. */
|
|
487
|
+
machines?: {
|
|
488
|
+
organizationId: string;
|
|
489
|
+
name: string;
|
|
490
|
+
byteRates?: Machine["byteRates"];
|
|
313
491
|
}[];
|
|
314
492
|
}
|
|
315
493
|
export declare function fakeControlPlane(options?: FakeControlPlaneOptions): Promise<FakeControlPlane>;
|