hammerkit 1.24.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/optimizer/get-cache-directory.js +5 -0
- package/dist/planner/work-secret.js +6 -2
- package/dist/program.js +2 -0
- package/dist/remote/remote-commands.js +16 -12
- package/dist/remote/remote-work-commands.js +75 -3
- package/dist/schema/reference-parser.js +8 -2
- package/dist/secrets/provider-values.js +4 -0
- package/dist/server/accounts-api.js +42 -0
- package/dist/server/approvals-api.js +11 -6
- package/dist/server/definitions-api.js +65 -21
- package/dist/server/definitions.js +54 -142
- package/dist/server/executor.js +180 -181
- package/dist/server/git-checkout.js +55 -3
- package/dist/server/plan.js +296 -0
- package/dist/server/policy.js +98 -50
- package/dist/server/proposals-api.js +30 -8
- package/dist/server/proposals.js +50 -3
- package/dist/server/repo-access.js +127 -0
- package/dist/server/repo.js +94 -0
- package/dist/server/run-environment.js +20 -0
- package/dist/server/run-plan.js +44 -0
- package/dist/server/run-request.js +23 -5
- package/dist/server/run-store.js +61 -0
- package/dist/server/run-worker-command.js +17 -0
- package/dist/server/run-worker-main.js +7 -0
- package/dist/server/run-worker.js +179 -0
- package/dist/server/runs.js +189 -36
- package/dist/server/server-command.js +18 -0
- package/dist/server/server-config.js +203 -42
- package/dist/server/server.js +148 -52
- package/dist/server/sources-api.js +1 -1
- package/package.json +1 -1
|
@@ -14,7 +14,12 @@ function getCacheStatsFile(cacheId) {
|
|
|
14
14
|
return (0, path_1.join)(getCacheDirectory(cacheId), 'stats.json');
|
|
15
15
|
}
|
|
16
16
|
exports.getCacheStatsFile = getCacheStatsFile;
|
|
17
|
+
// `HAMMERKIT_DIR` puts hammerkit's directory (the cache) elsewhere than below the home directory;
|
|
18
|
+
// a server uses it to keep the caches of runs apart.
|
|
17
19
|
function getHammerkitDirectory() {
|
|
20
|
+
if (process.env.HAMMERKIT_DIR) {
|
|
21
|
+
return process.env.HAMMERKIT_DIR;
|
|
22
|
+
}
|
|
18
23
|
if (process.platform === 'win32') {
|
|
19
24
|
return (0, path_1.join)(process.env.APPDATA, 'hammerkit');
|
|
20
25
|
}
|
|
@@ -66,9 +66,13 @@ function parseSecretSource(secret, ref, cwd, envs, catalog, taskAccount, label)
|
|
|
66
66
|
if (providerRef.length === 0 || providerRef.startsWith('-')) {
|
|
67
67
|
throw new Error(`${label}: a reference for ${kind} must not be empty or start with -`);
|
|
68
68
|
}
|
|
69
|
-
const accountName =
|
|
69
|
+
const accountName = catalog.explicitAccounts
|
|
70
|
+
? secret.account
|
|
71
|
+
: secret.account ?? taskAccount ?? Object.values(catalog.accounts).find((a) => a.default)?.name;
|
|
70
72
|
if (!accountName) {
|
|
71
|
-
throw new Error(
|
|
73
|
+
throw new Error(catalog.explicitAccounts
|
|
74
|
+
? `${label}: ${kind}:${providerRef} needs account: on the secret, a server uses neither a task's account nor a default`
|
|
75
|
+
: `${label}: ${kind}:${providerRef} needs an account, and none is named or marked default: true`);
|
|
72
76
|
}
|
|
73
77
|
const account = catalog.accounts[accountName];
|
|
74
78
|
if (!account) {
|
package/dist/program.js
CHANGED
|
@@ -27,6 +27,7 @@ const units_1 = require("./utils/units");
|
|
|
27
27
|
const cache_inventory_1 = require("./cache/cache-inventory");
|
|
28
28
|
const remote_commands_1 = require("./remote/remote-commands");
|
|
29
29
|
const server_command_1 = require("./server/server-command");
|
|
30
|
+
const run_worker_command_1 = require("./server/run-worker-command");
|
|
30
31
|
async function createCli(fileName, environment, workScope) {
|
|
31
32
|
const { ctx, scope } = await (0, schema_parser_1.createParseContext)(fileName, environment);
|
|
32
33
|
const referencedScope = await (0, reference_parser_1.parseReferences)(ctx, scope, environment);
|
|
@@ -105,6 +106,7 @@ function parseWorkLabelScope(options) {
|
|
|
105
106
|
async function getProgram(environment, argv) {
|
|
106
107
|
const program = new commander_1.Command();
|
|
107
108
|
(0, server_command_1.registerServerCommand)(program, environment);
|
|
109
|
+
(0, run_worker_command_1.registerRunWorkerCommand)(program, environment);
|
|
108
110
|
const parsed = (0, parse_set_arguments_1.parseSetArguments)(argv);
|
|
109
111
|
const args = parsed.args;
|
|
110
112
|
environment.processEnvs = { ...environment.processEnvs, ...parsed.values };
|
|
@@ -108,7 +108,7 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
108
108
|
const me = await (0, server_client_1.getMe)(server, result.accessToken);
|
|
109
109
|
environment.stdout.write(`signed in to ${name} as ${me.sub}\n`);
|
|
110
110
|
environment.stdout.write(me.accounts.length > 0
|
|
111
|
-
? `service accounts: ${me.accounts.join(', ')}\n`
|
|
111
|
+
? `service accounts: ${me.accounts.map((a) => `${(0, server_client_1.printable)(a.name)} (${a.level})`).join(', ')}\n`
|
|
112
112
|
: 'you have no service accounts on this server\n');
|
|
113
113
|
}
|
|
114
114
|
catch (e) {
|
|
@@ -131,21 +131,25 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
131
131
|
const chosen = await getServer(name);
|
|
132
132
|
const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
|
|
133
133
|
const me = await (0, server_client_1.getMe)(chosen.server, token);
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
const { body } = await (0, server_client_1.callServer)(chosen.server, token, 'GET', '/v1/accounts');
|
|
135
|
+
environment.stdout.write(`${chosen.name}: ${(0, server_client_1.printable)(me.sub)} (${(0, server_client_1.printable)(me.iss)})\n`);
|
|
136
|
+
if (body.accounts.length === 0) {
|
|
136
137
|
environment.stdout.write('no service accounts\n');
|
|
137
138
|
}
|
|
138
|
-
for (const account of
|
|
139
|
-
|
|
139
|
+
for (const account of body.accounts) {
|
|
140
|
+
const level = account.level === 'use' ? 'use' : 'request, somebody who uses it approves';
|
|
141
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(account.name))} ${level}${account.description ? ` ${(0, server_client_1.printable)(account.description)}` : ''} from ${account.sources.map(server_client_1.printable).join(', ')}\n`);
|
|
140
142
|
}
|
|
141
|
-
if (me.
|
|
142
|
-
|
|
143
|
+
if (me.builds) {
|
|
144
|
+
const may = {
|
|
145
|
+
run: 'run published build files',
|
|
146
|
+
propose: 'propose build files, which somebody who publishes approves',
|
|
147
|
+
publish: 'publish build files',
|
|
148
|
+
}[me.builds];
|
|
149
|
+
environment.stdout.write(`you may ${may}\n`);
|
|
143
150
|
}
|
|
144
|
-
if (me.
|
|
145
|
-
environment.stdout.write(`
|
|
146
|
-
}
|
|
147
|
-
if (me.request && me.request.length > 0) {
|
|
148
|
-
environment.stdout.write(`may request runs, which an admin approves: ${me.request.join(', ')}\n`);
|
|
151
|
+
if (me.repos.length > 0) {
|
|
152
|
+
environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
|
|
149
153
|
}
|
|
150
154
|
}));
|
|
151
155
|
(0, remote_work_commands_1.registerWorkCommands)(remote, { environment, buildFile, getServer, guarded });
|
|
@@ -28,11 +28,38 @@ function describe(run) {
|
|
|
28
28
|
`${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
29
|
` ${(0, server_client_1.printable)(run.definition.name)} ${(0, server_client_1.printable)(run.definition.hash)}`,
|
|
30
30
|
` tasks: ${run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'selected by labels'}`,
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
` 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)}` : ''}`,
|
|
32
|
+
...(run.approvals ?? []).map((approval) => ` ${(0, server_client_1.printable)(approval.account)}: ${approval.approvedBy
|
|
33
|
+
? `approved by ${(0, server_client_1.printable)(approval.approvedBy.sub)}`
|
|
34
|
+
: 'waits for somebody who uses it to approve'}`),
|
|
35
|
+
...flagLines(run),
|
|
36
|
+
` decide before ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`,
|
|
34
37
|
];
|
|
35
38
|
}
|
|
39
|
+
// What to look at first, one line.
|
|
40
|
+
function flagLines(run) {
|
|
41
|
+
const flagged = Object.entries(run.plan ?? {}).filter(([, entry]) => entry.flags.length > 0);
|
|
42
|
+
return flagged.length > 0
|
|
43
|
+
? [
|
|
44
|
+
` look at: ${flagged
|
|
45
|
+
.map(([name, entry]) => `${(0, server_client_1.printable)(name)} (${entry.flags.map(server_client_1.printable).join(', ')})`)
|
|
46
|
+
.join(', ')}`,
|
|
47
|
+
]
|
|
48
|
+
: [];
|
|
49
|
+
}
|
|
50
|
+
// The tasks and services the run executes and what each does, never a secret's value.
|
|
51
|
+
function describePlan(plan) {
|
|
52
|
+
return Object.entries(plan).flatMap(([name, entry]) => [
|
|
53
|
+
` ${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
|
+
...entry.cmds.map((cmd) => ` $ ${(0, server_client_1.printable)(cmd)}`),
|
|
55
|
+
...(entry.deps.length > 0 ? [` after: ${entry.deps.map(server_client_1.printable).join(', ')}`] : []),
|
|
56
|
+
...(entry.needs.length > 0 ? [` needs: ${entry.needs.map(server_client_1.printable).join(', ')}`] : []),
|
|
57
|
+
...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)}`),
|
|
58
|
+
...(entry.mounts.length > 0 ? [` mounts: ${entry.mounts.map(server_client_1.printable).join(', ')}`] : []),
|
|
59
|
+
...(entry.ports.length > 0 ? [` ports: ${entry.ports.map(server_client_1.printable).join(', ')}`] : []),
|
|
60
|
+
...(entry.generates.length > 0 ? [` generates: ${entry.generates.map(server_client_1.printable).join(', ')}`] : []),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
36
63
|
function registerWorkCommands(remote, context) {
|
|
37
64
|
const { environment, buildFile, getServer, guarded } = context;
|
|
38
65
|
const connect = async (name) => {
|
|
@@ -75,6 +102,20 @@ function registerWorkCommands(remote, context) {
|
|
|
75
102
|
}
|
|
76
103
|
}
|
|
77
104
|
}));
|
|
105
|
+
remote
|
|
106
|
+
.command('show <run> [server]')
|
|
107
|
+
.description('show a run that waits for your approval in full: the commands, secrets and accounts of its tasks')
|
|
108
|
+
.action(guarded('Remote show was not successful', async (run, name) => {
|
|
109
|
+
if (!RUN_ID.test(run)) {
|
|
110
|
+
throw new Error(`${(0, server_client_1.printable)(run)} is not a run id, list them with: hammerkit remote approvals`);
|
|
111
|
+
}
|
|
112
|
+
const { server, token } = await connect(name);
|
|
113
|
+
const view = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/approvals/${run}`)).body;
|
|
114
|
+
for (const line of describe(view)) {
|
|
115
|
+
environment.stdout.write(`${line}\n`);
|
|
116
|
+
}
|
|
117
|
+
environment.stdout.write(view.plan ? `${describePlan(view.plan).join('\n')}\n` : 'the server did not record what its tasks do\n');
|
|
118
|
+
}));
|
|
78
119
|
remote
|
|
79
120
|
.command('proposals [server]')
|
|
80
121
|
.description('list the proposed versions of build files you made or may approve')
|
|
@@ -92,6 +133,17 @@ function registerWorkCommands(remote, context) {
|
|
|
92
133
|
environment.stdout.write(`${colors_1.default.bold(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)}`)} proposed by ${who}, ${state}\n`);
|
|
93
134
|
const names = [...proposal.tasks, ...proposal.services];
|
|
94
135
|
environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
136
|
+
if (proposal.accounts.length > 0) {
|
|
137
|
+
environment.stdout.write(` accounts: ${proposal.accounts.map(server_client_1.printable).join(', ')}${proposal.missing.length > 0
|
|
138
|
+
? `, the proposer does not use ${proposal.missing.map(server_client_1.printable).join(', ')}`
|
|
139
|
+
: ''}\n`);
|
|
140
|
+
}
|
|
141
|
+
for (const line of flagLines(proposal)) {
|
|
142
|
+
environment.stdout.write(`${line}\n`);
|
|
143
|
+
}
|
|
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
|
+
}
|
|
95
147
|
if (proposal.state === 'waiting') {
|
|
96
148
|
environment.stdout.write(` decide before ${(0, server_client_1.printable)(proposal.expiresAt)}\n`);
|
|
97
149
|
}
|
|
@@ -117,9 +169,29 @@ function registerWorkCommands(remote, context) {
|
|
|
117
169
|
: undefined;
|
|
118
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`);
|
|
119
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
|
+
}
|
|
120
175
|
const lines = (0, text_diff_1.diffBundles)(base, proposed);
|
|
121
176
|
environment.stdout.write(lines.length > 0 ? `${lines.join('\n')}\n` : 'the files are the same\n');
|
|
122
177
|
}));
|
|
178
|
+
for (const action of ['yank', 'unyank']) {
|
|
179
|
+
remote
|
|
180
|
+
.command(`${action} <version> [server]`)
|
|
181
|
+
.description(action === 'yank'
|
|
182
|
+
? 'pull a published version (name@hash) back: no run starts from it and it is not latest'
|
|
183
|
+
: 'let a yanked version (name@hash) go on')
|
|
184
|
+
.addOption(new commander_1.Option('--reason <text>', 'why, shown to whoever tries to run it').argParser((value) => value))
|
|
185
|
+
.action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
|
|
186
|
+
const version = parseProposal(target);
|
|
187
|
+
if (!version) {
|
|
188
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it like payments@sha256:...`);
|
|
189
|
+
}
|
|
190
|
+
const { name: serverName, server, token } = await connect(name);
|
|
191
|
+
await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${encodeURIComponent(version.name)}/${version.hash}/${action}`, options.reason ? { reason: options.reason } : undefined);
|
|
192
|
+
environment.stdout.write(`${action === 'yank' ? 'yanked' : 'let go on'} ${(0, server_client_1.printable)(target)} on ${serverName}\n`);
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
123
195
|
for (const action of ['approve', 'reject']) {
|
|
124
196
|
remote
|
|
125
197
|
.command(`${action} <target> [server]`)
|
|
@@ -26,8 +26,12 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
26
26
|
projectRoot: await (0, project_root_1.findProjectRoot)(mainScope.cwd, environment),
|
|
27
27
|
};
|
|
28
28
|
for (const file of Object.values(ctx.files)) {
|
|
29
|
+
if (environment.hosted && (file.schema.secretProviders || file.schema.secretAccounts)) {
|
|
30
|
+
throw new Error(`${file.fileName} declares secret providers or accounts, which a server defines in its own config`);
|
|
31
|
+
}
|
|
29
32
|
if (!reference.envFiles[file.cwd]) {
|
|
30
|
-
|
|
33
|
+
// a server does not read the `.env` of a checkout: it is code of the project
|
|
34
|
+
reference.envFiles[file.cwd] = environment.hosted ? {} : await (0, read_env_file_1.readEnvFile)(file.cwd, environment);
|
|
31
35
|
}
|
|
32
36
|
const buildFileEnvs = file.schema.envs;
|
|
33
37
|
if (file.schema.tasks) {
|
|
@@ -92,7 +96,9 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
92
96
|
resolveInit(reference, service);
|
|
93
97
|
}
|
|
94
98
|
reference.caches = (0, resolve_cache_1.withBuiltinCaches)(declaredCaches);
|
|
95
|
-
reference.secrets =
|
|
99
|
+
reference.secrets = environment.hosted
|
|
100
|
+
? { ...environment.hosted.catalog, explicitAccounts: true }
|
|
101
|
+
: (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference);
|
|
96
102
|
return reference;
|
|
97
103
|
}
|
|
98
104
|
exports.parseReferences = parseReferences;
|
|
@@ -16,6 +16,10 @@ function keyOf(binding, ref) {
|
|
|
16
16
|
return [binding.definition.name, binding.account.name, ref].join('\0');
|
|
17
17
|
}
|
|
18
18
|
function fetchProviderValue(binding, ref, environment) {
|
|
19
|
+
const allowed = environment.hosted?.accounts;
|
|
20
|
+
if (allowed && !allowed.includes(binding.account.name)) {
|
|
21
|
+
return Promise.reject(new Error(`the run may not read secrets as ${binding.account.name}`));
|
|
22
|
+
}
|
|
19
23
|
let values = runs.get(environment);
|
|
20
24
|
if (!values) {
|
|
21
25
|
values = new Map();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleAccounts = void 0;
|
|
4
|
+
const http_1 = require("./http");
|
|
5
|
+
const policy_1 = require("./policy");
|
|
6
|
+
/**
|
|
7
|
+
* Who the caller is and what they may do:
|
|
8
|
+
*
|
|
9
|
+
* GET /v1/me identity, accounts with their level, repositories and the builds level
|
|
10
|
+
* GET /v1/accounts the accounts the caller has a level on, with what the operator says about them
|
|
11
|
+
*/
|
|
12
|
+
async function handleAccounts(config, log, principal, req, res, url) {
|
|
13
|
+
if (url.pathname !== '/v1/me' && url.pathname !== '/v1/accounts') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (req.method !== 'GET') {
|
|
17
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
21
|
+
const accounts = Object.entries(grant.accounts)
|
|
22
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
23
|
+
.map(([name, level]) => ({ name, level }));
|
|
24
|
+
if (url.pathname === '/v1/me') {
|
|
25
|
+
log(`grant iss=${principal.iss} sub=${principal.sub} accounts=[${accounts
|
|
26
|
+
.map((account) => `${account.name}:${account.level}`)
|
|
27
|
+
.join(',')}] entitlements=[${grant.matched.join(',')}]`);
|
|
28
|
+
(0, http_1.send)(res, 200, { iss: principal.iss, sub: principal.sub, accounts, repos: grant.repos, builds: grant.builds });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
(0, http_1.send)(res, 200, {
|
|
32
|
+
accounts: accounts.map(({ name, level }) => ({
|
|
33
|
+
name,
|
|
34
|
+
...(config.accounts[name]?.description ? { description: config.accounts[name].description } : {}),
|
|
35
|
+
level,
|
|
36
|
+
sources: config.accounts[name]?.source ?? ['upload', 'ref'],
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
exports.handleAccounts = handleAccounts;
|
|
42
|
+
//# sourceMappingURL=accounts-api.js.map
|
|
@@ -17,15 +17,16 @@ function toApprovalView(run) {
|
|
|
17
17
|
createdAt: run.createdAt,
|
|
18
18
|
definition: run.definition,
|
|
19
19
|
tasks: run.tasks,
|
|
20
|
-
|
|
20
|
+
accounts: run.accounts,
|
|
21
|
+
plan: run.plan,
|
|
21
22
|
source: run.source,
|
|
22
23
|
options: run.options,
|
|
23
|
-
|
|
24
|
+
approvals: run.approvals,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
* The runs waiting for an approval, which
|
|
28
|
-
* use
|
|
28
|
+
* The runs waiting for an approval, which somebody who uses the accounts they
|
|
29
|
+
* need gives (one person is enough when they use all of them):
|
|
29
30
|
*
|
|
30
31
|
* GET /v1/approvals the runs the caller may approve
|
|
31
32
|
* GET /v1/approvals/<id> one of them
|
|
@@ -42,7 +43,7 @@ async function handleApprovals(config, registry, log, principal, req, res, url)
|
|
|
42
43
|
}
|
|
43
44
|
const [, id, action] = match;
|
|
44
45
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
45
|
-
const canDecide = (account) => (0, policy_1.
|
|
46
|
+
const canDecide = (account) => (0, policy_1.canUse)(grant, account);
|
|
46
47
|
if (!id) {
|
|
47
48
|
if (req.method !== 'GET') {
|
|
48
49
|
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
@@ -50,7 +51,8 @@ async function handleApprovals(config, registry, log, principal, req, res, url)
|
|
|
50
51
|
}
|
|
51
52
|
const open = registry
|
|
52
53
|
.waiting()
|
|
53
|
-
.filter((run) => !(run.owner.iss === principal.iss && run.owner.sub === principal.sub) &&
|
|
54
|
+
.filter((run) => !(run.owner.iss === principal.iss && run.owner.sub === principal.sub) &&
|
|
55
|
+
(run.approvals ?? []).some((approval) => !approval.approvedBy && canDecide(approval.account)));
|
|
54
56
|
(0, http_1.send)(res, 200, { approvals: open.map(toApprovalView) });
|
|
55
57
|
return true;
|
|
56
58
|
}
|
|
@@ -90,6 +92,9 @@ async function handleApprovals(config, registry, log, principal, req, res, url)
|
|
|
90
92
|
case 'not-waiting':
|
|
91
93
|
(0, http_1.send)(res, 409, { error: `the run is ${result.state}, it does not wait for an approval` });
|
|
92
94
|
break;
|
|
95
|
+
case 'already-approved':
|
|
96
|
+
(0, http_1.send)(res, 409, { error: 'you approved all you can, the run waits for somebody who uses the other accounts' });
|
|
97
|
+
break;
|
|
93
98
|
default:
|
|
94
99
|
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
95
100
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleDefinitions = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
4
5
|
const bundle_1 = require("../remote/bundle");
|
|
5
6
|
const definitions_1 = require("./definitions");
|
|
6
7
|
const http_1 = require("./http");
|
|
8
|
+
const oauth_1 = require("./oauth");
|
|
7
9
|
const policy_1 = require("./policy");
|
|
8
10
|
const proposals_1 = require("./proposals");
|
|
9
11
|
const server_config_1 = require("./server-config");
|
|
@@ -18,13 +20,16 @@ const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
|
|
|
18
20
|
* GET /v1/definitions/<name> the versions of one
|
|
19
21
|
* GET /v1/definitions/<name>/<hash|latest> one version
|
|
20
22
|
* GET /v1/definitions/<name>/<hash|latest>/files the files of one version
|
|
23
|
+
* POST /v1/definitions/<name>/<hash>/yank pull a version back (body: optional {reason})
|
|
24
|
+
* POST /v1/definitions/<name>/<hash>/unyank let it go on
|
|
21
25
|
*/
|
|
22
26
|
async function handleDefinitions(config, store, proposals, log, principal, req, res, url) {
|
|
23
|
-
const match = /^\/v1\/definitions(?:\/([^/]+)(?:\/([^/]+)(\/files)?)?)?$/.exec(url.pathname);
|
|
27
|
+
const match = /^\/v1\/definitions(?:\/([^/]+)(?:\/([^/]+)(\/files|\/yank|\/unyank)?)?)?$/.exec(url.pathname);
|
|
24
28
|
if (!match) {
|
|
25
29
|
return false;
|
|
26
30
|
}
|
|
27
|
-
const [, name, version,
|
|
31
|
+
const [, name, version, suffix] = match;
|
|
32
|
+
const files = suffix === '/files';
|
|
28
33
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
29
34
|
if (name !== undefined && !server_config_1.definitionName.safeParse(name).success) {
|
|
30
35
|
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
@@ -34,8 +39,14 @@ async function handleDefinitions(config, store, proposals, log, principal, req,
|
|
|
34
39
|
await publish(config, store, proposals, log, principal, name, req, res);
|
|
35
40
|
return true;
|
|
36
41
|
}
|
|
42
|
+
if ((suffix === '/yank' || suffix === '/unyank') && req.method === 'POST') {
|
|
43
|
+
await yank(config, store, log, principal, name, version, suffix === '/unyank', req, res);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
37
46
|
if (req.method !== 'GET') {
|
|
38
|
-
(0, http_1.send)(res, 405, { error: 'method not allowed' }, {
|
|
47
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, {
|
|
48
|
+
allow: suffix === '/yank' || suffix === '/unyank' ? 'POST' : name !== undefined && !version ? 'GET, PUT' : 'GET',
|
|
49
|
+
});
|
|
39
50
|
return true;
|
|
40
51
|
}
|
|
41
52
|
// what the caller may not see does not exist for them
|
|
@@ -71,12 +82,40 @@ async function handleDefinitions(config, store, proposals, log, principal, req,
|
|
|
71
82
|
return true;
|
|
72
83
|
}
|
|
73
84
|
exports.handleDefinitions = handleDefinitions;
|
|
85
|
+
const yankSchema = zod_1.z.object({ reason: zod_1.z.string().max(500).optional() }).strict();
|
|
86
|
+
// Yanking is allowed to who publishes the build file or uses an account the version
|
|
87
|
+
// uses; letting a version go on again is the publisher's call.
|
|
88
|
+
async function yank(config, store, log, principal, name, hash, undo, req, res) {
|
|
89
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
90
|
+
const version = bundle_1.HASH_PATTERN.test(hash) ? store.find(name, hash) : undefined;
|
|
91
|
+
if (!version || !(0, policy_1.maySeeDefinitions)(grant)) {
|
|
92
|
+
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (undo ? !(0, policy_1.mayPublish)(grant) : !(0, policy_1.mayYank)(grant, version)) {
|
|
96
|
+
log(`refuse ${undo ? 'unyank' : 'yank'} ${name}@${hash} iss=${principal.iss} sub=${principal.sub}`);
|
|
97
|
+
(0, http_1.send)(res, 403, {
|
|
98
|
+
error: undo
|
|
99
|
+
? 'a version is let go on by who publishes the build file'
|
|
100
|
+
: 'a version is yanked by who publishes the build file or uses an account it uses',
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const body = yankSchema.safeParse(req.headers['content-type'] ? await (0, http_1.readJson)(req) : {});
|
|
105
|
+
if (!body.success) {
|
|
106
|
+
const issue = body.error.issues[0];
|
|
107
|
+
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const changed = await store.yank(name, hash, principal, { reason: body.data.reason, undo });
|
|
111
|
+
log(`${undo ? 'unyank' : 'yank'} ${name}@${hash} iss=${principal.iss} sub=${principal.sub}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
|
|
112
|
+
(0, http_1.send)(res, 200, changed);
|
|
113
|
+
}
|
|
74
114
|
async function publish(config, store, proposals, log, principal, name, req, res) {
|
|
75
115
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
(0, http_1.send)(res, 403, { error: `you may not publish ${name}` });
|
|
116
|
+
if (!(0, policy_1.mayPublish)(grant) && !(0, policy_1.mayPropose)(grant)) {
|
|
117
|
+
log(`refuse publish iss=${principal.iss} sub=${principal.sub}: no builds permission`);
|
|
118
|
+
(0, http_1.send)(res, 403, { error: `you may not publish ${name}, it needs builds: propose or publish` });
|
|
80
119
|
return;
|
|
81
120
|
}
|
|
82
121
|
const parsed = bundle_1.bundleSchema.safeParse(await (0, http_1.readJson)(req, MAX_PUBLISH_BYTES));
|
|
@@ -86,22 +125,27 @@ async function publish(config, store, proposals, log, principal, name, req, res)
|
|
|
86
125
|
return;
|
|
87
126
|
}
|
|
88
127
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
// a version can't run before it is approved, but it has to load, as it would
|
|
129
|
+
// have when published, so the approver is not asked about a broken one
|
|
130
|
+
const { version, existing } = await store.prepare(name, parsed.data, principal, {
|
|
131
|
+
noRemote: !(0, policy_1.mayPublish)(grant),
|
|
132
|
+
});
|
|
133
|
+
if (existing) {
|
|
134
|
+
(0, http_1.send)(res, 200, { ...existing, created: false });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// a version that names an account is published by somebody who uses it; anyone
|
|
138
|
+
// else proposes it, and somebody who does approves it (four eyes)
|
|
139
|
+
const missing = (version.accounts ?? []).filter((account) => !(0, policy_1.canUse)(grant, account));
|
|
140
|
+
if (!(0, policy_1.mayPublish)(grant) || missing.length > 0) {
|
|
141
|
+
const { proposal, created } = proposals.propose(version, parsed.data, principal, missing, store.find(name, 'latest')?.hash ?? null);
|
|
142
|
+
log(`propose ${name} ${proposal.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${proposal.tasks.length} missing=[${missing.join(',')}]`);
|
|
99
143
|
(0, http_1.send)(res, created ? 202 : 200, { ...proposal, created });
|
|
100
144
|
return;
|
|
101
145
|
}
|
|
102
|
-
const { version, created } = await store.publish(name, parsed.data, principal);
|
|
103
|
-
log(`publish ${name} ${
|
|
104
|
-
(0, http_1.send)(res, created ? 201 : 200, { ...
|
|
146
|
+
const { version: published, created } = await store.publish(name, parsed.data, principal);
|
|
147
|
+
log(`publish ${name} ${published.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${published.tasks.length}`);
|
|
148
|
+
(0, http_1.send)(res, created ? 201 : 200, { ...published, created });
|
|
105
149
|
}
|
|
106
150
|
catch (e) {
|
|
107
151
|
if (e instanceof definitions_1.DefinitionError) {
|
|
@@ -110,7 +154,7 @@ async function publish(config, store, proposals, log, principal, name, req, res)
|
|
|
110
154
|
return;
|
|
111
155
|
}
|
|
112
156
|
if (e instanceof proposals_1.ProposalError) {
|
|
113
|
-
(0, http_1.send)(res,
|
|
157
|
+
(0, http_1.send)(res, e.status, { error: e.message });
|
|
114
158
|
return;
|
|
115
159
|
}
|
|
116
160
|
throw e;
|