hammerkit 1.27.0-next.9 → 2.0.0-next.2
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/build.schema.json +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +6 -3
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- package/dist/main.js +2 -0
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-kubernetes.js +136 -13
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +415 -48
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +962 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.deviceLogin = void 0;
|
|
3
|
+
exports.deviceLogin = exports.NoSignInError = void 0;
|
|
4
4
|
const server_client_1 = require("./server-client");
|
|
5
5
|
const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
6
6
|
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
@@ -8,6 +8,11 @@ const SLOW_DOWN_SECONDS = 5;
|
|
|
8
8
|
const FETCH_TIMEOUT_MS = 10000;
|
|
9
9
|
const MIN_INTERVAL_SECONDS = 1;
|
|
10
10
|
const MAX_LIFETIME_SECONDS = 30 * 60;
|
|
11
|
+
// The server takes tokens of this issuer but offers no device sign-in for it: the token has
|
|
12
|
+
// to come from elsewhere, such as a token command.
|
|
13
|
+
class NoSignInError extends server_client_1.ServerError {
|
|
14
|
+
}
|
|
15
|
+
exports.NoSignInError = NoSignInError;
|
|
11
16
|
const isLoopbackHost = (hostname) => ['localhost', '127.0.0.1', '[::1]'].includes(hostname);
|
|
12
17
|
// An endpoint must use the scheme of the issuer: loopback http is for a test
|
|
13
18
|
// identity provider on this machine only, never beside an https issuer.
|
|
@@ -89,7 +94,7 @@ async function deviceLogin(server, options) {
|
|
|
89
94
|
throw new server_client_1.ServerError(`${server.url} does not accept tokens from ${server.issuer} (it accepts ${accepted})`);
|
|
90
95
|
}
|
|
91
96
|
if (!offered.clientId) {
|
|
92
|
-
throw new
|
|
97
|
+
throw new NoSignInError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
|
|
93
98
|
}
|
|
94
99
|
// What was pinned (by `remote add` or an earlier login) wins; the server can
|
|
95
100
|
// only offer the same, never something else to sign in with.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pinLogin = exports.setDefaultLocation = exports.removeServer = exports.addServer = exports.normalizeServerUrl = exports.validateServerName = exports.writeHostConfig = exports.readHostConfig = exports.getHostConfigPath = exports.hasServer = exports.hostConfigSchema = exports.hostServerSchema = exports.RESERVED_SERVER_NAMES = void 0;
|
|
3
|
+
exports.pinLogin = exports.setDefaultLocation = exports.removeServer = exports.addServer = exports.normalizeServerUrl = exports.validateServerName = exports.writeHostConfig = exports.readHostConfig = exports.getHostConfigPath = exports.getRegisteredServer = exports.getTokenCommand = exports.hasServer = exports.hostConfigSchema = exports.hostServerSchema = exports.RESERVED_SERVER_NAMES = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const yaml_1 = require("yaml");
|
|
7
7
|
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
8
|
+
const build_file_secret_provider_schema_1 = require("../schema/build-file-secret-provider-schema");
|
|
8
9
|
// Names with a meaning of their own in `run.on` / `--on`
|
|
9
10
|
exports.RESERVED_SERVER_NAMES = ['local', 'auto'];
|
|
10
11
|
const serverNamePattern = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
@@ -28,12 +29,25 @@ exports.hostServerSchema = zod_1.z
|
|
|
28
29
|
scope: zod_1.z.string().min(1).optional(),
|
|
29
30
|
})
|
|
30
31
|
.strict();
|
|
32
|
+
// A command that prints the token for a server instead of a login, see token-source.ts.
|
|
33
|
+
// It is bound to the url and issuer it was registered for, like a sign-in, and lives
|
|
34
|
+
// beside `servers` rather than in it: hammerkit before 2.0 refuses unknown keys of a
|
|
35
|
+
// server, but keeps unknown top-level keys. It is only ever read from the host config,
|
|
36
|
+
// a build file can't name one.
|
|
37
|
+
const tokenCommandSchema = zod_1.z
|
|
38
|
+
.object({ url: zod_1.z.string(), issuer: zod_1.z.string(), command: zod_1.z.string().trim().min(1) })
|
|
39
|
+
.passthrough();
|
|
31
40
|
// Unknown top-level keys are kept when the file is written back, so a newer
|
|
32
41
|
// hammerkit's settings survive an older one editing the file.
|
|
33
42
|
exports.hostConfigSchema = zod_1.z
|
|
34
43
|
.object({
|
|
35
44
|
servers: zod_1.z.record(exports.hostServerSchema).default({}),
|
|
45
|
+
tokenCommands: zod_1.z.record(tokenCommandSchema).optional(),
|
|
36
46
|
run: zod_1.z.object({ on: zod_1.z.string().optional() }).passthrough().default({}),
|
|
47
|
+
// how this machine reads secrets, beside what the build files declare (see
|
|
48
|
+
// collectSecretCatalog); a server reads only its own config
|
|
49
|
+
secretProviders: build_file_secret_provider_schema_1.buildFileSecretProvidersSchema.optional(),
|
|
50
|
+
secretAccounts: build_file_secret_provider_schema_1.buildFileSecretAccountsSchema.optional(),
|
|
37
51
|
})
|
|
38
52
|
.passthrough();
|
|
39
53
|
// Names like "constructor" must not find inherited members.
|
|
@@ -41,6 +55,35 @@ function hasServer(servers, name) {
|
|
|
41
55
|
return Object.prototype.hasOwnProperty.call(servers, name);
|
|
42
56
|
}
|
|
43
57
|
exports.hasServer = hasServer;
|
|
58
|
+
// The token command of a server, unless it was registered for another url or issuer (an
|
|
59
|
+
// older hammerkit that pointed the name elsewhere leaves it behind).
|
|
60
|
+
function getTokenCommand(config, name) {
|
|
61
|
+
const commands = config.tokenCommands ?? {};
|
|
62
|
+
const entry = Object.prototype.hasOwnProperty.call(commands, name) ? commands[name] : undefined;
|
|
63
|
+
const server = hasServer(config.servers, name) ? config.servers[name] : undefined;
|
|
64
|
+
return entry && server && entry.url === server.url && entry.issuer === server.issuer ? entry.command : undefined;
|
|
65
|
+
}
|
|
66
|
+
exports.getTokenCommand = getTokenCommand;
|
|
67
|
+
function getRegisteredServer(config, name) {
|
|
68
|
+
const tokenCommand = getTokenCommand(config, name);
|
|
69
|
+
return { ...config.servers[name], ...(tokenCommand ? { tokenCommand } : {}) };
|
|
70
|
+
}
|
|
71
|
+
exports.getRegisteredServer = getRegisteredServer;
|
|
72
|
+
function setTokenCommand(config, name, entry) {
|
|
73
|
+
const commands = { ...(config.tokenCommands ?? {}) };
|
|
74
|
+
if (entry) {
|
|
75
|
+
commands[name] = entry;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
delete commands[name];
|
|
79
|
+
}
|
|
80
|
+
if (Object.keys(commands).length > 0) {
|
|
81
|
+
config.tokenCommands = commands;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
delete config.tokenCommands;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
44
87
|
function getHostConfigPath(environment) {
|
|
45
88
|
return environment.processEnvs.HAMMERKIT_CONFIG ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'config.yaml');
|
|
46
89
|
}
|
|
@@ -121,16 +164,38 @@ async function addServer(environment, name, url, options) {
|
|
|
121
164
|
server[key] = options[key];
|
|
122
165
|
}
|
|
123
166
|
}
|
|
167
|
+
if (typeof options.tokenCommand === 'string' && !options.tokenCommand.trim()) {
|
|
168
|
+
throw new Error('the token command must not be empty, use --no-token-command to remove it');
|
|
169
|
+
}
|
|
124
170
|
const config = await readHostConfig(environment);
|
|
125
171
|
const existing = hasServer(config.servers, name) ? config.servers[name] : undefined;
|
|
126
|
-
const
|
|
172
|
+
const sameServer = (a, b) => a.url === b.url && a.issuer === b.issuer;
|
|
173
|
+
// Adding the same server again keeps what was pinned (by a login, too) unless it is given,
|
|
174
|
+
// so a later `remote add ... --token-command` needs no --force.
|
|
175
|
+
if (existing && sameServer(existing, server)) {
|
|
176
|
+
for (const key of PINNED) {
|
|
177
|
+
if (!server[key] && existing[key]) {
|
|
178
|
+
server[key] = existing[key];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const changed = (a, b) => !sameServer(a, b) || PINNED.some((key) => a[key] !== b[key]);
|
|
127
183
|
if (existing && changed(existing, server) && !options.force) {
|
|
128
184
|
throw new Error(`server "${name}" is already registered as ${existing.url} (issuer ${existing.issuer}); ` +
|
|
129
185
|
`use --force to point it somewhere else`);
|
|
130
186
|
}
|
|
187
|
+
// The command decides which identity's token goes to the server: changing it does not
|
|
188
|
+
// redirect anything, but a name pointed at another server does not keep the old one.
|
|
189
|
+
const kept = existing && sameServer(existing, server) ? getTokenCommand(config, name) : undefined;
|
|
190
|
+
const tokenCommand = typeof options.tokenCommand === 'string'
|
|
191
|
+
? options.tokenCommand.trim()
|
|
192
|
+
: options.tokenCommand === undefined
|
|
193
|
+
? kept
|
|
194
|
+
: undefined;
|
|
131
195
|
config.servers[name] = server;
|
|
196
|
+
setTokenCommand(config, name, tokenCommand ? { url: server.url, issuer: server.issuer, command: tokenCommand } : undefined);
|
|
132
197
|
await writeHostConfig(environment, config);
|
|
133
|
-
return server;
|
|
198
|
+
return { ...server, ...(tokenCommand ? { tokenCommand } : {}) };
|
|
134
199
|
}
|
|
135
200
|
exports.addServer = addServer;
|
|
136
201
|
async function removeServer(environment, name) {
|
|
@@ -139,6 +204,7 @@ async function removeServer(environment, name) {
|
|
|
139
204
|
throw new Error(`server "${name}" is not registered`);
|
|
140
205
|
}
|
|
141
206
|
delete config.servers[name];
|
|
207
|
+
setTokenCommand(config, name, undefined);
|
|
142
208
|
const resetDefault = config.run.on === name;
|
|
143
209
|
if (resetDefault) {
|
|
144
210
|
delete config.run.on;
|
|
@@ -40,6 +40,8 @@ passthrough = []) {
|
|
|
40
40
|
.addOption(new commander_1.Option('--client-id <id>', 'OAuth client to sign in with (pinned; default: what the server offers)'))
|
|
41
41
|
.addOption(new commander_1.Option('--audience <audience>', 'audience of the token (pinned; default: what the server offers)'))
|
|
42
42
|
.addOption(new commander_1.Option('--scope <scope>', 'scopes to request (pinned; default: what the server offers)'))
|
|
43
|
+
.addOption(new commander_1.Option('--token-command <command>', 'a command that prints the token for this server, instead of remote login (e.g. of a service account)'))
|
|
44
|
+
.addOption(new commander_1.Option('--no-token-command', 'remove the token command, sign in with remote login again'))
|
|
43
45
|
.addOption(new commander_1.Option('--force', 'point an already registered name at a different server').default(false))
|
|
44
46
|
.action(guarded('Remote add was not successful', async (name, url, options) => {
|
|
45
47
|
const server = await (0, host_config_1.addServer)(environment, name, url, {
|
|
@@ -47,9 +49,13 @@ passthrough = []) {
|
|
|
47
49
|
clientId: options.clientId,
|
|
48
50
|
audience: options.audience,
|
|
49
51
|
scope: options.scope,
|
|
52
|
+
tokenCommand: options.tokenCommand,
|
|
50
53
|
force: options.force,
|
|
51
54
|
});
|
|
52
55
|
environment.stdout.write(`added ${name}: ${server.url} (issuer ${server.issuer})\n`);
|
|
56
|
+
if (server.tokenCommand) {
|
|
57
|
+
environment.stdout.write(`tokens for ${name} come from: ${server.tokenCommand}\n`);
|
|
58
|
+
}
|
|
53
59
|
}));
|
|
54
60
|
remote
|
|
55
61
|
.command('list')
|
|
@@ -62,9 +68,10 @@ passthrough = []) {
|
|
|
62
68
|
return;
|
|
63
69
|
}
|
|
64
70
|
for (const name of names) {
|
|
65
|
-
const server = config
|
|
71
|
+
const server = (0, host_config_1.getRegisteredServer)(config, name);
|
|
66
72
|
const marker = config.run.on === name ? colors_1.default.green(' (default)') : '';
|
|
67
|
-
|
|
73
|
+
const token = server.tokenCommand ? ` token from: ${server.tokenCommand}` : '';
|
|
74
|
+
environment.stdout.write(`${colors_1.default.bold(name)}${marker} ${server.url} issuer ${server.issuer}${token}\n`);
|
|
68
75
|
}
|
|
69
76
|
if (config.run.on === 'auto' || config.run.on === 'local') {
|
|
70
77
|
environment.stdout.write(`default location: ${config.run.on}\n`);
|
|
@@ -95,17 +102,33 @@ passthrough = []) {
|
|
|
95
102
|
? `server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`
|
|
96
103
|
: 'name a server, or set one as the default with: hammerkit remote use <name>');
|
|
97
104
|
}
|
|
98
|
-
return { name: chosen, server: config
|
|
105
|
+
return { name: chosen, server: (0, host_config_1.getRegisteredServer)(config, chosen) };
|
|
99
106
|
};
|
|
100
107
|
remote
|
|
101
108
|
.command('login <name>')
|
|
102
109
|
.description('sign in to a registered server with your identity provider (device code, works headless)')
|
|
103
110
|
.action(guarded('Remote login was not successful', async (name) => {
|
|
104
111
|
const { server } = await getServer(name);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
if (server.tokenCommand) {
|
|
113
|
+
// a sign-in would never be used: the command's token goes first
|
|
114
|
+
throw new Error(`${name} takes its tokens from its token command (${server.tokenCommand}), it needs no sign-in; ` +
|
|
115
|
+
`to sign in instead, remove it with: hammerkit remote add ${name} ${server.url} --issuer ${server.issuer} --no-token-command`);
|
|
116
|
+
}
|
|
117
|
+
let login;
|
|
118
|
+
try {
|
|
119
|
+
login = await (0, device_login_1.deviceLogin)(server, {
|
|
120
|
+
prompt: (message) => environment.stdout.write(`${message}\n`),
|
|
121
|
+
signal: environment.abortCtrl.signal,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (e instanceof device_login_1.NoSignInError) {
|
|
126
|
+
throw new Error(`${e.message}: register a command that prints one, such as the token of a service account, with: ` +
|
|
127
|
+
`hammerkit remote add ${name} ${server.url} --issuer ${server.issuer} --token-command '<command>'`);
|
|
128
|
+
}
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
const { pinned, ...result } = login;
|
|
109
132
|
await (0, host_config_1.pinLogin)(environment, name, pinned);
|
|
110
133
|
await (0, credentials_1.saveToken)(environment, name, { url: server.url, issuer: server.issuer, ...result });
|
|
111
134
|
try {
|
|
@@ -155,6 +178,9 @@ passthrough = []) {
|
|
|
155
178
|
if (me.repos.length > 0) {
|
|
156
179
|
environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
|
|
157
180
|
}
|
|
181
|
+
if (me.observe) {
|
|
182
|
+
environment.stdout.write('you observe every run of the server, see them with: hammerkit remote runs --all\n');
|
|
183
|
+
}
|
|
158
184
|
}));
|
|
159
185
|
(0, remote_work_commands_1.registerWorkCommands)(remote, {
|
|
160
186
|
environment,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.describeTaskStats = exports.describeCapacity = exports.describeOverview = exports.describeServiceRun = exports.describeWork = exports.describeWait = void 0;
|
|
7
|
+
const colors_1 = __importDefault(require("colors"));
|
|
8
|
+
const capacity_format_1 = require("../server/capacity-format");
|
|
9
|
+
const timings_1 = require("../server/timings");
|
|
10
|
+
const wait_reason_1 = require("../server/wait-reason");
|
|
11
|
+
Object.defineProperty(exports, "describeWait", { enumerable: true, get: function () { return wait_reason_1.describeWait; } });
|
|
12
|
+
const server_client_1 = require("./server-client");
|
|
13
|
+
// What a run does, in a few words: its tasks, or the services it keeps up.
|
|
14
|
+
function describeWork(run) {
|
|
15
|
+
if (run.up) {
|
|
16
|
+
return `${(0, server_client_1.printable)(run.up.name)}: up ${run.up.services.map(server_client_1.printable).join(', ')}`;
|
|
17
|
+
}
|
|
18
|
+
return run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'by labels';
|
|
19
|
+
}
|
|
20
|
+
exports.describeWork = describeWork;
|
|
21
|
+
function ownerOf(run) {
|
|
22
|
+
return run.owner ? ` of ${(0, server_client_1.printable)(run.owner.sub)}` : '';
|
|
23
|
+
}
|
|
24
|
+
function describeMeter(name, meter) {
|
|
25
|
+
const full = meter.limit !== undefined && meter.used >= meter.limit;
|
|
26
|
+
const count = `${meter.used}${meter.limit !== undefined ? ` of ${meter.limit}` : ''}`;
|
|
27
|
+
return ` ${name.padEnd(9)} ${count.padEnd(9)} ${meter.runs.length > 0 ? meter.runs.map(server_client_1.printable).join(', ') : '-'}${full ? colors_1.default.yellow(' full') : ''}`;
|
|
28
|
+
}
|
|
29
|
+
// A service run on one line, then a line per endpoint: when it ends, when it sleeps.
|
|
30
|
+
function describeServiceRun(run) {
|
|
31
|
+
const times = [
|
|
32
|
+
run.state === 'up' || run.state === 'asleep' ? (run.expiresAt ? `until ${run.expiresAt}` : '') : '',
|
|
33
|
+
run.state === 'up' && run.sleepsAt ? `sleeps at ${run.sleepsAt}` : '',
|
|
34
|
+
run.wakes ? `woken ${run.wakes}×` : '',
|
|
35
|
+
].filter(Boolean);
|
|
36
|
+
return [
|
|
37
|
+
` ${(0, server_client_1.printable)(run.id)} ${(0, server_client_1.printable)(run.state)} ${describeWork(run)}${ownerOf(run)}${times.length > 0 ? ` ${(0, server_client_1.printable)(times.join(', '))}` : ''}`,
|
|
38
|
+
...(run.up?.endpoints ?? []).map((endpoint) => ` ${(0, server_client_1.printable)(endpoint.service)}:${endpoint.port}${endpoint.url ? ` ${(0, server_client_1.printable)(endpoint.url)}` : ''}`),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
exports.describeServiceRun = describeServiceRun;
|
|
42
|
+
// What `hammerkit remote status` prints: the places, the queue, the caller's limits.
|
|
43
|
+
function describeOverview(server, overview, now = Date.now()) {
|
|
44
|
+
const lines = [
|
|
45
|
+
`${(0, server_client_1.printable)(server)}: signed in as ${(0, server_client_1.printable)(overview.you.sub)}${overview.you.observe ? ', you observe every run' : ''}`,
|
|
46
|
+
`places: ${overview.places.used} of ${overview.places.max} taken (runs.maxConcurrent)`,
|
|
47
|
+
...overview.places.runs.map((run) => ` ${(0, server_client_1.printable)(run.id)} ${describeWork(run)}${ownerOf(run)}${run.startedAt ? ` since ${(0, server_client_1.printable)(run.startedAt)}, ${(0, timings_1.roughly)((0, timings_1.ranMs)(run, now) ?? 0)}` : ''}`),
|
|
48
|
+
];
|
|
49
|
+
if (overview.places.others > 0) {
|
|
50
|
+
lines.push(` ${overview.places.others} ${overview.places.others === 1 ? 'run' : 'runs'} of others`);
|
|
51
|
+
}
|
|
52
|
+
// a place a run holds that is not running: a service run waking up, or a place not given back
|
|
53
|
+
const unaccounted = overview.places.used - overview.places.runs.length - overview.places.others;
|
|
54
|
+
if (unaccounted > 0) {
|
|
55
|
+
lines.push(` ${unaccounted} taken by no running run`);
|
|
56
|
+
}
|
|
57
|
+
lines.push(`queue: ${overview.queue.length === 0 ? 'empty' : overview.queue.length}`);
|
|
58
|
+
for (const entry of overview.queue) {
|
|
59
|
+
const waited = entry.run ? (0, timings_1.waitedMs)(entry.run, now) : undefined;
|
|
60
|
+
const what = entry.run
|
|
61
|
+
? `${(0, server_client_1.printable)(entry.run.id)} ${describeWork(entry.run)}${ownerOf(entry.run)}${waited !== undefined ? ` waiting ${(0, timings_1.roughly)(waited)}` : ''}`
|
|
62
|
+
: 'a run of somebody else';
|
|
63
|
+
lines.push(` ${entry.position}. ${what} waits: ${(0, server_client_1.printable)((0, wait_reason_1.describeWait)(entry.waitsFor, entry.mine))}`);
|
|
64
|
+
}
|
|
65
|
+
lines.push(`waiting for an approval: ${overview.approvals.waiting} of at most ${overview.approvals.max}`);
|
|
66
|
+
for (const run of overview.approvals.runs) {
|
|
67
|
+
const accounts = (run.approvals ?? [])
|
|
68
|
+
.filter((approval) => !approval.approvedBy)
|
|
69
|
+
.map((approval) => approval.account);
|
|
70
|
+
lines.push(` ${(0, server_client_1.printable)(run.id)} ${describeWork(run)}${ownerOf(run)} needs ${accounts
|
|
71
|
+
.map(server_client_1.printable)
|
|
72
|
+
.join(', ')} until ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push(`service runs: ${overview.services.open}${overview.services.max !== undefined ? ` of ${overview.services.max}` : ''} places taken on the server`);
|
|
75
|
+
for (const run of overview.services.runs) {
|
|
76
|
+
lines.push(...describeServiceRun(run));
|
|
77
|
+
}
|
|
78
|
+
lines.push('your limits:', describeMeter('running', overview.limits.running), describeMeter('queued', overview.limits.queued), describeMeter('approval', overview.limits.waiting), describeMeter('services', overview.limits.services), ...(overview.limits.asleep ? [describeMeter('asleep', overview.limits.asleep)] : []));
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
exports.describeOverview = describeOverview;
|
|
82
|
+
// The machines of the server and what runs take of them, for `remote capacity`; `short` for
|
|
83
|
+
// the two lines of `remote status`.
|
|
84
|
+
function describeCapacity(capacity, short = false) {
|
|
85
|
+
const { machines, runs } = capacity;
|
|
86
|
+
const resources = (value) => value ? `${(0, capacity_format_1.formatCpu)(value.cpu)} CPU, ${(0, capacity_format_1.formatMemory)(value.memory)}` : '?';
|
|
87
|
+
const kind = capacity.backend === 'kubernetes' ? (machines?.count === 1 ? 'node' : 'nodes') : 'machine';
|
|
88
|
+
const lines = [
|
|
89
|
+
`capacity: ${machines
|
|
90
|
+
? `${machines.count} ${kind} with ${resources(machines.allocatable)}`
|
|
91
|
+
: 'what the machines have is not known'}${machines?.used ? `, ${resources(machines.used)} in use` : ''}${machines?.load ? `, load ${machines.load.map((load) => load.toFixed(2)).join(' ')}` : ''}`,
|
|
92
|
+
` runs: ${runs.pods !== undefined ? `${(0, capacity_format_1.plural)(runs.pods, 'pod')}, ` : ''}${runs.containers !== undefined ? `${runs.containers} containers, ` : ''}${capacity.backend === 'kubernetes' ? `requested ${resources(runs.requested)}` : ''}${runs.used ? `${capacity.backend === 'kubernetes' ? ', ' : ''}in use ${resources(runs.used)}` : ''}`,
|
|
93
|
+
];
|
|
94
|
+
if (short) {
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
for (const group of capacity.groups) {
|
|
98
|
+
lines.push(` ${(0, server_client_1.printable)(group.name)} ${group.kind === 'service-run' ? 'service run' : 'caller'}${group.owner ? ` of ${(0, server_client_1.printable)(group.owner.sub)}` : ''}${group.run ? ` ${(0, server_client_1.printable)(group.run)}` : ''} ${(0, capacity_format_1.plural)(group.pods ?? 0, 'pod')} requested ${resources(group.requested)}${group.quota ? ` of ${resources(group.quota)}` : ''}${group.used ? ` in use ${resources(group.used)}` : ''}`);
|
|
99
|
+
}
|
|
100
|
+
for (const problem of capacity.problems) {
|
|
101
|
+
lines.push(colors_1.default.yellow(` not shown: ${(0, server_client_1.printable)(problem)}`));
|
|
102
|
+
}
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
exports.describeCapacity = describeCapacity;
|
|
106
|
+
// What the past runs of each task took, for `remote tasks`.
|
|
107
|
+
function describeTaskStats(stats) {
|
|
108
|
+
const span = (ms) => (ms === undefined ? '-' : (0, timings_1.roughly)(ms));
|
|
109
|
+
return stats.map((entry) => `${(0, server_client_1.printable)(entry.task)} ${(0, server_client_1.printable)(entry.repo)} last ${(0, server_client_1.printable)(entry.last.status)} ${(0, server_client_1.printable)(entry.last.at)}${entry.last.durationMs !== undefined ? ` took ${(0, timings_1.roughly)(entry.last.durationMs)}` : ''} ${(0, capacity_format_1.plural)(entry.runs, 'run')}${entry.failed ? `, ${entry.failed} failed` : ''}${entry.cached ? `, ${entry.cached} cached` : ''} median ${span(entry.medianMs)}, 90% ${span(entry.p90Ms)}, waited ${span(entry.medianWaitMs)}`);
|
|
110
|
+
}
|
|
111
|
+
exports.describeTaskStats = describeTaskStats;
|
|
112
|
+
//# sourceMappingURL=remote-status.js.map
|