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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.detectPlatform = exports.readServerConfig = exports.serverConfigSchema = exports.entitlementSchema = exports.definitionName = exports.issuerSchema = exports.serverPlatformSchema = void 0;
|
|
3
|
+
exports.detectPlatform = exports.getSecretCatalog = exports.readServerConfig = exports.getSourceAccounts = exports.serverConfigSchema = exports.sourceSchema = exports.entitlementSchema = exports.buildsLevelSchema = exports.accountLevelSchema = exports.accountSchema = exports.definitionName = exports.issuerSchema = exports.serverPlatformSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const yaml_1 = require("yaml");
|
|
6
6
|
const log_1 = require("../log");
|
|
7
7
|
const units_1 = require("../utils/units");
|
|
8
|
+
const build_file_secret_provider_schema_1 = require("../schema/build-file-secret-provider-schema");
|
|
9
|
+
const repo_1 = require("./repo");
|
|
8
10
|
const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost', '::1'];
|
|
9
11
|
exports.serverPlatformSchema = zod_1.z
|
|
10
12
|
.object({
|
|
@@ -62,50 +64,79 @@ function checkWho(who, ctx) {
|
|
|
62
64
|
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['claims', 'match'], message: 'match at least one claim' });
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
|
-
|
|
67
|
+
// The service accounts a server knows. A run never names one: the accounts it
|
|
68
|
+
// uses are the ones its secrets are read as, and an entitlement says who may use
|
|
69
|
+
// or only request each.
|
|
70
|
+
const accountNameSchema = zod_1.z
|
|
71
|
+
.string()
|
|
72
|
+
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, 'a name starts with a letter and holds letters, digits, - and _');
|
|
73
|
+
exports.accountSchema = zod_1.z
|
|
66
74
|
.object({
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
// (someone with it in `accounts`) approves them
|
|
73
|
-
request: zod_1.z.array(zod_1.z.string().min(1)).optional(),
|
|
74
|
-
// the build files (by name) the caller may publish, `*` for any; a separate
|
|
75
|
-
// permission from running them, so who defines work and who runs it can differ
|
|
76
|
-
publish: zod_1.z.array(zod_1.z.union([zod_1.z.literal('*'), exports.definitionName])).optional(),
|
|
77
|
-
// the build files the caller may propose a new version of: it can not run until
|
|
78
|
-
// someone who may `publish` that name approves it
|
|
79
|
-
propose: zod_1.z.array(zod_1.z.union([zod_1.z.literal('*'), exports.definitionName])).optional(),
|
|
80
|
-
// the account used when the caller names none; one of accounts or request
|
|
81
|
-
default: zod_1.z.string().min(1).optional(),
|
|
82
|
-
// where the code may come from: the caller's working tree or a git ref
|
|
75
|
+
description: zod_1.z.string().min(1).max(200).optional(),
|
|
76
|
+
// per secret provider, the environment that makes its command act as this
|
|
77
|
+
// account; the credentials are files and variables of the operator's machine
|
|
78
|
+
providers: build_file_secret_provider_schema_1.buildFileSecretAccountSchema.shape.providers.default({}),
|
|
79
|
+
// where the code of a run using this account may come from; both when left out
|
|
83
80
|
source: zod_1.z
|
|
84
81
|
.array(zod_1.z.enum(['upload', 'ref']))
|
|
85
82
|
.min(1)
|
|
86
83
|
.optional(),
|
|
84
|
+
// the refs (branches or tags, `release/*` for a level) a run using this account
|
|
85
|
+
// may be started from; a commit is not accepted then, it names no branch
|
|
86
|
+
refs: zod_1.z.array(zod_1.z.string().min(1).max(200)).min(1).optional(),
|
|
87
87
|
})
|
|
88
88
|
.strict()
|
|
89
|
-
.superRefine((
|
|
90
|
-
|
|
91
|
-
if (entitlement.accounts.length === 0 &&
|
|
92
|
-
(entitlement.request ?? []).length === 0 &&
|
|
93
|
-
(entitlement.propose ?? []).length === 0 &&
|
|
94
|
-
(entitlement.publish ?? []).length === 0) {
|
|
89
|
+
.superRefine((account, ctx) => {
|
|
90
|
+
if (account.refs && (account.source?.length !== 1 || account.source[0] !== 'ref')) {
|
|
95
91
|
ctx.addIssue({
|
|
96
92
|
code: zod_1.z.ZodIssueCode.custom,
|
|
97
|
-
|
|
93
|
+
path: ['refs'],
|
|
94
|
+
message: 'refs only restricts runs from a git ref, set source: [ref] too, an upload names no ref',
|
|
98
95
|
});
|
|
99
96
|
}
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
});
|
|
98
|
+
exports.accountLevelSchema = zod_1.z.enum(['use', 'request']);
|
|
99
|
+
// What a caller may do with the build files: run what is published, propose new
|
|
100
|
+
// versions that someone else approves, or publish. Each level includes the lower.
|
|
101
|
+
exports.buildsLevelSchema = zod_1.z.enum(['run', 'propose', 'publish']);
|
|
102
|
+
const repoPatternSchema = zod_1.z.string().refine(repo_1.isRepoPattern, 'expected host/path, such as github.com/corp/*');
|
|
103
|
+
exports.entitlementSchema = zod_1.z
|
|
104
|
+
.object({
|
|
105
|
+
...whoShape,
|
|
106
|
+
// `use`: runs on the account at once and approves the runs others request on it;
|
|
107
|
+
// `request`: runs wait until somebody who may `use` the account approves them
|
|
108
|
+
accounts: zod_1.z.record(accountNameSchema, exports.accountLevelSchema).default({}),
|
|
109
|
+
// the repositories (`host/path` patterns) the caller may run from a git ref
|
|
110
|
+
repos: zod_1.z.array(repoPatternSchema).default([]),
|
|
111
|
+
builds: exports.buildsLevelSchema.optional(),
|
|
112
|
+
})
|
|
113
|
+
.strict()
|
|
114
|
+
.superRefine((entitlement, ctx) => {
|
|
115
|
+
checkWho(entitlement, ctx);
|
|
116
|
+
if (Object.keys(entitlement.accounts).length === 0 && entitlement.builds === undefined) {
|
|
102
117
|
ctx.addIssue({
|
|
103
118
|
code: zod_1.z.ZodIssueCode.custom,
|
|
104
|
-
|
|
105
|
-
message: 'default has to be one of accounts or request',
|
|
119
|
+
message: 'name accounts to use or request, or a builds level',
|
|
106
120
|
});
|
|
107
121
|
}
|
|
108
122
|
});
|
|
123
|
+
// A repository the server can read: `github.com/corp/*` for an organisation,
|
|
124
|
+
// `gitlab.corp.com/platform/**` for a group with its subgroups. The credential is a
|
|
125
|
+
// secret of a provider, read as `account`, and handed to git for that fetch only.
|
|
126
|
+
exports.sourceSchema = zod_1.z
|
|
127
|
+
.object({
|
|
128
|
+
match: repoPatternSchema,
|
|
129
|
+
secret: zod_1.z
|
|
130
|
+
.object({
|
|
131
|
+
from: zod_1.z.string().regex(/^[A-Za-z][A-Za-z0-9_-]*:.+$/, 'expected provider:reference'),
|
|
132
|
+
account: accountNameSchema,
|
|
133
|
+
// the user name of the basic authentication the token is sent with
|
|
134
|
+
username: zod_1.z.string().min(1).default('x-access-token'),
|
|
135
|
+
})
|
|
136
|
+
.strict()
|
|
137
|
+
.optional(),
|
|
138
|
+
})
|
|
139
|
+
.strict();
|
|
109
140
|
const duration = zod_1.z.string().refine((value) => {
|
|
110
141
|
try {
|
|
111
142
|
return (0, units_1.parseDuration)(value) > 0;
|
|
@@ -136,7 +167,11 @@ exports.serverConfigSchema = zod_1.z
|
|
|
136
167
|
// the OAuth issuers whose tokens the server accepts, and the audience those
|
|
137
168
|
// tokens have to carry
|
|
138
169
|
issuers: zod_1.z.array(exports.issuerSchema).min(1, 'at least one issuer is needed, OAuth is the only way to call the server'),
|
|
139
|
-
//
|
|
170
|
+
// the service accounts, and the commands that read secrets as them; a build file
|
|
171
|
+
// can not declare either, only name an account on its secrets
|
|
172
|
+
accounts: zod_1.z.record(accountNameSchema, exports.accountSchema).default({}),
|
|
173
|
+
secretProviders: build_file_secret_provider_schema_1.buildFileSecretProvidersSchema.default({}),
|
|
174
|
+
// who may use which service account and which build files; default deny
|
|
140
175
|
entitlements: zod_1.z.array(exports.entitlementSchema).default([]),
|
|
141
176
|
approvals: zod_1.z
|
|
142
177
|
.object({
|
|
@@ -144,21 +179,24 @@ exports.serverConfigSchema = zod_1.z
|
|
|
144
179
|
timeout: duration.default('1h'),
|
|
145
180
|
// runs waiting at the same time, more are refused
|
|
146
181
|
maxWaiting: zod_1.z.number().int().min(1).default(100),
|
|
182
|
+
// after a version was rejected, who proposed it can not propose that build
|
|
183
|
+
// file again, and nobody that version, for this long
|
|
184
|
+
cooldown: duration.default('10m'),
|
|
147
185
|
})
|
|
148
186
|
.strict()
|
|
149
187
|
.default({}),
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.
|
|
161
|
-
.default(
|
|
188
|
+
// the repositories a run may get its code from, as patterns of `host/path`, and
|
|
189
|
+
// the secret that reads them: a token in a secret provider, read as a service
|
|
190
|
+
// account of the operator. The caller names a repository and a ref in the request,
|
|
191
|
+
// never a url.
|
|
192
|
+
sources: zod_1.z.array(exports.sourceSchema).default([]),
|
|
193
|
+
// what tasks may run on: containers, and the local runtime that runs commands on
|
|
194
|
+
// this machine as the user of the server. Local tasks read the whole file system of that
|
|
195
|
+
// user, so they have to be allowed by name.
|
|
196
|
+
runtimes: zod_1.z
|
|
197
|
+
.array(zod_1.z.enum(['docker', 'local']))
|
|
198
|
+
.min(1)
|
|
199
|
+
.default(['docker']),
|
|
162
200
|
// where the server keeps what must survive a restart: the published build files
|
|
163
201
|
storage: zod_1.z
|
|
164
202
|
.object({ directory: zod_1.z.string().min(1).optional() })
|
|
@@ -171,6 +209,39 @@ exports.serverConfigSchema = zod_1.z
|
|
|
171
209
|
maxConcurrent: zod_1.z.number().int().min(1).default(4),
|
|
172
210
|
// finished runs that are kept for their state and logs
|
|
173
211
|
keep: zod_1.z.number().int().min(1).default(200),
|
|
212
|
+
// what one caller (issuer and subject) may have at a time, so a single
|
|
213
|
+
// caller can not take the whole machine; over it a request gets 429
|
|
214
|
+
limits: zod_1.z
|
|
215
|
+
.object({
|
|
216
|
+
// runs that wait for an approval
|
|
217
|
+
waiting: zod_1.z.number().int().min(1).default(10),
|
|
218
|
+
// runs that wait in the queue for a free place
|
|
219
|
+
queued: zod_1.z.number().int().min(1).default(20),
|
|
220
|
+
// runs that execute at the same time, the others wait in the queue
|
|
221
|
+
running: zod_1.z.number().int().min(1).default(2),
|
|
222
|
+
})
|
|
223
|
+
.strict()
|
|
224
|
+
.default({}),
|
|
225
|
+
// a run that executes longer is stopped and fails
|
|
226
|
+
maxDuration: duration.default('1h'),
|
|
227
|
+
// log output kept per run; the rest is cut off
|
|
228
|
+
maxLogBytes: zod_1.z
|
|
229
|
+
.number()
|
|
230
|
+
.int()
|
|
231
|
+
.min(1024)
|
|
232
|
+
.default(1024 * 1024),
|
|
233
|
+
// the environment of a run: the variables of the server process that tasks
|
|
234
|
+
// (and the git fetch) can see. Nothing else of the server's environment
|
|
235
|
+
// is passed on, so there is no list built in; an operator names what the
|
|
236
|
+
// tools of the host need, such as PATH and DOCKER_HOST.
|
|
237
|
+
env: zod_1.z
|
|
238
|
+
.object({
|
|
239
|
+
allow: zod_1.z.array(zod_1.z.string().regex(/^[A-Za-z_][A-Za-z0-9_()]*$/)).default([]),
|
|
240
|
+
// fixed values that are set in addition
|
|
241
|
+
set: zod_1.z.record(zod_1.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), zod_1.z.string()).default({}),
|
|
242
|
+
})
|
|
243
|
+
.strict()
|
|
244
|
+
.default({}),
|
|
174
245
|
})
|
|
175
246
|
.strict()
|
|
176
247
|
.default({}),
|
|
@@ -196,6 +267,70 @@ exports.serverConfigSchema = zod_1.z
|
|
|
196
267
|
});
|
|
197
268
|
}
|
|
198
269
|
});
|
|
270
|
+
for (const [name, account] of Object.entries(config.accounts)) {
|
|
271
|
+
for (const provider of Object.keys(account.providers)) {
|
|
272
|
+
if (!config.secretProviders[provider]) {
|
|
273
|
+
ctx.addIssue({
|
|
274
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
275
|
+
path: ['accounts', name, 'providers', provider],
|
|
276
|
+
message: `secret provider ${provider} is not declared in secretProviders`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
config.entitlements.forEach((entitlement, index) => {
|
|
282
|
+
for (const account of Object.keys(entitlement.accounts)) {
|
|
283
|
+
if (!config.accounts[account]) {
|
|
284
|
+
const known = Object.keys(config.accounts);
|
|
285
|
+
ctx.addIssue({
|
|
286
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
287
|
+
path: ['entitlements', index, 'accounts', account],
|
|
288
|
+
message: `unknown account ${account}, ${known.length > 0 ? `accounts: ${known.join(', ')}` : 'none is defined'}`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
config.sources.forEach((source, index) => {
|
|
294
|
+
if (!source.secret) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const { from, account } = source.secret;
|
|
298
|
+
const kind = from.substring(0, from.indexOf(':'));
|
|
299
|
+
const where = ['sources', index, 'secret'];
|
|
300
|
+
if (!config.accounts[account]) {
|
|
301
|
+
ctx.addIssue({
|
|
302
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
303
|
+
path: [...where, 'account'],
|
|
304
|
+
message: `unknown account ${account}`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
else if (!config.accounts[account].providers[kind]) {
|
|
308
|
+
ctx.addIssue({
|
|
309
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
310
|
+
path: [...where, 'from'],
|
|
311
|
+
message: `account ${account} has no credentials for the provider ${kind}`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (!config.secretProviders[kind]) {
|
|
315
|
+
ctx.addIssue({
|
|
316
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
317
|
+
path: [...where, 'from'],
|
|
318
|
+
message: `secret provider ${kind} is not declared in secretProviders`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const readers = getSourceAccounts(config);
|
|
323
|
+
config.entitlements.forEach((entitlement, index) => {
|
|
324
|
+
for (const account of Object.keys(entitlement.accounts)) {
|
|
325
|
+
if (readers.includes(account)) {
|
|
326
|
+
ctx.addIssue({
|
|
327
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
328
|
+
path: ['entitlements', index, 'accounts', account],
|
|
329
|
+
message: `the account ${account} reads the sources, no entitlement may name it`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
199
334
|
if (!config.tls && !config.behindTlsProxy && !LOOPBACK_HOSTS.includes(config.listen.host)) {
|
|
200
335
|
ctx.addIssue({
|
|
201
336
|
code: zod_1.z.ZodIssueCode.custom,
|
|
@@ -204,6 +339,11 @@ exports.serverConfigSchema = zod_1.z
|
|
|
204
339
|
});
|
|
205
340
|
}
|
|
206
341
|
});
|
|
342
|
+
// The accounts that read repositories: no caller uses them and no build file names them.
|
|
343
|
+
function getSourceAccounts(config) {
|
|
344
|
+
return [...new Set(config.sources.flatMap((source) => (source.secret ? [source.secret.account] : [])))];
|
|
345
|
+
}
|
|
346
|
+
exports.getSourceAccounts = getSourceAccounts;
|
|
207
347
|
async function readServerConfig(environment, path) {
|
|
208
348
|
if (!(await environment.file.exists(path))) {
|
|
209
349
|
throw new Error(`unable to read ${path}: no such file`);
|
|
@@ -224,6 +364,27 @@ async function readServerConfig(environment, path) {
|
|
|
224
364
|
return result.data;
|
|
225
365
|
}
|
|
226
366
|
exports.readServerConfig = readServerConfig;
|
|
367
|
+
// The secret providers and accounts of the config as the engine reads them, so a
|
|
368
|
+
// secret of a build file is read as an account of the operator.
|
|
369
|
+
function getSecretCatalog(config) {
|
|
370
|
+
const providers = {};
|
|
371
|
+
for (const [name, provider] of Object.entries(config.secretProviders)) {
|
|
372
|
+
providers[name] = {
|
|
373
|
+
name,
|
|
374
|
+
type: 'command',
|
|
375
|
+
command: provider.command,
|
|
376
|
+
env: provider.env ?? {},
|
|
377
|
+
timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? '30s'),
|
|
378
|
+
declaredIn: 'the server config',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const accounts = {};
|
|
382
|
+
for (const [name, account] of Object.entries(config.accounts)) {
|
|
383
|
+
accounts[name] = { name, default: false, providers: account.providers, declaredIn: 'the server config' };
|
|
384
|
+
}
|
|
385
|
+
return { providers, accounts, explicitAccounts: true };
|
|
386
|
+
}
|
|
387
|
+
exports.getSecretCatalog = getSecretCatalog;
|
|
227
388
|
function detectPlatform() {
|
|
228
389
|
const os = { darwin: 'macos', win32: 'windows' }[process.platform] ?? 'linux';
|
|
229
390
|
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
|