hammerkit 1.27.0-next.2 → 1.27.0-next.21
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 +27 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +1 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-task.js +5 -3
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +7 -0
- package/dist/executer/scheduler/state-key.js +42 -5
- 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 +207 -65
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +626 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/network/egress-proxy.js +45 -6
- 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 +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +15 -2
- 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/utils/task-arguments.js +3 -0
- 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-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +97 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +35 -13
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +64 -3
- package/dist/remote/remote-commands.js +41 -9
- package/dist/remote/remote-work-commands.js +246 -1
- 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 +5 -1
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/build-file-include-schema.js +10 -4
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/schema-parser.js +36 -4
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +56 -5
- package/dist/server/kubernetes-backend.js +303 -64
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +126 -62
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +64 -3
- package/dist/server/run-worker.js +146 -10
- package/dist/server/runs.js +347 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +213 -14
- package/dist/server/server.js +268 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
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");
|
|
@@ -28,11 +28,20 @@ exports.hostServerSchema = zod_1.z
|
|
|
28
28
|
scope: zod_1.z.string().min(1).optional(),
|
|
29
29
|
})
|
|
30
30
|
.strict();
|
|
31
|
+
// A command that prints the token for a server instead of a login, see token-source.ts.
|
|
32
|
+
// It is bound to the url and issuer it was registered for, like a sign-in, and lives
|
|
33
|
+
// beside `servers` rather than in it: hammerkit before 1.27 refuses unknown keys of a
|
|
34
|
+
// server, but keeps unknown top-level keys. It is only ever read from the host config,
|
|
35
|
+
// a build file can't name one.
|
|
36
|
+
const tokenCommandSchema = zod_1.z
|
|
37
|
+
.object({ url: zod_1.z.string(), issuer: zod_1.z.string(), command: zod_1.z.string().trim().min(1) })
|
|
38
|
+
.passthrough();
|
|
31
39
|
// Unknown top-level keys are kept when the file is written back, so a newer
|
|
32
40
|
// hammerkit's settings survive an older one editing the file.
|
|
33
41
|
exports.hostConfigSchema = zod_1.z
|
|
34
42
|
.object({
|
|
35
43
|
servers: zod_1.z.record(exports.hostServerSchema).default({}),
|
|
44
|
+
tokenCommands: zod_1.z.record(tokenCommandSchema).optional(),
|
|
36
45
|
run: zod_1.z.object({ on: zod_1.z.string().optional() }).passthrough().default({}),
|
|
37
46
|
})
|
|
38
47
|
.passthrough();
|
|
@@ -41,6 +50,35 @@ function hasServer(servers, name) {
|
|
|
41
50
|
return Object.prototype.hasOwnProperty.call(servers, name);
|
|
42
51
|
}
|
|
43
52
|
exports.hasServer = hasServer;
|
|
53
|
+
// The token command of a server, unless it was registered for another url or issuer (an
|
|
54
|
+
// older hammerkit that pointed the name elsewhere leaves it behind).
|
|
55
|
+
function getTokenCommand(config, name) {
|
|
56
|
+
const commands = config.tokenCommands ?? {};
|
|
57
|
+
const entry = Object.prototype.hasOwnProperty.call(commands, name) ? commands[name] : undefined;
|
|
58
|
+
const server = hasServer(config.servers, name) ? config.servers[name] : undefined;
|
|
59
|
+
return entry && server && entry.url === server.url && entry.issuer === server.issuer ? entry.command : undefined;
|
|
60
|
+
}
|
|
61
|
+
exports.getTokenCommand = getTokenCommand;
|
|
62
|
+
function getRegisteredServer(config, name) {
|
|
63
|
+
const tokenCommand = getTokenCommand(config, name);
|
|
64
|
+
return { ...config.servers[name], ...(tokenCommand ? { tokenCommand } : {}) };
|
|
65
|
+
}
|
|
66
|
+
exports.getRegisteredServer = getRegisteredServer;
|
|
67
|
+
function setTokenCommand(config, name, entry) {
|
|
68
|
+
const commands = { ...(config.tokenCommands ?? {}) };
|
|
69
|
+
if (entry) {
|
|
70
|
+
commands[name] = entry;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
delete commands[name];
|
|
74
|
+
}
|
|
75
|
+
if (Object.keys(commands).length > 0) {
|
|
76
|
+
config.tokenCommands = commands;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
delete config.tokenCommands;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
44
82
|
function getHostConfigPath(environment) {
|
|
45
83
|
return environment.processEnvs.HAMMERKIT_CONFIG ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'config.yaml');
|
|
46
84
|
}
|
|
@@ -121,16 +159,38 @@ async function addServer(environment, name, url, options) {
|
|
|
121
159
|
server[key] = options[key];
|
|
122
160
|
}
|
|
123
161
|
}
|
|
162
|
+
if (typeof options.tokenCommand === 'string' && !options.tokenCommand.trim()) {
|
|
163
|
+
throw new Error('the token command must not be empty, use --no-token-command to remove it');
|
|
164
|
+
}
|
|
124
165
|
const config = await readHostConfig(environment);
|
|
125
166
|
const existing = hasServer(config.servers, name) ? config.servers[name] : undefined;
|
|
126
|
-
const
|
|
167
|
+
const sameServer = (a, b) => a.url === b.url && a.issuer === b.issuer;
|
|
168
|
+
// Adding the same server again keeps what was pinned (by a login, too) unless it is given,
|
|
169
|
+
// so a later `remote add ... --token-command` needs no --force.
|
|
170
|
+
if (existing && sameServer(existing, server)) {
|
|
171
|
+
for (const key of PINNED) {
|
|
172
|
+
if (!server[key] && existing[key]) {
|
|
173
|
+
server[key] = existing[key];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const changed = (a, b) => !sameServer(a, b) || PINNED.some((key) => a[key] !== b[key]);
|
|
127
178
|
if (existing && changed(existing, server) && !options.force) {
|
|
128
179
|
throw new Error(`server "${name}" is already registered as ${existing.url} (issuer ${existing.issuer}); ` +
|
|
129
180
|
`use --force to point it somewhere else`);
|
|
130
181
|
}
|
|
182
|
+
// The command decides which identity's token goes to the server: changing it does not
|
|
183
|
+
// redirect anything, but a name pointed at another server does not keep the old one.
|
|
184
|
+
const kept = existing && sameServer(existing, server) ? getTokenCommand(config, name) : undefined;
|
|
185
|
+
const tokenCommand = typeof options.tokenCommand === 'string'
|
|
186
|
+
? options.tokenCommand.trim()
|
|
187
|
+
: options.tokenCommand === undefined
|
|
188
|
+
? kept
|
|
189
|
+
: undefined;
|
|
131
190
|
config.servers[name] = server;
|
|
191
|
+
setTokenCommand(config, name, tokenCommand ? { url: server.url, issuer: server.issuer, command: tokenCommand } : undefined);
|
|
132
192
|
await writeHostConfig(environment, config);
|
|
133
|
-
return server;
|
|
193
|
+
return { ...server, ...(tokenCommand ? { tokenCommand } : {}) };
|
|
134
194
|
}
|
|
135
195
|
exports.addServer = addServer;
|
|
136
196
|
async function removeServer(environment, name) {
|
|
@@ -139,6 +199,7 @@ async function removeServer(environment, name) {
|
|
|
139
199
|
throw new Error(`server "${name}" is not registered`);
|
|
140
200
|
}
|
|
141
201
|
delete config.servers[name];
|
|
202
|
+
setTokenCommand(config, name, undefined);
|
|
142
203
|
const resetDefault = config.run.on === name;
|
|
143
204
|
if (resetDefault) {
|
|
144
205
|
delete config.run.on;
|
|
@@ -16,7 +16,9 @@ const remote_work_commands_1 = require("./remote-work-commands");
|
|
|
16
16
|
const mcp_command_1 = require("../harness/mcp-command");
|
|
17
17
|
// Host-level commands: they only touch the machine's config file, so they work
|
|
18
18
|
// with or without a build file in the working directory.
|
|
19
|
-
function registerRemoteCommands(program, environment, buildFile
|
|
19
|
+
function registerRemoteCommands(program, environment, buildFile,
|
|
20
|
+
// what follows `--`, the arguments of `remote run`
|
|
21
|
+
passthrough = []) {
|
|
20
22
|
const remote = program.command('remote').description('register the servers runs can be sent to');
|
|
21
23
|
const guarded = (failure, fn) => {
|
|
22
24
|
return async (...args) => {
|
|
@@ -38,6 +40,8 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
38
40
|
.addOption(new commander_1.Option('--client-id <id>', 'OAuth client to sign in with (pinned; default: what the server offers)'))
|
|
39
41
|
.addOption(new commander_1.Option('--audience <audience>', 'audience of the token (pinned; default: what the server offers)'))
|
|
40
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'))
|
|
41
45
|
.addOption(new commander_1.Option('--force', 'point an already registered name at a different server').default(false))
|
|
42
46
|
.action(guarded('Remote add was not successful', async (name, url, options) => {
|
|
43
47
|
const server = await (0, host_config_1.addServer)(environment, name, url, {
|
|
@@ -45,9 +49,13 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
45
49
|
clientId: options.clientId,
|
|
46
50
|
audience: options.audience,
|
|
47
51
|
scope: options.scope,
|
|
52
|
+
tokenCommand: options.tokenCommand,
|
|
48
53
|
force: options.force,
|
|
49
54
|
});
|
|
50
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
|
+
}
|
|
51
59
|
}));
|
|
52
60
|
remote
|
|
53
61
|
.command('list')
|
|
@@ -60,9 +68,10 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
60
68
|
return;
|
|
61
69
|
}
|
|
62
70
|
for (const name of names) {
|
|
63
|
-
const server = config
|
|
71
|
+
const server = (0, host_config_1.getRegisteredServer)(config, name);
|
|
64
72
|
const marker = config.run.on === name ? colors_1.default.green(' (default)') : '';
|
|
65
|
-
|
|
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`);
|
|
66
75
|
}
|
|
67
76
|
if (config.run.on === 'auto' || config.run.on === 'local') {
|
|
68
77
|
environment.stdout.write(`default location: ${config.run.on}\n`);
|
|
@@ -93,17 +102,33 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
93
102
|
? `server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`
|
|
94
103
|
: 'name a server, or set one as the default with: hammerkit remote use <name>');
|
|
95
104
|
}
|
|
96
|
-
return { name: chosen, server: config
|
|
105
|
+
return { name: chosen, server: (0, host_config_1.getRegisteredServer)(config, chosen) };
|
|
97
106
|
};
|
|
98
107
|
remote
|
|
99
108
|
.command('login <name>')
|
|
100
109
|
.description('sign in to a registered server with your identity provider (device code, works headless)')
|
|
101
110
|
.action(guarded('Remote login was not successful', async (name) => {
|
|
102
111
|
const { server } = await getServer(name);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
132
|
await (0, host_config_1.pinLogin)(environment, name, pinned);
|
|
108
133
|
await (0, credentials_1.saveToken)(environment, name, { url: server.url, issuer: server.issuer, ...result });
|
|
109
134
|
try {
|
|
@@ -154,7 +179,14 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
154
179
|
environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
|
|
155
180
|
}
|
|
156
181
|
}));
|
|
157
|
-
(0, remote_work_commands_1.registerWorkCommands)(remote, {
|
|
182
|
+
(0, remote_work_commands_1.registerWorkCommands)(remote, {
|
|
183
|
+
environment,
|
|
184
|
+
buildFile,
|
|
185
|
+
getServer,
|
|
186
|
+
guarded,
|
|
187
|
+
passthrough,
|
|
188
|
+
fail: (message, exitCode) => program.error(message, { exitCode }),
|
|
189
|
+
});
|
|
158
190
|
(0, mcp_command_1.registerMcpCommand)(remote, environment);
|
|
159
191
|
}
|
|
160
192
|
exports.registerRemoteCommands = registerRemoteCommands;
|
|
@@ -12,7 +12,23 @@ const bundle_1 = require("./bundle");
|
|
|
12
12
|
const annotations_1 = require("./annotations");
|
|
13
13
|
const server_client_1 = require("./server-client");
|
|
14
14
|
const bundle_text_1 = require("./bundle-text");
|
|
15
|
+
const sandbox_client_1 = require("../harness/sandbox-client");
|
|
16
|
+
const units_1 = require("../utils/units");
|
|
17
|
+
const service_tools_1 = require("../harness/service-tools");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
const check_keys_1 = require("../optimizer/check-keys");
|
|
15
20
|
const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
|
|
21
|
+
// how a check passed, one line
|
|
22
|
+
function describePassed(passed) {
|
|
23
|
+
const from = passed.source.type === 'ref'
|
|
24
|
+
? `${passed.source.ref} at ${passed.source.commit.slice(0, 12)}`
|
|
25
|
+
: `a check of sandbox ${passed.source.sandbox}, fetched at ${passed.source.commit.slice(0, 12)}`;
|
|
26
|
+
return `passed ${passed.finishedAt}${passed.status === 'cached'
|
|
27
|
+
? ' from the cache'
|
|
28
|
+
: passed.durationMs !== undefined
|
|
29
|
+
? ` in ${(0, units_1.formatDuration)(passed.durationMs)}`
|
|
30
|
+
: ''}, ${passed.run} by ${passed.by.sub}, from ${from}`;
|
|
31
|
+
}
|
|
16
32
|
// what the publisher said about where a version comes from, one line
|
|
17
33
|
// What a publisher said about where a version comes from: the server takes it as said,
|
|
18
34
|
// so anybody else is told it is not checked.
|
|
@@ -29,6 +45,11 @@ function yankLines(version) {
|
|
|
29
45
|
]
|
|
30
46
|
: [];
|
|
31
47
|
}
|
|
48
|
+
// A line a task printed, shown as it is but for control characters, so it can't rewrite
|
|
49
|
+
// the terminal; tabs stay.
|
|
50
|
+
function printableLine(text) {
|
|
51
|
+
return text.split('\t').map(server_client_1.printableWhole).join('\t');
|
|
52
|
+
}
|
|
32
53
|
const collectOption = (value, previous = []) => [...previous, value];
|
|
33
54
|
// What a person who approves is shown of a run: everything the decision rests on.
|
|
34
55
|
function describeApproval(run) {
|
|
@@ -89,7 +110,22 @@ function describePlan(plan) {
|
|
|
89
110
|
}
|
|
90
111
|
exports.describePlan = describePlan;
|
|
91
112
|
function registerWorkCommands(remote, context) {
|
|
92
|
-
const { environment, buildFile, getServer, guarded } = context;
|
|
113
|
+
const { environment, buildFile, getServer, guarded, passthrough, fail } = context;
|
|
114
|
+
// the ref a run starts from: what the options name, else the checkout here
|
|
115
|
+
const refSource = async (options) => {
|
|
116
|
+
const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
|
|
117
|
+
const repo = options.repo ?? checkout.repo;
|
|
118
|
+
const ref = options.ref ?? checkout.branch;
|
|
119
|
+
// the commit checked out here is the code meant, when the ref is the branch it is on
|
|
120
|
+
const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
|
|
121
|
+
if (!repo) {
|
|
122
|
+
throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
|
|
123
|
+
}
|
|
124
|
+
if (!ref) {
|
|
125
|
+
throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
|
|
126
|
+
}
|
|
127
|
+
return { type: 'ref', repo, ref, ...(commit ? { commit } : {}) };
|
|
128
|
+
};
|
|
93
129
|
const connect = async (name) => {
|
|
94
130
|
const chosen = await getServer(name);
|
|
95
131
|
// a CI job or an agent brings its own token, a person signs in
|
|
@@ -122,6 +158,215 @@ function registerWorkCommands(remote, context) {
|
|
|
122
158
|
environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
|
|
123
159
|
}
|
|
124
160
|
}));
|
|
161
|
+
remote
|
|
162
|
+
.command('run <tasks...>')
|
|
163
|
+
.description('run tasks of a published build file on a server from a git ref, print their output and exit as they did')
|
|
164
|
+
.addOption(new commander_1.Option('--server <name>', 'the server to run on; the default of remote use when left out'))
|
|
165
|
+
.addOption(new commander_1.Option('--build <hash>', 'the published version to run (sha256:...), or HAMMERKIT_BUILD; the hash of the build file here when left out'))
|
|
166
|
+
.addOption(new commander_1.Option('--repo <host/path>', 'the repository, such as github.com/corp/payments; origin of the checkout here when left out'))
|
|
167
|
+
.addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to run; the branch checked out here when left out'))
|
|
168
|
+
.addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at, else the run is refused; the commit checked out here when --ref is left out'))
|
|
169
|
+
.addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
|
|
170
|
+
.addOption(new commander_1.Option('--cache <method>', 'as --cache of run').choices(['checksum', 'modify-date', 'none']))
|
|
171
|
+
.addOption(new commander_1.Option('--timeout <duration>', 'as --timeout of run: for tasks without their own, such as 30m'))
|
|
172
|
+
.addOption(new commander_1.Option('--max-timeout <duration>', 'as --max-timeout of run: for every task, also one with its own timeout'))
|
|
173
|
+
.addOption(new commander_1.Option('--detach', 'submit the run and exit, without following it: for a check that CI verifies later'))
|
|
174
|
+
.action(guarded('Remote run was not successful', async (tasks, options) => {
|
|
175
|
+
const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
|
|
176
|
+
const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
|
|
177
|
+
if (!bundle_1.HASH_PATTERN.test(definition)) {
|
|
178
|
+
throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
|
|
179
|
+
}
|
|
180
|
+
const source = await refSource(options);
|
|
181
|
+
const ref = source.ref;
|
|
182
|
+
const { name: serverName, server } = await getServer(options.server);
|
|
183
|
+
const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
|
|
184
|
+
const runOptions = {};
|
|
185
|
+
for (const key of ['env', 'cache', 'timeout', 'maxTimeout']) {
|
|
186
|
+
if (options[key] !== undefined) {
|
|
187
|
+
runOptions[key] = options[key];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (passthrough.length > 0) {
|
|
191
|
+
runOptions.args = passthrough;
|
|
192
|
+
}
|
|
193
|
+
let submitted;
|
|
194
|
+
try {
|
|
195
|
+
submitted = await (0, sandbox_client_1.submitRun)(connection, {
|
|
196
|
+
definition,
|
|
197
|
+
tasks,
|
|
198
|
+
source,
|
|
199
|
+
options: runOptions,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
if (e.status === 400 && !named && /no published build file/.test(String(e))) {
|
|
204
|
+
throw new Error(`the build file here is not published on ${serverName} (${definition}), publish it first`);
|
|
205
|
+
}
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
const at = submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : '';
|
|
209
|
+
environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${tasks.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(ref)}${at}\n`);
|
|
210
|
+
if (options.detach) {
|
|
211
|
+
environment.stdout.write(`follow it with: hammerkit remote show ${(0, server_client_1.printable)(submitted.id)}\n`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const prefixed = tasks.length > 1;
|
|
215
|
+
const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
|
|
216
|
+
signal: environment.abortCtrl.signal,
|
|
217
|
+
onLine: (line) => {
|
|
218
|
+
environment.stdout.write(`${prefixed || line.task !== tasks[0] ? `[${(0, server_client_1.printable)(line.task)}] ` : ''}${printableLine(line.text)}\n`);
|
|
219
|
+
},
|
|
220
|
+
onWaiting: (run) => {
|
|
221
|
+
for (const approval of run.approvals ?? []) {
|
|
222
|
+
environment.stdout.write(`waiting until somebody who uses ${(0, server_client_1.printable)(approval.account)} approves it, before ${(0, server_client_1.printable)(approval.expiresAt)}: hammerkit remote approve ${(0, server_client_1.printable)(run.id)}\n`);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const run = followed.run;
|
|
227
|
+
if (run.state === 'succeeded') {
|
|
228
|
+
environment.stdout.write(`${(0, server_client_1.printable)(run.id)} succeeded\n`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// the exit code of the first task that failed, as `hammerkit run` would end
|
|
232
|
+
const failed = tasks
|
|
233
|
+
.map((task) => run.results?.[task])
|
|
234
|
+
.concat(Object.values(run.results ?? {}))
|
|
235
|
+
.find((result) => result?.status === 'failed');
|
|
236
|
+
const exitCode = failed?.exitCode && failed.exitCode > 0 ? failed.exitCode : 1;
|
|
237
|
+
fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, exitCode);
|
|
238
|
+
}));
|
|
239
|
+
remote
|
|
240
|
+
.command('up <services...>')
|
|
241
|
+
.description('start services of a published build file on a server from a git ref and keep them up until their ttl ends, ' +
|
|
242
|
+
'with their endpoints at addresses of their own behind the sign-in of the server')
|
|
243
|
+
.addOption(new commander_1.Option('--server <name>', 'the server to run on; the default of remote use when left out'))
|
|
244
|
+
.addOption(new commander_1.Option('--endpoint <service:port>', 'publish this port of a service, such as web:8080; may be given more than once').argParser(collectOption))
|
|
245
|
+
.addOption(new commander_1.Option('--ttl <duration>', 'how long they stay once ready, such as 2h, then they are removed; the server default when left out'))
|
|
246
|
+
.addOption(new commander_1.Option('--sleep-ttl <duration>', 'how long they stay up without a request, such as 30m, then they sleep until the next request wakes them; ' +
|
|
247
|
+
'the server default when left out'))
|
|
248
|
+
.addOption(new commander_1.Option('--name <name>', 'a later run of yours with the same name replaces this one and keeps its addresses; the branch when left out'))
|
|
249
|
+
.addOption(new commander_1.Option('--build <hash>', 'the published version to run (sha256:...), or HAMMERKIT_BUILD; the hash of the build file here when left out'))
|
|
250
|
+
.addOption(new commander_1.Option('--repo <host/path>', 'the repository; origin of the checkout here when left out'))
|
|
251
|
+
.addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to start; the branch checked out here when left out'))
|
|
252
|
+
.addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at; the commit checked out here when --ref is left out'))
|
|
253
|
+
.addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
|
|
254
|
+
.action(guarded('Remote up was not successful', async (services, options) => {
|
|
255
|
+
const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
|
|
256
|
+
const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
|
|
257
|
+
if (!bundle_1.HASH_PATTERN.test(definition)) {
|
|
258
|
+
throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
|
|
259
|
+
}
|
|
260
|
+
const endpoints = (options.endpoint ?? []).map(service_tools_1.parseEndpoint);
|
|
261
|
+
const source = await refSource(options);
|
|
262
|
+
const { name: serverName, server } = await getServer(options.server);
|
|
263
|
+
const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
|
|
264
|
+
const submitted = await (0, sandbox_client_1.submitRun)(connection, {
|
|
265
|
+
definition,
|
|
266
|
+
services,
|
|
267
|
+
endpoints,
|
|
268
|
+
...(options.ttl ? { ttl: options.ttl } : {}),
|
|
269
|
+
...(options.sleepTtl ? { sleepTtl: options.sleepTtl } : {}),
|
|
270
|
+
...(options.name ? { name: options.name } : {}),
|
|
271
|
+
source,
|
|
272
|
+
options: options.env ? { env: options.env } : {},
|
|
273
|
+
});
|
|
274
|
+
environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${services.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(source.ref)}${submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : ''}\n`);
|
|
275
|
+
const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
|
|
276
|
+
signal: environment.abortCtrl.signal,
|
|
277
|
+
untilUp: true,
|
|
278
|
+
onLine: (line) => environment.stdout.write(`[${(0, server_client_1.printable)(line.task)}] ${printableLine(line.text)}\n`),
|
|
279
|
+
onWaiting: (run) => {
|
|
280
|
+
for (const approval of run.approvals ?? []) {
|
|
281
|
+
environment.stdout.write(`waiting until somebody who uses ${(0, server_client_1.printable)(approval.account)} approves it, before ${(0, server_client_1.printable)(approval.expiresAt)}: hammerkit remote approve ${(0, server_client_1.printable)(run.id)}\n`);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const run = followed.run;
|
|
286
|
+
if (run.state !== 'up') {
|
|
287
|
+
fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, 1);
|
|
288
|
+
}
|
|
289
|
+
environment.stdout.write(`${(0, server_client_1.printable)(run.id)} is up until ${(0, server_client_1.printable)(run.expiresAt ?? '-')}${run.up?.sleepTtl ? `, asleep after ${(0, units_1.formatDuration)(run.up.sleepTtl)} without a request` : ''}\n`);
|
|
290
|
+
for (const endpoint of run.up?.endpoints ?? []) {
|
|
291
|
+
environment.stdout.write(` ${(0, server_client_1.printable)(endpoint.service)}:${endpoint.port} ${(0, server_client_1.printable)(endpoint.url)}\n`);
|
|
292
|
+
}
|
|
293
|
+
environment.stdout.write(`extend it with: hammerkit remote extend ${(0, server_client_1.printable)(run.id)} 8h, stop it with: hammerkit remote stop ${(0, server_client_1.printable)(run.id)}\n`);
|
|
294
|
+
}));
|
|
295
|
+
remote
|
|
296
|
+
.command('extend <run> <ttl> [server]')
|
|
297
|
+
.description('keep a service run that is up for this long from now, such as 8h, at most the maxTtl of the server')
|
|
298
|
+
.action(guarded('Remote extend was not successful', async (id, ttl, name) => {
|
|
299
|
+
if (!RUN_ID.test(id)) {
|
|
300
|
+
throw new Error(`${(0, server_client_1.printable)(id)} is not a run id (run_...)`);
|
|
301
|
+
}
|
|
302
|
+
const { name: serverName, server, token } = await connect(name);
|
|
303
|
+
const run = (await (0, server_client_1.callServer)(server, token, 'POST', `/v1/runs/${id}/extend`, { ttl })).body;
|
|
304
|
+
environment.stdout.write(`${(0, server_client_1.printable)(run.id)} on ${serverName} stays up until ${(0, server_client_1.printable)(run.expiresAt ?? '-')}\n`);
|
|
305
|
+
}));
|
|
306
|
+
remote
|
|
307
|
+
.command('stop <run> [server]')
|
|
308
|
+
.description('stop a run: a service run takes its services and endpoints down')
|
|
309
|
+
.action(guarded('Remote stop was not successful', async (id, name) => {
|
|
310
|
+
if (!RUN_ID.test(id)) {
|
|
311
|
+
throw new Error(`${(0, server_client_1.printable)(id)} is not a run id (run_...)`);
|
|
312
|
+
}
|
|
313
|
+
const { name: serverName, server, token } = await connect(name);
|
|
314
|
+
const run = (await (0, server_client_1.callServer)(server, token, 'POST', `/v1/runs/${id}/cancel`)).body;
|
|
315
|
+
environment.stdout.write(`${(0, server_client_1.printable)(run.id)} on ${serverName}: ${run.state === 'up' || run.state === 'running' ? 'stopping' : (0, server_client_1.printable)(run.state)}\n`);
|
|
316
|
+
}));
|
|
317
|
+
remote
|
|
318
|
+
.command('verify <tasks...>')
|
|
319
|
+
.description('check that the server already passed tasks for the files checked out here, without running them; ' +
|
|
320
|
+
'exits 1 at once for any it did not')
|
|
321
|
+
.addOption(new commander_1.Option('--server <name>', 'the server to ask; the default of remote use when left out'))
|
|
322
|
+
.addOption(new commander_1.Option('--repo <host/path>', 'the repository, such as github.com/corp/payments; origin of the checkout here when left out'))
|
|
323
|
+
.addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
|
|
324
|
+
.action(guarded('Remote verify was not successful', async (tasks, options) => {
|
|
325
|
+
const repo = options.repo ?? (await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile))).repo;
|
|
326
|
+
if (!repo) {
|
|
327
|
+
throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
|
|
328
|
+
}
|
|
329
|
+
const keys = [];
|
|
330
|
+
const unkeyed = [];
|
|
331
|
+
for (const task of tasks) {
|
|
332
|
+
const found = (await (0, check_keys_1.getCheckKeys)(buildFile, environment, { taskName: task, environmentName: options.env ?? null }))[task];
|
|
333
|
+
if (!found) {
|
|
334
|
+
throw new Error(`the build file has no task ${(0, server_client_1.printable)(task)}`);
|
|
335
|
+
}
|
|
336
|
+
if (found.key === null) {
|
|
337
|
+
unkeyed.push({ task, reason: found.reason });
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
keys.push({ task, key: found.key });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const { name: serverName, server, token } = await connect(options.server);
|
|
344
|
+
const answer = keys.length > 0
|
|
345
|
+
? await (0, server_client_1.callServer)(server, token, 'POST', '/v1/checks/lookup', { repo, keys })
|
|
346
|
+
: { body: { checks: [] } };
|
|
347
|
+
const missing = [];
|
|
348
|
+
// by what was asked, never by what was answered: a task the answer leaves out, or
|
|
349
|
+
// answers for another key, is not verified
|
|
350
|
+
for (const { task, key } of keys) {
|
|
351
|
+
const check = answer.body.checks.find((answered) => answered.task === task && answered.key === key);
|
|
352
|
+
if (check?.passed) {
|
|
353
|
+
environment.stdout.write(`${(0, server_client_1.printable)(task)}: ${(0, server_client_1.printable)(describePassed(check.passed))}\n`);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
missing.push(task);
|
|
357
|
+
environment.stdout.write(`${(0, server_client_1.printable)(task)}: no run on ${serverName} passed it for these files${check?.reason ? `, ${(0, server_client_1.printable)(check.reason)}` : ''}\n`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const { task, reason } of unkeyed) {
|
|
361
|
+
environment.stdout.write(`${(0, server_client_1.printable)(task)}: can not be verified, ${(0, server_client_1.printable)(reason)}\n`);
|
|
362
|
+
}
|
|
363
|
+
if (missing.length > 0 || unkeyed.length > 0) {
|
|
364
|
+
const run = missing.length > 0 ? `; run on the server: hammerkit remote run ${missing.map(server_client_1.printable).join(' ')}` : '';
|
|
365
|
+
// whole names: a task of an include is named like build:all
|
|
366
|
+
const names = [...missing, ...unkeyed.map(({ task }) => task)].map(server_client_1.printable);
|
|
367
|
+
fail(`not verified: ${names.join(', ')}${run}`, 1);
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
125
370
|
remote
|
|
126
371
|
.command('versions [server]')
|
|
127
372
|
.description('list the published versions of build files on a server, newest first, with where they come from')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printable = exports.ServerError = void 0;
|
|
3
|
+
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printableLines = exports.printable = exports.ServerError = void 0;
|
|
4
4
|
const discovery_1 = require("../server/discovery");
|
|
5
5
|
const TIMEOUT_MS = 10000;
|
|
6
6
|
const WRITE_TIMEOUT_MS = 30000;
|
|
@@ -21,6 +21,17 @@ function printable(value) {
|
|
|
21
21
|
return text.length > 300 ? `${text.slice(0, 300)}… (+${text.length - 300} chars)` : text;
|
|
22
22
|
}
|
|
23
23
|
exports.printable = printable;
|
|
24
|
+
// Text of several lines, such as every problem of a refused publish: each line made
|
|
25
|
+
// printable and cut like `printable`, one per line, and at most `MAX_LINES` of them.
|
|
26
|
+
const MAX_LINES = 100;
|
|
27
|
+
function printableLines(value) {
|
|
28
|
+
const lines = String(value)
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.filter((line) => line.trim().length > 0);
|
|
31
|
+
const shown = lines.slice(0, MAX_LINES).map(printable);
|
|
32
|
+
return lines.length > MAX_LINES ? [...shown, `… (+${lines.length - MAX_LINES} lines)`] : shown;
|
|
33
|
+
}
|
|
34
|
+
exports.printableLines = printableLines;
|
|
24
35
|
// The same, never cut: for what somebody decides on, such as a command.
|
|
25
36
|
function printableWhole(value) {
|
|
26
37
|
return String(value)
|
|
@@ -92,7 +103,9 @@ async function callServer(server, token, method, path, body) {
|
|
|
92
103
|
throw new ServerError(`${server.url} did not accept the token`);
|
|
93
104
|
}
|
|
94
105
|
if (!response.ok) {
|
|
95
|
-
const
|
|
106
|
+
const lines = answer && typeof answer.error === 'string' ? printableLines(answer.error) : [];
|
|
107
|
+
// a refused publish says every problem, each on a line of its own
|
|
108
|
+
const said = lines.length > 1 ? `:\n${lines.map((line) => ` ${line}`).join('\n')}` : lines.length === 1 ? `: ${lines[0]}` : '';
|
|
96
109
|
throw new ServerError(`${server.url} answered ${response.status}${said}`, response.status);
|
|
97
110
|
}
|
|
98
111
|
return { status: response.status, body: answer };
|
|
@@ -9,6 +9,54 @@ function isToken(value) {
|
|
|
9
9
|
return /^[\x21-\x7e]+$/.test(value);
|
|
10
10
|
}
|
|
11
11
|
exports.isToken = isToken;
|
|
12
|
+
// What a failed token command said on stderr, for the user's own terminal: never its stdout,
|
|
13
|
+
// and never in an error message, which a harness hands on to an agent.
|
|
14
|
+
const MAX_STDERR_LINES = 20;
|
|
15
|
+
const MAX_STDERR_LINE = 300;
|
|
16
|
+
function writeStderr(environment, what, stderr) {
|
|
17
|
+
const lines = stderr
|
|
18
|
+
.split(/\r?\n/)
|
|
19
|
+
.map((line) => line.replace(/[^\x20-\x7e\t]/g, '?').slice(0, MAX_STDERR_LINE))
|
|
20
|
+
.filter((line) => line.trim())
|
|
21
|
+
.slice(-MAX_STDERR_LINES);
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
environment.stderr.write(`${what}: ${line}\n`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function commandSource(environment, command, what) {
|
|
27
|
+
let current;
|
|
28
|
+
const run = () => new Promise((resolve, reject) => {
|
|
29
|
+
(0, child_process_1.exec)(command, { env: environment.processEnvs, timeout: 60000 }, (error, stdout, stderr) => {
|
|
30
|
+
const token = String(stdout).trim();
|
|
31
|
+
if (error || !token) {
|
|
32
|
+
writeStderr(environment, what, String(stderr));
|
|
33
|
+
const status = error?.killed
|
|
34
|
+
? ', it did not end within a minute'
|
|
35
|
+
: typeof error?.code === 'number'
|
|
36
|
+
? `, it exited with ${error.code}`
|
|
37
|
+
: '';
|
|
38
|
+
reject(new Error(`${what} printed no token${status}`));
|
|
39
|
+
}
|
|
40
|
+
else if (!isToken(token)) {
|
|
41
|
+
// never the output itself: it can hold more secrets than the token
|
|
42
|
+
reject(new Error(`${what} printed more than a token, it has to print the token alone`));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
resolve(token);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
token: () => {
|
|
51
|
+
current = current ?? run();
|
|
52
|
+
current.catch(() => (current = undefined));
|
|
53
|
+
return current;
|
|
54
|
+
},
|
|
55
|
+
renew: async () => {
|
|
56
|
+
current = undefined;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
12
60
|
/**
|
|
13
61
|
* The token a client of a server sends: a harness, and the `remote` commands. In this order:
|
|
14
62
|
*
|
|
@@ -16,42 +64,29 @@ exports.isToken = isToken;
|
|
|
16
64
|
* identity, such as a workload identity or a service account of the identity provider. It
|
|
17
65
|
* runs again when the server no longer accepts the token.
|
|
18
66
|
* - `HAMMERKIT_TOKEN`: such a token, as the agent's platform hands it over.
|
|
67
|
+
* - the token command of the server in the host config (`remote add --token-command`): the
|
|
68
|
+
* same as `HAMMERKIT_TOKEN_COMMAND`, for one server, so each server can have the service
|
|
69
|
+
* account and audience it accepts. It only ever comes from the host config, never from a
|
|
70
|
+
* build file: whoever writes it decides what runs on this machine.
|
|
19
71
|
* - the sign-in of `hammerkit remote login <server>`.
|
|
20
72
|
*
|
|
21
73
|
* The token only goes to the URL the server is registered with in the host config.
|
|
22
74
|
*/
|
|
23
75
|
function getTokenSource(environment, name, server) {
|
|
76
|
+
// a variable holds for every server: say so when it takes the place of the one registered
|
|
77
|
+
const overrides = (variable) => {
|
|
78
|
+
if (server.tokenCommand) {
|
|
79
|
+
environment.stderr.write(`${variable} is set, so the token command of ${name} is not used\n`);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
24
82
|
const command = environment.processEnvs.HAMMERKIT_TOKEN_COMMAND;
|
|
25
83
|
if (command) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
(0, child_process_1.exec)(command, { env: environment.processEnvs, timeout: 60000 }, (error, stdout) => {
|
|
29
|
-
const token = String(stdout).trim();
|
|
30
|
-
if (error || !token) {
|
|
31
|
-
reject(new Error('HAMMERKIT_TOKEN_COMMAND printed no token'));
|
|
32
|
-
}
|
|
33
|
-
else if (!isToken(token)) {
|
|
34
|
-
// never the output itself: it can hold more secrets than the token
|
|
35
|
-
reject(new Error('HAMMERKIT_TOKEN_COMMAND printed more than a token, it has to print the token alone'));
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
resolve(token);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
return {
|
|
43
|
-
token: () => {
|
|
44
|
-
current = current ?? run();
|
|
45
|
-
current.catch(() => (current = undefined));
|
|
46
|
-
return current;
|
|
47
|
-
},
|
|
48
|
-
renew: async () => {
|
|
49
|
-
current = undefined;
|
|
50
|
-
},
|
|
51
|
-
};
|
|
84
|
+
overrides('HAMMERKIT_TOKEN_COMMAND');
|
|
85
|
+
return commandSource(environment, command, 'HAMMERKIT_TOKEN_COMMAND');
|
|
52
86
|
}
|
|
53
87
|
const fixed = environment.processEnvs.HAMMERKIT_TOKEN?.trim();
|
|
54
88
|
if (fixed) {
|
|
89
|
+
overrides('HAMMERKIT_TOKEN');
|
|
55
90
|
return {
|
|
56
91
|
token: async () => {
|
|
57
92
|
if (!isToken(fixed)) {
|
|
@@ -61,6 +96,9 @@ function getTokenSource(environment, name, server) {
|
|
|
61
96
|
},
|
|
62
97
|
};
|
|
63
98
|
}
|
|
99
|
+
if (server.tokenCommand) {
|
|
100
|
+
return commandSource(environment, server.tokenCommand, `the token command of ${name}`);
|
|
101
|
+
}
|
|
64
102
|
return { token: () => (0, credentials_1.getAccessToken)(environment, name, server) };
|
|
65
103
|
}
|
|
66
104
|
exports.getTokenSource = getTokenSource;
|