hammerkit 1.26.0 → 1.27.0-next.10
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 +62 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +24 -2
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +411 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +42 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- 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 +46 -28
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +132 -5
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +27 -11
- package/dist/kubernetes/ensure-persistent-data.js +75 -24
- package/dist/kubernetes/get-service-ip.js +16 -2
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +320 -0
- package/dist/kubernetes/kept-claims.js +203 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +583 -0
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +37 -4
- package/dist/kubernetes/restore-kubernetes-data.js +28 -7
- package/dist/kubernetes/secrets.js +7 -9
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +104 -12
- package/dist/log.js +1 -1
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +407 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +5 -2
- package/dist/planner/service-definition.js +5 -0
- package/dist/planner/utils/append-work-dependencies.js +5 -2
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +5 -0
- package/dist/planner/utils/append-work-task.js +20 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +43 -1
- package/dist/planner/work-runtime-kubernetes.js +370 -111
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/program.js +33 -5
- package/dist/remote/annotations.js +115 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +15 -3
- package/dist/remote/remote-work-commands.js +272 -71
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +7 -0
- package/dist/schema/build-file-container-service-schema.js +8 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +2 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +5 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +26 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +603 -0
- package/dist/server/kubernetes-sandbox-storage.js +997 -0
- package/dist/server/oauth.js +12 -0
- package/dist/server/plan.js +88 -2
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +48 -2
- package/dist/server/run-request.js +29 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +191 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +216 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +443 -22
- package/dist/server/server.js +219 -40
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/run-id.js +12 -0
- package/dist/utils/tar.js +25 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
|
@@ -3,32 +3,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.registerWorkCommands = void 0;
|
|
6
|
+
exports.registerWorkCommands = exports.describePlan = exports.describeApproval = void 0;
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const colors_1 = __importDefault(require("colors"));
|
|
9
|
-
const
|
|
9
|
+
const token_source_1 = require("./token-source");
|
|
10
10
|
const collect_bundle_1 = require("./collect-bundle");
|
|
11
11
|
const bundle_1 = require("./bundle");
|
|
12
|
+
const annotations_1 = require("./annotations");
|
|
12
13
|
const server_client_1 = require("./server-client");
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
function parseProposal(value) {
|
|
17
|
-
const match = PROPOSAL_NAME.exec(value);
|
|
18
|
-
return match && bundle_1.HASH_PATTERN.test(match[2]) ? { name: match[1], hash: match[2] } : undefined;
|
|
19
|
-
}
|
|
14
|
+
const bundle_text_1 = require("./bundle-text");
|
|
15
|
+
const sandbox_client_1 = require("../harness/sandbox-client");
|
|
16
|
+
const path_1 = require("path");
|
|
20
17
|
const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
|
|
18
|
+
// what the publisher said about where a version comes from, one line
|
|
19
|
+
// What a publisher said about where a version comes from: the server takes it as said,
|
|
20
|
+
// so anybody else is told it is not checked.
|
|
21
|
+
function annotationLines(annotations, label = 'from (unverified)') {
|
|
22
|
+
const entries = Object.entries(annotations ?? {});
|
|
23
|
+
return entries.length > 0
|
|
24
|
+
? [` ${label}: ${entries.map(([key, value]) => `${(0, server_client_1.printable)(key)}=${(0, server_client_1.printable)(value)}`).join(' ')}`]
|
|
25
|
+
: [];
|
|
26
|
+
}
|
|
27
|
+
function yankLines(version) {
|
|
28
|
+
return version.yanked
|
|
29
|
+
? [
|
|
30
|
+
` yanked by ${(0, server_client_1.printable)(version.yanked.by.sub)}${version.yanked.reason ? `: ${(0, server_client_1.printable)(version.yanked.reason)}` : ''}`,
|
|
31
|
+
]
|
|
32
|
+
: [];
|
|
33
|
+
}
|
|
34
|
+
// A line a task printed, shown as it is but for control characters, so it can't rewrite
|
|
35
|
+
// the terminal; tabs stay.
|
|
36
|
+
function printableLine(text) {
|
|
37
|
+
return text.split('\t').map(server_client_1.printableWhole).join('\t');
|
|
38
|
+
}
|
|
39
|
+
const collectOption = (value, previous = []) => [...previous, value];
|
|
21
40
|
// What a person who approves is shown of a run: everything the decision rests on.
|
|
22
|
-
function
|
|
23
|
-
const source = run.source.type === 'ref'
|
|
24
|
-
|
|
41
|
+
function describeApproval(run) {
|
|
42
|
+
const source = run.source.type === 'ref'
|
|
43
|
+
? `ref ${run.source.ref}`
|
|
44
|
+
: run.source.type === 'sandbox'
|
|
45
|
+
? `sandbox of ${run.source.repo} at ${run.source.commit}, ${run.source.clean ? 'unchanged' : `with changes, files ${run.source.tree}`} (files .gitignore leaves out are not shown)`
|
|
46
|
+
: `snapshot ${run.source.snapshot}`;
|
|
47
|
+
// the arguments are shown whole on a line of their own, each as a JSON string,
|
|
48
|
+
// so commas, spaces and quotes inside one stay visible
|
|
49
|
+
const { args, ...settings } = run.options;
|
|
50
|
+
const options = Object.entries(settings)
|
|
25
51
|
.map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : value}`)
|
|
26
52
|
.join(' ');
|
|
27
53
|
return [
|
|
28
54
|
`${colors_1.default.bold((0, server_client_1.printable)(run.id))} requested by ${(0, server_client_1.printable)(run.requestedBy.sub)} (${(0, server_client_1.printable)(run.requestedBy.iss)})`,
|
|
29
|
-
`
|
|
55
|
+
` build file ${(0, server_client_1.printable)(run.definition)}`,
|
|
30
56
|
` tasks: ${run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'selected by labels'}`,
|
|
31
57
|
` accounts: ${run.accounts.length > 0 ? run.accounts.map(server_client_1.printable).join(', ') : 'none'}, source ${(0, server_client_1.printable)(source)}${options ? `, ${(0, server_client_1.printable)(options)}` : ''}`,
|
|
58
|
+
...(Array.isArray(args) && args.length > 0
|
|
59
|
+
? [` arguments: ${args.map((arg) => (0, server_client_1.printableWhole)(JSON.stringify(arg))).join(' ')}`]
|
|
60
|
+
: []),
|
|
32
61
|
...(run.approvals ?? []).map((approval) => ` ${(0, server_client_1.printable)(approval.account)}: ${approval.approvedBy
|
|
33
62
|
? `approved by ${(0, server_client_1.printable)(approval.approvedBy.sub)}`
|
|
34
63
|
: 'waits for somebody who uses it to approve'}`),
|
|
@@ -36,6 +65,7 @@ function describe(run) {
|
|
|
36
65
|
` decide before ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`,
|
|
37
66
|
];
|
|
38
67
|
}
|
|
68
|
+
exports.describeApproval = describeApproval;
|
|
39
69
|
// What to look at first, one line.
|
|
40
70
|
function flagLines(run) {
|
|
41
71
|
const flagged = Object.entries(run.plan ?? {}).filter(([, entry]) => entry.flags.length > 0);
|
|
@@ -51,7 +81,11 @@ function flagLines(run) {
|
|
|
51
81
|
function describePlan(plan) {
|
|
52
82
|
return Object.entries(plan).flatMap(([name, entry]) => [
|
|
53
83
|
` ${colors_1.default.bold((0, server_client_1.printable)(name))} ${entry.kind}, ${(0, server_client_1.printable)(entry.runtime)}${entry.image ? ` ${(0, server_client_1.printable)(entry.image)}` : ''}${entry.flags.length > 0 ? ` [${entry.flags.map(server_client_1.printable).join(', ')}]` : ''}`,
|
|
54
|
-
|
|
84
|
+
// a command is what is decided on: shown whole, however long
|
|
85
|
+
...entry.cmds.map((cmd) => ` $ ${(0, server_client_1.printableWhole)(cmd)}`),
|
|
86
|
+
...(entry.network !== undefined && entry.network !== 'open'
|
|
87
|
+
? [` network: ${(0, server_client_1.printable)(typeof entry.network === 'string' ? entry.network : JSON.stringify(entry.network))}`]
|
|
88
|
+
: []),
|
|
55
89
|
...(entry.deps.length > 0 ? [` after: ${entry.deps.map(server_client_1.printable).join(', ')}`] : []),
|
|
56
90
|
...(entry.needs.length > 0 ? [` needs: ${entry.needs.map(server_client_1.printable).join(', ')}`] : []),
|
|
57
91
|
...entry.secrets.map((secret) => ` secret ${(0, server_client_1.printable)(secret.from)}${secret.account ? ` as ${(0, server_client_1.printable)(secret.account)}` : ''} -> ${(0, server_client_1.printable)(secret.target)}`),
|
|
@@ -60,29 +94,159 @@ function describePlan(plan) {
|
|
|
60
94
|
...(entry.generates.length > 0 ? [` generates: ${entry.generates.map(server_client_1.printable).join(', ')}`] : []),
|
|
61
95
|
]);
|
|
62
96
|
}
|
|
97
|
+
exports.describePlan = describePlan;
|
|
63
98
|
function registerWorkCommands(remote, context) {
|
|
64
|
-
const { environment, buildFile, getServer, guarded } = context;
|
|
99
|
+
const { environment, buildFile, getServer, guarded, passthrough, fail } = context;
|
|
65
100
|
const connect = async (name) => {
|
|
66
101
|
const chosen = await getServer(name);
|
|
67
|
-
|
|
102
|
+
// a CI job or an agent brings its own token, a person signs in
|
|
103
|
+
const token = await (0, token_source_1.getTokenSource)(environment, chosen.name, chosen.server).token();
|
|
68
104
|
return { ...chosen, token };
|
|
69
105
|
};
|
|
70
106
|
remote
|
|
71
|
-
.command('publish
|
|
72
|
-
.description('publish the build file and the files it includes to a server,
|
|
73
|
-
.
|
|
107
|
+
.command('publish [server]')
|
|
108
|
+
.description('publish the build file and the files it includes to a server, runs refer to it by its hash')
|
|
109
|
+
.addOption(new commander_1.Option('--annotation <key=value>', 'say more about where it comes from, or leave one out with key=; path, repo, commit, machine and user are filled in').argParser(collectOption))
|
|
110
|
+
.action(guarded('Remote publish was not successful', async (name, options) => {
|
|
74
111
|
const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
|
|
112
|
+
const annotations = (0, annotations_1.applyAnnotations)(await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(bundle.files)), options.annotation ?? []);
|
|
75
113
|
const { name: serverName, server, token } = await connect(name);
|
|
76
|
-
const answer = await (0, server_client_1.callServer)(server, token, '
|
|
114
|
+
const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions', {
|
|
115
|
+
...bundle,
|
|
116
|
+
annotations,
|
|
117
|
+
});
|
|
77
118
|
const version = answer.body;
|
|
78
119
|
const files = Object.keys(bundle.files).length;
|
|
79
120
|
const proposed = answer.status === 202;
|
|
80
121
|
const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
|
|
81
|
-
environment.stdout.write(`${verb}
|
|
122
|
+
environment.stdout.write(`${verb} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
|
|
82
123
|
const names = [...(version.tasks ?? []), ...(version.services ?? [])];
|
|
83
124
|
environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
125
|
+
for (const line of annotationLines(version.annotations, 'from')) {
|
|
126
|
+
environment.stdout.write(`${line.trimStart()}\n`);
|
|
127
|
+
}
|
|
84
128
|
if (proposed) {
|
|
85
|
-
environment.stdout.write(`it can not run before somebody who may publish
|
|
129
|
+
environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
remote
|
|
133
|
+
.command('run <tasks...>')
|
|
134
|
+
.description('run tasks of a published build file on a server from a git ref, print their output and exit as they did')
|
|
135
|
+
.addOption(new commander_1.Option('--server <name>', 'the server to run on; the default of remote use when left out'))
|
|
136
|
+
.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'))
|
|
137
|
+
.addOption(new commander_1.Option('--repo <host/path>', 'the repository, such as github.com/corp/payments; origin of the checkout here when left out'))
|
|
138
|
+
.addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to run; the branch checked out here when left out'))
|
|
139
|
+
.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'))
|
|
140
|
+
.addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
|
|
141
|
+
.addOption(new commander_1.Option('--cache <method>', 'as --cache of run').choices(['checksum', 'modify-date', 'none']))
|
|
142
|
+
.addOption(new commander_1.Option('--timeout <duration>', 'stop the run after this long, such as 30m'))
|
|
143
|
+
.action(guarded('Remote run was not successful', async (tasks, options) => {
|
|
144
|
+
const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
|
|
145
|
+
const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
|
|
146
|
+
if (!bundle_1.HASH_PATTERN.test(definition)) {
|
|
147
|
+
throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
|
|
148
|
+
}
|
|
149
|
+
const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
|
|
150
|
+
const repo = options.repo ?? checkout.repo;
|
|
151
|
+
const ref = options.ref ?? checkout.branch;
|
|
152
|
+
// the commit checked out here is the code meant, when the ref is the branch it is on
|
|
153
|
+
const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
|
|
154
|
+
if (!repo) {
|
|
155
|
+
throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
|
|
156
|
+
}
|
|
157
|
+
if (!ref) {
|
|
158
|
+
throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
|
|
159
|
+
}
|
|
160
|
+
const { name: serverName, server } = await getServer(options.server);
|
|
161
|
+
const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
|
|
162
|
+
const runOptions = {};
|
|
163
|
+
for (const key of ['env', 'cache', 'timeout']) {
|
|
164
|
+
if (options[key] !== undefined) {
|
|
165
|
+
runOptions[key] = options[key];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (passthrough.length > 0) {
|
|
169
|
+
runOptions.args = passthrough;
|
|
170
|
+
}
|
|
171
|
+
let submitted;
|
|
172
|
+
try {
|
|
173
|
+
submitted = await (0, sandbox_client_1.submitRun)(connection, {
|
|
174
|
+
definition,
|
|
175
|
+
tasks,
|
|
176
|
+
source: { type: 'ref', repo, ref, ...(commit ? { commit } : {}) },
|
|
177
|
+
options: runOptions,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (e.status === 400 && !named && /no published build file/.test(String(e))) {
|
|
182
|
+
throw new Error(`the build file here is not published on ${serverName} (${definition}), publish it first`);
|
|
183
|
+
}
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
const at = submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : '';
|
|
187
|
+
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`);
|
|
188
|
+
const prefixed = tasks.length > 1;
|
|
189
|
+
const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
|
|
190
|
+
signal: environment.abortCtrl.signal,
|
|
191
|
+
onLine: (line) => {
|
|
192
|
+
environment.stdout.write(`${prefixed || line.task !== tasks[0] ? `[${(0, server_client_1.printable)(line.task)}] ` : ''}${printableLine(line.text)}\n`);
|
|
193
|
+
},
|
|
194
|
+
onWaiting: (run) => {
|
|
195
|
+
for (const approval of run.approvals ?? []) {
|
|
196
|
+
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`);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const run = followed.run;
|
|
201
|
+
if (run.state === 'succeeded') {
|
|
202
|
+
environment.stdout.write(`${(0, server_client_1.printable)(run.id)} succeeded\n`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// the exit code of the first task that failed, as `hammerkit run` would end
|
|
206
|
+
const failed = tasks
|
|
207
|
+
.map((task) => run.results?.[task])
|
|
208
|
+
.concat(Object.values(run.results ?? {}))
|
|
209
|
+
.find((result) => result?.status === 'failed');
|
|
210
|
+
const exitCode = failed?.exitCode && failed.exitCode > 0 ? failed.exitCode : 1;
|
|
211
|
+
fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, exitCode);
|
|
212
|
+
}));
|
|
213
|
+
remote
|
|
214
|
+
.command('versions [server]')
|
|
215
|
+
.description('list the published versions of build files on a server, newest first, with where they come from')
|
|
216
|
+
.addOption(new commander_1.Option('--filter <key=value>', 'only the versions with this annotation, such as repo=github.com/corp/payments').argParser(collectOption))
|
|
217
|
+
.action(guarded('Remote versions was not successful', async (name, options) => {
|
|
218
|
+
const query = new URLSearchParams();
|
|
219
|
+
for (const filter of (options.filter ?? [])) {
|
|
220
|
+
const index = filter.indexOf('=');
|
|
221
|
+
if (index <= 0) {
|
|
222
|
+
throw new Error(`${(0, server_client_1.printable)(filter)} is not a filter, write it as key=value`);
|
|
223
|
+
}
|
|
224
|
+
if (index === filter.length - 1) {
|
|
225
|
+
throw new Error(`${(0, server_client_1.printable)(filter)} matches nothing, an annotation always has a value`);
|
|
226
|
+
}
|
|
227
|
+
query.append(filter.slice(0, index), filter.slice(index + 1));
|
|
228
|
+
}
|
|
229
|
+
const { name: serverName, server, token } = await connect(name);
|
|
230
|
+
const path = `/v1/definitions${query.toString() !== '' ? `?${query}` : ''}`;
|
|
231
|
+
const { body } = await (0, server_client_1.callServer)(server, token, 'GET', path);
|
|
232
|
+
if (body.definitions.length === 0) {
|
|
233
|
+
environment.stdout.write(`${serverName}: no published versions${query.toString() !== '' ? ' match' : ''}\n`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
environment.stdout.write(`${serverName}: ${body.definitions.length} published\n`);
|
|
237
|
+
for (const version of body.definitions) {
|
|
238
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(version.hash))} published by ${(0, server_client_1.printable)(version.publishedBy.sub)} at ${(0, server_client_1.printable)(version.publishedAt)}${version.approvedBy ? `, approved by ${(0, server_client_1.printable)(version.approvedBy.sub)}` : ''}\n`);
|
|
239
|
+
for (const line of annotationLines(version.annotations)) {
|
|
240
|
+
environment.stdout.write(`${line}\n`);
|
|
241
|
+
}
|
|
242
|
+
const names = [...version.tasks, ...version.services];
|
|
243
|
+
environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
244
|
+
if (version.accounts.length > 0) {
|
|
245
|
+
environment.stdout.write(` accounts: ${version.accounts.map(server_client_1.printable).join(', ')}\n`);
|
|
246
|
+
}
|
|
247
|
+
for (const line of yankLines(version)) {
|
|
248
|
+
environment.stdout.write(`${line}\n`);
|
|
249
|
+
}
|
|
86
250
|
}
|
|
87
251
|
}));
|
|
88
252
|
remote
|
|
@@ -97,21 +261,84 @@ function registerWorkCommands(remote, context) {
|
|
|
97
261
|
}
|
|
98
262
|
environment.stdout.write(`${serverName}: ${body.approvals.length} waiting for your approval\n`);
|
|
99
263
|
for (const run of body.approvals) {
|
|
100
|
-
for (const line of
|
|
264
|
+
for (const line of describeApproval(run)) {
|
|
101
265
|
environment.stdout.write(`${line}\n`);
|
|
102
266
|
}
|
|
103
267
|
}
|
|
104
268
|
}));
|
|
269
|
+
// A version that is published: who published it, what its tasks do and every file.
|
|
270
|
+
const showPublished = async (hash, serverName, server, token) => {
|
|
271
|
+
const path = `/v1/definitions/${hash}`;
|
|
272
|
+
let version;
|
|
273
|
+
try {
|
|
274
|
+
version = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
if (e instanceof server_client_1.ServerError && e.status === 404) {
|
|
278
|
+
throw new Error(`${serverName} has no proposal or published version ${(0, server_client_1.printable)(hash)} you may see`);
|
|
279
|
+
}
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
const files = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
|
|
283
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(version.hash))} on ${serverName}, published by ${(0, server_client_1.printable)(version.publishedBy.sub)} (${(0, server_client_1.printable)(version.publishedBy.iss)}) at ${(0, server_client_1.printable)(version.publishedAt)}${version.approvedBy ? `, approved by ${(0, server_client_1.printable)(version.approvedBy.sub)}` : ''}\n`);
|
|
284
|
+
for (const line of [
|
|
285
|
+
...yankLines(version),
|
|
286
|
+
...annotationLines(version.annotations),
|
|
287
|
+
...flagLines(version),
|
|
288
|
+
...(version.plan ? describePlan(version.plan) : [' the server did not record what its tasks do']),
|
|
289
|
+
...(0, bundle_text_1.showBundle)(files),
|
|
290
|
+
]) {
|
|
291
|
+
environment.stdout.write(`${line}\n`);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
105
294
|
remote
|
|
106
|
-
.command('show <
|
|
107
|
-
.description('show a run
|
|
108
|
-
.action(guarded('Remote show was not successful', async (
|
|
109
|
-
if (!RUN_ID.test(
|
|
110
|
-
throw new Error(`${(0, server_client_1.printable)(
|
|
111
|
-
}
|
|
112
|
-
const { server, token } = await connect(name);
|
|
113
|
-
|
|
114
|
-
|
|
295
|
+
.command('show <target> [server]')
|
|
296
|
+
.description('show a run (run_...) or a proposed or published version (sha256:...) in full: the commands, secrets and accounts of its tasks, and the files of a version')
|
|
297
|
+
.action(guarded('Remote show was not successful', async (target, name) => {
|
|
298
|
+
if (!RUN_ID.test(target) && !bundle_1.HASH_PATTERN.test(target)) {
|
|
299
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a version, list them with: hammerkit remote approvals, remote proposals or remote versions`);
|
|
300
|
+
}
|
|
301
|
+
const { name: serverName, server, token } = await connect(name);
|
|
302
|
+
if (bundle_1.HASH_PATTERN.test(target)) {
|
|
303
|
+
const path = `/v1/proposals/${target}`;
|
|
304
|
+
let proposal;
|
|
305
|
+
try {
|
|
306
|
+
proposal = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
if (e instanceof server_client_1.ServerError && e.status === 404) {
|
|
310
|
+
await showPublished(target, serverName, server, token);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
throw e;
|
|
314
|
+
}
|
|
315
|
+
if (proposal.state === 'approved') {
|
|
316
|
+
await showPublished(target, serverName, server, token);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (proposal.state !== 'waiting') {
|
|
320
|
+
// a decided proposal keeps no files
|
|
321
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} on ${serverName}, proposed by ${(0, server_client_1.printable)(proposal.proposedBy.sub)}, ${(0, server_client_1.printable)(proposal.state)}${proposal.decidedBy ? ` by ${(0, server_client_1.printable)(proposal.decidedBy.sub)}` : ''}${proposal.reason ? `: ${(0, server_client_1.printable)(proposal.reason)}` : ''}\n`);
|
|
322
|
+
for (const line of annotationLines(proposal.annotations)) {
|
|
323
|
+
environment.stdout.write(`${line}\n`);
|
|
324
|
+
}
|
|
325
|
+
environment.stdout.write(' its files are not kept once it is decided\n');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const files = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
|
|
329
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} on ${serverName}, proposed by ${(0, server_client_1.printable)(proposal.proposedBy.sub)} (${(0, server_client_1.printable)(proposal.proposedBy.iss)}), ${(0, server_client_1.printable)(proposal.state)}\n`);
|
|
330
|
+
for (const line of [
|
|
331
|
+
...annotationLines(proposal.annotations),
|
|
332
|
+
...flagLines(proposal),
|
|
333
|
+
...describePlan(proposal.plan ?? {}),
|
|
334
|
+
...(0, bundle_text_1.showBundle)(files),
|
|
335
|
+
]) {
|
|
336
|
+
environment.stdout.write(`${line}\n`);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const view = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/approvals/${target}`)).body;
|
|
341
|
+
for (const line of describeApproval(view)) {
|
|
115
342
|
environment.stdout.write(`${line}\n`);
|
|
116
343
|
}
|
|
117
344
|
environment.stdout.write(view.plan ? `${describePlan(view.plan).join('\n')}\n` : 'the server did not record what its tasks do\n');
|
|
@@ -130,7 +357,10 @@ function registerWorkCommands(remote, context) {
|
|
|
130
357
|
for (const proposal of body.proposals) {
|
|
131
358
|
const who = proposal.mine ? 'you' : (0, server_client_1.printable)(proposal.proposedBy.sub);
|
|
132
359
|
const state = proposal.canDecide ? colors_1.default.bold('waits for your approval') : (0, server_client_1.printable)(proposal.state);
|
|
133
|
-
environment.stdout.write(`${colors_1.default.bold(
|
|
360
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} proposed by ${who}, ${state}\n`);
|
|
361
|
+
for (const line of annotationLines(proposal.annotations)) {
|
|
362
|
+
environment.stdout.write(`${line}\n`);
|
|
363
|
+
}
|
|
134
364
|
const names = [...proposal.tasks, ...proposal.services];
|
|
135
365
|
environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
136
366
|
if (proposal.accounts.length > 0) {
|
|
@@ -141,9 +371,6 @@ function registerWorkCommands(remote, context) {
|
|
|
141
371
|
for (const line of flagLines(proposal)) {
|
|
142
372
|
environment.stdout.write(`${line}\n`);
|
|
143
373
|
}
|
|
144
|
-
if (proposal.stale) {
|
|
145
|
-
environment.stdout.write(` stale: ${(0, server_client_1.printable)(proposal.name)} has a newer version, reject it and propose again on top of it\n`);
|
|
146
|
-
}
|
|
147
374
|
if (proposal.state === 'waiting') {
|
|
148
375
|
environment.stdout.write(` decide before ${(0, server_client_1.printable)(proposal.expiresAt)}\n`);
|
|
149
376
|
}
|
|
@@ -152,43 +379,19 @@ function registerWorkCommands(remote, context) {
|
|
|
152
379
|
}
|
|
153
380
|
}
|
|
154
381
|
}));
|
|
155
|
-
remote
|
|
156
|
-
.command('diff <proposal> [server]')
|
|
157
|
-
.description('show what a proposed version (name@hash) changes against the version it follows')
|
|
158
|
-
.action(guarded('Remote diff was not successful', async (target, name) => {
|
|
159
|
-
const proposal = parseProposal(target);
|
|
160
|
-
if (!proposal) {
|
|
161
|
-
throw new Error(`${(0, server_client_1.printable)(target)} is not a proposal, list them with: hammerkit remote proposals`);
|
|
162
|
-
}
|
|
163
|
-
const { name: serverName, server, token } = await connect(name);
|
|
164
|
-
const path = `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}`;
|
|
165
|
-
const view = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
|
|
166
|
-
const proposed = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
|
|
167
|
-
const base = view.base
|
|
168
|
-
? (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/definitions/${encodeURIComponent(proposal.name)}/${view.base}/files`)).body
|
|
169
|
-
: undefined;
|
|
170
|
-
environment.stdout.write(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)} on ${serverName}, proposed by ${(0, server_client_1.printable)(view.proposedBy.sub)} (${(0, server_client_1.printable)(view.proposedBy.iss)})\n`);
|
|
171
|
-
environment.stdout.write(base ? `compared with ${(0, server_client_1.printable)(view.base)}\n` : 'no version is published yet, everything is new\n');
|
|
172
|
-
if (view.stale) {
|
|
173
|
-
environment.stdout.write(`warning: ${(0, server_client_1.printable)(proposal.name)} has a newer version (${(0, server_client_1.printable)(view.latest)}), this can only be rejected\n`);
|
|
174
|
-
}
|
|
175
|
-
const lines = (0, text_diff_1.diffBundles)(base, proposed);
|
|
176
|
-
environment.stdout.write(lines.length > 0 ? `${lines.join('\n')}\n` : 'the files are the same\n');
|
|
177
|
-
}));
|
|
178
382
|
for (const action of ['yank', 'unyank']) {
|
|
179
383
|
remote
|
|
180
384
|
.command(`${action} <version> [server]`)
|
|
181
385
|
.description(action === 'yank'
|
|
182
|
-
? 'pull a published version (
|
|
183
|
-
: 'let a yanked version (
|
|
386
|
+
? 'pull a published version (sha256:...) back: no run starts from it'
|
|
387
|
+
: 'let a yanked version (sha256:...) go on')
|
|
184
388
|
.addOption(new commander_1.Option('--reason <text>', 'why, shown to whoever tries to run it').argParser((value) => value))
|
|
185
389
|
.action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it like payments@sha256:...`);
|
|
390
|
+
if (!bundle_1.HASH_PATTERN.test(target)) {
|
|
391
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it by its hash: sha256:...`);
|
|
189
392
|
}
|
|
190
393
|
const { name: serverName, server, token } = await connect(name);
|
|
191
|
-
await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${
|
|
394
|
+
await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${target}/${action}`, options.reason ? { reason: options.reason } : undefined);
|
|
192
395
|
environment.stdout.write(`${action === 'yank' ? 'yanked' : 'let go on'} ${(0, server_client_1.printable)(target)} on ${serverName}\n`);
|
|
193
396
|
}));
|
|
194
397
|
}
|
|
@@ -196,18 +399,16 @@ function registerWorkCommands(remote, context) {
|
|
|
196
399
|
remote
|
|
197
400
|
.command(`${action} <target> [server]`)
|
|
198
401
|
.description(action === 'approve'
|
|
199
|
-
? 'let a run (run_...) or a proposed version (
|
|
200
|
-
: 'end a run (run_...) or drop a proposed version (
|
|
402
|
+
? 'let a run (run_...) or a proposed version (sha256:...) that waits for your approval go ahead'
|
|
403
|
+
: 'end a run (run_...) or drop a proposed version (sha256:...) that waits for your approval')
|
|
201
404
|
.addOption(new commander_1.Option('--reason <text>', 'why, shown to who asked').argParser((value) => value))
|
|
202
405
|
.action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
|
|
203
|
-
const proposal =
|
|
406
|
+
const proposal = bundle_1.HASH_PATTERN.test(target);
|
|
204
407
|
if (!proposal && !RUN_ID.test(target)) {
|
|
205
408
|
throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a proposed version, list them with: hammerkit remote approvals or remote proposals`);
|
|
206
409
|
}
|
|
207
410
|
const { name: serverName, server, token } = await connect(name);
|
|
208
|
-
const path = proposal
|
|
209
|
-
? `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}/${action}`
|
|
210
|
-
: `/v1/approvals/${target}/${action}`;
|
|
411
|
+
const path = proposal ? `/v1/proposals/${target}/${action}` : `/v1/approvals/${target}/${action}`;
|
|
211
412
|
const { body } = await (0, server_client_1.callServer)(server, token, 'POST', path, options.reason ? { reason: options.reason } : undefined);
|
|
212
413
|
const rejected = body.state === 'rejected';
|
|
213
414
|
environment.stdout.write(proposal
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.callServer = exports.getMe = exports.getDiscovery = exports.printable = exports.ServerError = void 0;
|
|
3
|
+
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = 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;
|
|
7
7
|
class ServerError extends Error {
|
|
8
|
+
// what the server answered, when it did
|
|
9
|
+
constructor(message, status) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
8
13
|
}
|
|
9
14
|
exports.ServerError = ServerError;
|
|
10
15
|
// Text from the server or the identity provider is shown to the user: no
|
|
11
|
-
// control characters, so it can't rewrite the terminal
|
|
16
|
+
// control characters, so it can't rewrite the terminal, and format characters
|
|
17
|
+
// (bidi overrides, zero-width) written out, so text reads in the order it runs.
|
|
18
|
+
// Long text is cut, and says how much was.
|
|
12
19
|
function printable(value) {
|
|
20
|
+
const text = printableWhole(value);
|
|
21
|
+
return text.length > 300 ? `${text.slice(0, 300)}… (+${text.length - 300} chars)` : text;
|
|
22
|
+
}
|
|
23
|
+
exports.printable = printable;
|
|
24
|
+
// The same, never cut: for what somebody decides on, such as a command.
|
|
25
|
+
function printableWhole(value) {
|
|
13
26
|
return String(value)
|
|
14
27
|
.replace(/\p{Cc}/gu, '?')
|
|
15
|
-
.
|
|
28
|
+
.replace(/\p{Cf}/gu, (char) => `\\u{${char.codePointAt(0)?.toString(16)}}`);
|
|
16
29
|
}
|
|
17
|
-
exports.
|
|
30
|
+
exports.printableWhole = printableWhole;
|
|
18
31
|
async function getJson(url, token) {
|
|
19
32
|
let response;
|
|
20
33
|
try {
|
|
@@ -80,7 +93,7 @@ async function callServer(server, token, method, path, body) {
|
|
|
80
93
|
}
|
|
81
94
|
if (!response.ok) {
|
|
82
95
|
const said = answer && typeof answer.error === 'string' ? `: ${printable(answer.error)}` : '';
|
|
83
|
-
throw new ServerError(`${server.url} answered ${response.status}${said}
|
|
96
|
+
throw new ServerError(`${server.url} answered ${response.status}${said}`, response.status);
|
|
84
97
|
}
|
|
85
98
|
return { status: response.status, body: answer };
|
|
86
99
|
}
|
|
@@ -53,6 +53,16 @@ function hashSource(manifest) {
|
|
|
53
53
|
}
|
|
54
54
|
exports.hashSource = hashSource;
|
|
55
55
|
const DENIED_DIRECTORIES = ['.git', '.ssh', '.aws', '.gnupg', '.kube'];
|
|
56
|
+
// the short names Windows gives these directories, `GIT~1` for `.git`
|
|
57
|
+
const DENIED_SHORT_NAMES = /^(git|ssh|aws|gnupg|kube)~\d+$/;
|
|
58
|
+
// A name as a case-insensitive disk opens it: Windows and macOS open `.SSH` as `.ssh`, and
|
|
59
|
+
// Windows drops trailing dots and spaces (`.git.`) and reads `name:stream` as `name`.
|
|
60
|
+
function canonicalName(name) {
|
|
61
|
+
return name
|
|
62
|
+
.replace(/:.*$/, '')
|
|
63
|
+
.replace(/[. ]+$/, '')
|
|
64
|
+
.toLowerCase();
|
|
65
|
+
}
|
|
56
66
|
const DENIED_NAMES = [
|
|
57
67
|
/^\.env$/i,
|
|
58
68
|
/^\.env\.(?!example$|sample$|template$|dist$)/i,
|
|
@@ -65,12 +75,15 @@ const DENIED_NAMES = [
|
|
|
65
75
|
// must not be what decides that a key leaves the machine.
|
|
66
76
|
function deniedSourcePath(path) {
|
|
67
77
|
const parts = path.split('/');
|
|
68
|
-
const directory = parts.slice(0, -1).find((part) =>
|
|
78
|
+
const directory = parts.slice(0, -1).find((part) => {
|
|
79
|
+
const name = canonicalName(part);
|
|
80
|
+
return DENIED_DIRECTORIES.includes(name) || DENIED_SHORT_NAMES.test(name);
|
|
81
|
+
});
|
|
69
82
|
if (directory !== undefined) {
|
|
70
83
|
return `${directory}/ holds credentials or history`;
|
|
71
84
|
}
|
|
72
85
|
const name = parts[parts.length - 1];
|
|
73
|
-
if (DENIED_NAMES.some((pattern) => pattern.test(name))) {
|
|
86
|
+
if (DENIED_NAMES.some((pattern) => pattern.test(name) || pattern.test(canonicalName(name)))) {
|
|
74
87
|
return `${name} looks like a credential file`;
|
|
75
88
|
}
|
|
76
89
|
return null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTokenSource = exports.isToken = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const credentials_1 = require("./credentials");
|
|
6
|
+
// A bearer token is one word of visible ASCII; anything else is output of a token command
|
|
7
|
+
// that holds more than the token, which must never reach an error message.
|
|
8
|
+
function isToken(value) {
|
|
9
|
+
return /^[\x21-\x7e]+$/.test(value);
|
|
10
|
+
}
|
|
11
|
+
exports.isToken = isToken;
|
|
12
|
+
/**
|
|
13
|
+
* The token a client of a server sends: a harness, and the `remote` commands. In this order:
|
|
14
|
+
*
|
|
15
|
+
* - `HAMMERKIT_TOKEN_COMMAND`: a command that prints a short-lived token of the agent's own
|
|
16
|
+
* identity, such as a workload identity or a service account of the identity provider. It
|
|
17
|
+
* runs again when the server no longer accepts the token.
|
|
18
|
+
* - `HAMMERKIT_TOKEN`: such a token, as the agent's platform hands it over.
|
|
19
|
+
* - the sign-in of `hammerkit remote login <server>`.
|
|
20
|
+
*
|
|
21
|
+
* The token only goes to the URL the server is registered with in the host config.
|
|
22
|
+
*/
|
|
23
|
+
function getTokenSource(environment, name, server) {
|
|
24
|
+
const command = environment.processEnvs.HAMMERKIT_TOKEN_COMMAND;
|
|
25
|
+
if (command) {
|
|
26
|
+
let current;
|
|
27
|
+
const run = () => new Promise((resolve, reject) => {
|
|
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
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const fixed = environment.processEnvs.HAMMERKIT_TOKEN?.trim();
|
|
54
|
+
if (fixed) {
|
|
55
|
+
return {
|
|
56
|
+
token: async () => {
|
|
57
|
+
if (!isToken(fixed)) {
|
|
58
|
+
throw new Error('HAMMERKIT_TOKEN holds more than a token, it has to be the token alone');
|
|
59
|
+
}
|
|
60
|
+
return fixed;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { token: () => (0, credentials_1.getAccessToken)(environment, name, server) };
|
|
65
|
+
}
|
|
66
|
+
exports.getTokenSource = getTokenSource;
|
|
67
|
+
//# sourceMappingURL=token-source.js.map
|
|
@@ -12,6 +12,7 @@ const duration_schema_1 = require("./duration-schema");
|
|
|
12
12
|
const build_file_secret_schema_1 = require("./build-file-secret-schema");
|
|
13
13
|
const resources_schema_1 = require("./resources-schema");
|
|
14
14
|
const runner_schema_1 = require("./runner-schema");
|
|
15
|
+
const network_schema_1 = require("./network-schema");
|
|
15
16
|
// what `extend` adds to (the rest, e.g. `image` or `shell`, the task just overrides)
|
|
16
17
|
exports.RESET_PROPERTIES = ['deps', 'needs', 'src', 'generates', 'cmds', 'mounts', 'envs', 'labels'];
|
|
17
18
|
exports.buildFileBaseTaskSchema = (0, zod_1.object)({
|
|
@@ -39,6 +40,12 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
|
|
|
39
40
|
account: (0, zod_1.string)().optional(),
|
|
40
41
|
// CPU and memory limits of the container; a local task is not limited
|
|
41
42
|
resources: resources_schema_1.resourcesSchema.optional(),
|
|
43
|
+
// what the container reaches besides its needs; a local task can be limited only where
|
|
44
|
+
// it runs in a container, on a server's cluster
|
|
45
|
+
network: network_schema_1.networkSchema.optional(),
|
|
46
|
+
// whether its last command takes the arguments after `--`: false refuses them, true
|
|
47
|
+
// says it is meant to, which a server with `runs.args: declared` requires
|
|
48
|
+
args: (0, zod_1.boolean)().optional(),
|
|
42
49
|
// Name of the server (see `hammerkit remote`) the task has to run on; defaults
|
|
43
50
|
// to the build file's `runner`. Never a local fallback when it is unknown.
|
|
44
51
|
runner: runner_schema_1.runnerSchema.optional(),
|