hammerkit 1.27.0-next.21 → 1.27.0-next.23
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 +70 -0
- package/dist/executer/docker-task.js +21 -1
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/sandbox-runner.js +3 -1
- package/dist/planner/utils/append-work-task.js +3 -1
- package/dist/planner/work-runtime-kubernetes.js +4 -2
- package/dist/remote/host-config.js +5 -0
- package/dist/remote/remote-work-commands.js +33 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/build-file-container-task-schema.js +4 -0
- package/dist/schema/build-file-include-schema.js +12 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +99 -10
- package/dist/schema/schema-parser.js +56 -37
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/definitions-api.js +62 -1
- package/dist/server/executor.js +1 -0
- package/dist/server/kubernetes-backend.js +71 -8
- package/dist/server/plan.js +62 -41
- package/dist/server/run-worker.js +9 -1
- package/dist/server/server-config.js +29 -0
- package/dist/server/server.js +6 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.findService = exports.findTask = exports.parseReferences = void 0;
|
|
3
|
+
exports.findService = exports.findTask = exports.parseReferences = exports.getPublishedFiles = void 0;
|
|
4
4
|
const build_file_task_schema_1 = require("./build-file-task-schema");
|
|
5
5
|
const build_file_service_schema_1 = require("./build-file-service-schema");
|
|
6
6
|
const split_name_1 = require("../planner/utils/split-name");
|
|
@@ -10,11 +10,59 @@ const merge_environment_variables_1 = require("../environment/merge-environment-
|
|
|
10
10
|
const resolve_cache_1 = require("../cache/resolve-cache");
|
|
11
11
|
const project_root_1 = require("../planner/project-root");
|
|
12
12
|
const secret_catalog_1 = require("../secrets/secret-catalog");
|
|
13
|
+
const host_config_1 = require("../remote/host-config");
|
|
14
|
+
// The providers and accounts of the host config, see HostSecretDeclarations
|
|
15
|
+
async function readHostSecrets(environment) {
|
|
16
|
+
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
17
|
+
return {
|
|
18
|
+
fileName: (0, host_config_1.getHostConfigPath)(environment),
|
|
19
|
+
secretProviders: config.secretProviders,
|
|
20
|
+
secretAccounts: config.secretAccounts,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// What a task or service names that a server left out (`publish: false`)
|
|
24
|
+
class UnpublishedError extends Error {
|
|
25
|
+
constructor(name, reason) {
|
|
26
|
+
super(`${name} is not published (${reason})`);
|
|
27
|
+
this.name = name;
|
|
28
|
+
this.reason = reason;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// The task or service `user` names another one by, which a server left out: the error says
|
|
32
|
+
// which published one still needs it, as an error elsewhere is left as it is
|
|
33
|
+
function findPublished(user, verb, find) {
|
|
34
|
+
try {
|
|
35
|
+
return find();
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof UnpublishedError) {
|
|
39
|
+
throw new Error(`${user} ${verb} ${e.name}, which is not published (${e.reason})`);
|
|
40
|
+
}
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// The build files a server publishes: the entry file and what it includes or references,
|
|
45
|
+
// except through an include with `publish: false`. The key of each is in ctx.files.
|
|
46
|
+
function getPublishedFiles(mainScope) {
|
|
47
|
+
const published = new Set([mainScope]);
|
|
48
|
+
const queue = [mainScope];
|
|
49
|
+
for (let scope = queue.shift(); scope; scope = queue.shift()) {
|
|
50
|
+
for (const reference of Object.values(scope.references)) {
|
|
51
|
+
if (reference.publish && !published.has(reference.scope)) {
|
|
52
|
+
published.add(reference.scope);
|
|
53
|
+
queue.push(reference.scope);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return published;
|
|
58
|
+
}
|
|
59
|
+
exports.getPublishedFiles = getPublishedFiles;
|
|
13
60
|
function combinePath(namePrefix, name) {
|
|
14
61
|
return namePrefix.length > 0 ? `${namePrefix}:${name}` : name;
|
|
15
62
|
}
|
|
16
63
|
async function parseReferences(ctx, mainScope, environment) {
|
|
17
64
|
const declaredCaches = {};
|
|
65
|
+
const unpublished = {};
|
|
18
66
|
const reference = {
|
|
19
67
|
files: {},
|
|
20
68
|
tasks: {},
|
|
@@ -24,9 +72,28 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
24
72
|
caches: {},
|
|
25
73
|
secrets: (0, secret_catalog_1.emptySecretCatalog)(),
|
|
26
74
|
projectRoot: await (0, project_root_1.findProjectRoot)(mainScope.cwd, environment),
|
|
75
|
+
unpublished,
|
|
76
|
+
};
|
|
77
|
+
// a server leaves out what is not published: it doesn't run, and isn't checked against
|
|
78
|
+
// its policy. Locally everything is there.
|
|
79
|
+
const publishedFiles = environment.hosted ? getPublishedFiles(mainScope) : null;
|
|
80
|
+
const isLeftOut = (file, name, schema) => {
|
|
81
|
+
if (!publishedFiles) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (!publishedFiles.has(file)) {
|
|
85
|
+
unpublished[name] = 'its build file is included with publish: false';
|
|
86
|
+
}
|
|
87
|
+
else if (schema.publish === false) {
|
|
88
|
+
unpublished[name] = 'publish: false';
|
|
89
|
+
}
|
|
90
|
+
return name in unpublished;
|
|
27
91
|
};
|
|
28
92
|
for (const file of Object.values(ctx.files)) {
|
|
29
|
-
|
|
93
|
+
// what a server leaves out of a file it doesn't publish: its tasks and services (see
|
|
94
|
+
// isLeftOut), and where it would run or store things, which only a local run reads
|
|
95
|
+
const published = !publishedFiles || publishedFiles.has(file);
|
|
96
|
+
if (published && environment.hosted && (file.schema.secretProviders || file.schema.secretAccounts)) {
|
|
30
97
|
throw new Error(`${file.fileName} declares secret providers or accounts, which a server defines in its own config`);
|
|
31
98
|
}
|
|
32
99
|
if (!reference.envFiles[file.cwd]) {
|
|
@@ -38,6 +105,9 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
38
105
|
for (const [taskName, task] of Object.entries(file.schema.tasks)) {
|
|
39
106
|
const taskEnvs = (0, merge_environment_variables_1.mergeEnvironmentVariables)(task.envs, buildFileEnvs);
|
|
40
107
|
const relativeName = combinePath(file.namePrefix, taskName);
|
|
108
|
+
if (isLeftOut(file, relativeName, task)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
41
111
|
reference.tasks[relativeName] = {
|
|
42
112
|
type: 'task',
|
|
43
113
|
schema: task,
|
|
@@ -51,7 +121,7 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
51
121
|
};
|
|
52
122
|
}
|
|
53
123
|
}
|
|
54
|
-
if (file.schema.environments) {
|
|
124
|
+
if (file.schema.environments && published) {
|
|
55
125
|
for (const [envName, env] of Object.entries(file.schema.environments)) {
|
|
56
126
|
reference.environments[envName] = {
|
|
57
127
|
type: 'environment',
|
|
@@ -61,7 +131,7 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
61
131
|
};
|
|
62
132
|
}
|
|
63
133
|
}
|
|
64
|
-
if (file.schema.caches) {
|
|
134
|
+
if (file.schema.caches && published) {
|
|
65
135
|
for (const [cacheName, cache] of Object.entries(file.schema.caches)) {
|
|
66
136
|
declaredCaches[cacheName] = cache;
|
|
67
137
|
}
|
|
@@ -69,6 +139,9 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
69
139
|
if (file.schema.services) {
|
|
70
140
|
for (const [serviceName, service] of Object.entries(file.schema.services)) {
|
|
71
141
|
const relativeName = combinePath(file.namePrefix, serviceName);
|
|
142
|
+
if (isLeftOut(file, relativeName, service)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
72
145
|
const serviceEnvs = (0, merge_environment_variables_1.mergeEnvironmentVariables)((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(service) ? {} : service.envs, buildFileEnvs);
|
|
73
146
|
reference.services[relativeName] = {
|
|
74
147
|
type: 'service',
|
|
@@ -98,7 +171,7 @@ async function parseReferences(ctx, mainScope, environment) {
|
|
|
98
171
|
reference.caches = (0, resolve_cache_1.withBuiltinCaches)(declaredCaches);
|
|
99
172
|
reference.secrets = environment.hosted
|
|
100
173
|
? { ...environment.hosted.catalog, explicitAccounts: true }
|
|
101
|
-
: (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference);
|
|
174
|
+
: (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference, await readHostSecrets(environment));
|
|
102
175
|
return reference;
|
|
103
176
|
}
|
|
104
177
|
exports.parseReferences = parseReferences;
|
|
@@ -122,7 +195,8 @@ function applySchemaExtension(reference, task) {
|
|
|
122
195
|
task.extension = 'applied';
|
|
123
196
|
return;
|
|
124
197
|
}
|
|
125
|
-
const
|
|
198
|
+
const name = task.schema.extend;
|
|
199
|
+
const extend = findPublished(task.relativeName, 'extends', () => findTask(reference, task.scope, name, null)).task;
|
|
126
200
|
applySchemaExtension(reference, extend);
|
|
127
201
|
task.schema.shell = task.schema.shell ?? extend.schema.shell;
|
|
128
202
|
task.schema.cache = task.schema.cache ?? extend.schema.cache;
|
|
@@ -182,7 +256,7 @@ function extendObject(base, extend) {
|
|
|
182
256
|
}
|
|
183
257
|
function resolveDeps(reference, taskOrService, scope, schema) {
|
|
184
258
|
for (const depName of schema.deps || []) {
|
|
185
|
-
const depTask = findTask(reference, scope, depName, null);
|
|
259
|
+
const depTask = findPublished(taskOrService.relativeName, 'depends on', () => findTask(reference, scope, depName, null));
|
|
186
260
|
taskOrService.deps.push({
|
|
187
261
|
task: depTask.task,
|
|
188
262
|
type: depTask.type,
|
|
@@ -195,7 +269,8 @@ function resolveInit(reference, service) {
|
|
|
195
269
|
if ((0, build_file_service_schema_1.isBuildFileKubernetesServiceSchema)(service.schema) || !service.schema.init) {
|
|
196
270
|
return;
|
|
197
271
|
}
|
|
198
|
-
const
|
|
272
|
+
const init = service.schema.init;
|
|
273
|
+
const initTask = findPublished(service.relativeName, 'runs as init', () => findTask(reference, service.scope, init, null));
|
|
199
274
|
service.init = {
|
|
200
275
|
task: initTask.task,
|
|
201
276
|
type: initTask.type,
|
|
@@ -213,13 +288,13 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
213
288
|
// the host name it is reached by: a service of an include, `services:postgres`, as
|
|
214
289
|
// `services-postgres`, since Docker and DNS take no colon
|
|
215
290
|
relativeName: needName.replace(/:/g, '-'),
|
|
216
|
-
service: findService(reference, scope, needName),
|
|
291
|
+
service: findPublished(taskOrService.relativeName, 'needs', () => findService(reference, scope, needName)),
|
|
217
292
|
});
|
|
218
293
|
}
|
|
219
294
|
else {
|
|
220
295
|
taskOrService.needs.push({
|
|
221
296
|
relativeName: needName.name,
|
|
222
|
-
service: findService(reference, scope, needName.service),
|
|
297
|
+
service: findPublished(taskOrService.relativeName, 'needs', () => findService(reference, scope, needName.service)),
|
|
223
298
|
});
|
|
224
299
|
}
|
|
225
300
|
}
|
|
@@ -230,6 +305,7 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
|
|
|
230
305
|
}
|
|
231
306
|
}
|
|
232
307
|
function findTask(ctx, scope, name, type) {
|
|
308
|
+
refuseNotFetched(scope, name);
|
|
233
309
|
const ref = (0, split_name_1.splitName)(name);
|
|
234
310
|
if (ref.prefix) {
|
|
235
311
|
if (scope.references[ref.prefix]) {
|
|
@@ -245,15 +321,25 @@ function findTask(ctx, scope, name, type) {
|
|
|
245
321
|
throw new Error('unable to find ' + name); // TODO improve
|
|
246
322
|
}
|
|
247
323
|
exports.findTask = findTask;
|
|
324
|
+
// A name in a git source a server did not fetch, since it is left out
|
|
325
|
+
function refuseNotFetched(scope, name) {
|
|
326
|
+
if (scope.notFetched) {
|
|
327
|
+
throw new UnpublishedError(combinePath(scope.namePrefix, name), 'its build file is included with publish: false');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
248
330
|
function resolveTask(ctx, scope, name, type) {
|
|
249
331
|
const fullName = combinePath(scope.namePrefix, name);
|
|
250
332
|
const buildFileTask = ctx.tasks[fullName];
|
|
333
|
+
if (!buildFileTask && ctx.unpublished?.[fullName]) {
|
|
334
|
+
throw new UnpublishedError(fullName, ctx.unpublished[fullName]);
|
|
335
|
+
}
|
|
251
336
|
if (!buildFileTask) {
|
|
252
337
|
throw new Error('unable to find ' + fullName);
|
|
253
338
|
}
|
|
254
339
|
return { task: buildFileTask, type };
|
|
255
340
|
}
|
|
256
341
|
function findService(ctx, scope, name) {
|
|
342
|
+
refuseNotFetched(scope, name);
|
|
257
343
|
const ref = (0, split_name_1.splitName)(name);
|
|
258
344
|
if (ref.prefix) {
|
|
259
345
|
if (scope.references[ref.prefix]) {
|
|
@@ -272,6 +358,9 @@ exports.findService = findService;
|
|
|
272
358
|
function resolveService(ctx, scope, name) {
|
|
273
359
|
const fullName = combinePath(scope.namePrefix, name);
|
|
274
360
|
const service = ctx.services[fullName];
|
|
361
|
+
if (!service && ctx.unpublished?.[fullName]) {
|
|
362
|
+
throw new UnpublishedError(fullName, ctx.unpublished[fullName]);
|
|
363
|
+
}
|
|
275
364
|
if (!service) {
|
|
276
365
|
throw new Error('unable to find ' + fullName);
|
|
277
366
|
}
|
|
@@ -13,60 +13,79 @@ async function createParseContext(fileName, environment, options) {
|
|
|
13
13
|
files: {},
|
|
14
14
|
refreshed: options?.refresh ? new Map() : undefined,
|
|
15
15
|
directory: options?.directory,
|
|
16
|
+
checkGitSource: options?.checkGitSource,
|
|
16
17
|
};
|
|
17
18
|
const scope = await appendBuildFile(workDirectory(ctx, (0, path_1.dirname)(fileName)), ctx, environment, fileName, []);
|
|
18
19
|
return { ctx, scope };
|
|
19
20
|
}
|
|
20
21
|
exports.createParseContext = createParseContext;
|
|
21
|
-
async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remote, inputs
|
|
22
|
+
async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remote, inputs,
|
|
23
|
+
// on a server: the file is included with `publish: false`, or below such an include
|
|
24
|
+
leftOut = false) {
|
|
22
25
|
const relativeName = namePrefix.join(':');
|
|
23
|
-
// the same file included with other inputs is another set of tasks
|
|
24
|
-
|
|
26
|
+
// the same file included with other inputs is another set of tasks, and on a server a
|
|
27
|
+
// file left out is another one than the same file published (its git sources differ)
|
|
28
|
+
const key = `${cwd}:${fileName}${inputs ? `:${JSON.stringify(inputs)}` : ''}${leftOut ? ':left-out' : ''}`;
|
|
25
29
|
if (ctx.files[key]) {
|
|
26
30
|
return ctx.files[key];
|
|
27
31
|
}
|
|
28
32
|
const input = await (0, read_build_file_1.read)(fileName, environment);
|
|
29
33
|
const result = await build_file_schema_1.buildFileSchema.safeParseAsync(input);
|
|
30
|
-
if (result.success) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (scope.references[referenceName]) {
|
|
48
|
-
throw new Error(referenceName + ' already exists');
|
|
49
|
-
}
|
|
50
|
-
scope.references[referenceName] = { type: 'reference', scope: referenceScope };
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
throw new parse_error_1.ParseError(result.error, fileName);
|
|
36
|
+
}
|
|
37
|
+
const scope = {
|
|
38
|
+
fileName,
|
|
39
|
+
cwd,
|
|
40
|
+
schema: inputs ? applyInputs(result.data, inputs, namePrefix, fileName) : result.data,
|
|
41
|
+
namePrefix: relativeName,
|
|
42
|
+
remote,
|
|
43
|
+
inputs,
|
|
44
|
+
references: {},
|
|
45
|
+
};
|
|
46
|
+
ctx.files[key] = scope;
|
|
47
|
+
for (const block of ['references', 'includes']) {
|
|
48
|
+
for (const [name, include] of Object.entries(scope.schema[block] ?? {})) {
|
|
49
|
+
if (scope.references[name]) {
|
|
50
|
+
throw new Error(name + ' already exists');
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
const publish = (0, build_file_include_schema_1.isPublished)(include);
|
|
53
|
+
const type = block === 'references' ? 'reference' : 'include';
|
|
54
|
+
// a server does not fetch what it leaves out: it doesn't run, and isn't checked
|
|
55
|
+
const leaveOut = !!environment.hosted && (leftOut || !publish);
|
|
56
|
+
if ((0, build_file_include_schema_1.isGitSource)(include) && leaveOut) {
|
|
57
|
+
scope.references[name] = { type, publish, scope: notFetched(cwd, [...namePrefix, name], include) };
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if ((0, build_file_include_schema_1.isGitSource)(include)) {
|
|
61
|
+
ctx.checkGitSource?.(include, scope, block, name);
|
|
61
62
|
}
|
|
63
|
+
const target = await locateInclude(ctx, scope, include, block === 'references'
|
|
64
|
+
? // a remote file's paths resolve within its repository
|
|
65
|
+
remote
|
|
66
|
+
? (0, path_1.dirname)(scope.fileName)
|
|
67
|
+
: readDirectory(ctx, scope.cwd)
|
|
68
|
+
: (0, path_1.dirname)(scope.fileName), environment);
|
|
69
|
+
const targetScope = await appendBuildFile(
|
|
70
|
+
// a reference works in its own directory, an include in the one of the including file
|
|
71
|
+
block === 'references' ? workDirectory(ctx, (0, path_1.dirname)(target.fileName)) : cwd, ctx, environment, target.fileName, [...namePrefix, name], target.remote, target.inputs, leaveOut);
|
|
72
|
+
scope.references[name] = { type, scope: targetScope, publish };
|
|
62
73
|
}
|
|
63
|
-
return scope;
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
throw new parse_error_1.ParseError(result.error, fileName);
|
|
67
74
|
}
|
|
75
|
+
return scope;
|
|
68
76
|
}
|
|
69
77
|
exports.appendBuildFile = appendBuildFile;
|
|
78
|
+
// The scope of a git source a server does not fetch, see ParseScope.notFetched
|
|
79
|
+
function notFetched(cwd, namePrefix, include) {
|
|
80
|
+
return {
|
|
81
|
+
fileName: `${include.git}${include.path ? ` ${include.path}` : ''}`,
|
|
82
|
+
cwd,
|
|
83
|
+
schema: {},
|
|
84
|
+
namePrefix: namePrefix.join(':'),
|
|
85
|
+
references: {},
|
|
86
|
+
notFetched: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
70
89
|
// The directory a build file works in, for the directory it was read from: the same
|
|
71
90
|
// one, unless the files were read from elsewhere (see BuildFileDirectory).
|
|
72
91
|
function workDirectory(ctx, directory) {
|
|
@@ -150,9 +150,11 @@ function candidates(command, path) {
|
|
|
150
150
|
.map((dir) => (0, path_1.resolve)(dir, command));
|
|
151
151
|
return bases.flatMap((base) => extensions.map((extension) => base + extension));
|
|
152
152
|
}
|
|
153
|
+
// bigint: a Windows file id has more digits than a number keeps, and two files whose ids
|
|
154
|
+
// round to the same number would be the same file
|
|
153
155
|
function statOf(path) {
|
|
154
156
|
try {
|
|
155
|
-
const stat = (0, fs_1.statSync)(path);
|
|
157
|
+
const stat = (0, fs_1.statSync)(path, { bigint: true });
|
|
156
158
|
return stat.isFile() ? stat : null;
|
|
157
159
|
}
|
|
158
160
|
catch {
|
|
@@ -11,10 +11,12 @@ function emptySecretCatalog() {
|
|
|
11
11
|
exports.emptySecretCatalog = emptySecretCatalog;
|
|
12
12
|
const FULL_COMMIT = /^[0-9a-f]{40}$/i;
|
|
13
13
|
// Providers and accounts are global names, like caches, collected from every
|
|
14
|
-
// loaded build file. A name declared twice is an error rather than a silent
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// loaded build file. A name declared twice is an error rather than a silent override:
|
|
15
|
+
// which definition runs with a service account's credential must not depend on include
|
|
16
|
+
// order. The host config then replaces what the build files declare, name by name (and
|
|
17
|
+
// its default account replaces theirs): the machine that runs a task says how it reads
|
|
18
|
+
// secrets, and a build file that still declares them loads as it is.
|
|
19
|
+
function collectSecretCatalog(files, environment, context, host) {
|
|
18
20
|
const catalog = emptySecretCatalog();
|
|
19
21
|
for (const file of files) {
|
|
20
22
|
const providers = Object.entries(file.schema.secretProviders ?? {});
|
|
@@ -28,25 +30,13 @@ function collectSecretCatalog(files, environment, context) {
|
|
|
28
30
|
if (catalog.providers[name]) {
|
|
29
31
|
throw new Error(`secret provider ${name} is declared in ${catalog.providers[name].declaredIn} and in ${file.fileName}`);
|
|
30
32
|
}
|
|
31
|
-
catalog.providers[name] =
|
|
32
|
-
name,
|
|
33
|
-
type: 'command',
|
|
34
|
-
command: provider.command.map((part) => applyEnvs(part, envs)),
|
|
35
|
-
env: provider.env ?? {},
|
|
36
|
-
timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? DEFAULT_TIMEOUT),
|
|
37
|
-
declaredIn: file.fileName,
|
|
38
|
-
};
|
|
33
|
+
catalog.providers[name] = toProvider(name, provider, envs, file.fileName);
|
|
39
34
|
}
|
|
40
35
|
for (const [name, account] of accounts) {
|
|
41
36
|
if (catalog.accounts[name]) {
|
|
42
37
|
throw new Error(`secret account ${name} is declared in ${catalog.accounts[name].declaredIn} and in ${file.fileName}`);
|
|
43
38
|
}
|
|
44
|
-
catalog.accounts[name] =
|
|
45
|
-
name,
|
|
46
|
-
default: account.default ?? false,
|
|
47
|
-
providers: account.providers,
|
|
48
|
-
declaredIn: file.fileName,
|
|
49
|
-
};
|
|
39
|
+
catalog.accounts[name] = toAccount(name, account, file.fileName);
|
|
50
40
|
}
|
|
51
41
|
}
|
|
52
42
|
const defaults = Object.values(catalog.accounts).filter((account) => account.default);
|
|
@@ -55,14 +45,48 @@ function collectSecretCatalog(files, environment, context) {
|
|
|
55
45
|
}
|
|
56
46
|
for (const account of Object.values(catalog.accounts)) {
|
|
57
47
|
for (const provider of Object.keys(account.providers)) {
|
|
58
|
-
if (!catalog.providers[provider]) {
|
|
48
|
+
if (!catalog.providers[provider] && !host?.secretProviders?.[provider]) {
|
|
59
49
|
throw new Error(`secret account ${account.name} (${account.declaredIn}) names provider ${provider}, which is not declared`);
|
|
60
50
|
}
|
|
61
51
|
}
|
|
62
52
|
}
|
|
53
|
+
if (host) {
|
|
54
|
+
// the host config has no envs of its own: `${NAME}` is read from the process
|
|
55
|
+
for (const [name, provider] of Object.entries(host.secretProviders ?? {})) {
|
|
56
|
+
catalog.providers[name] = toProvider(name, provider, {}, host.fileName);
|
|
57
|
+
}
|
|
58
|
+
const accounts = Object.entries(host.secretAccounts ?? {});
|
|
59
|
+
const hostDefaults = accounts.filter(([, account]) => account.default);
|
|
60
|
+
if (hostDefaults.length > 1) {
|
|
61
|
+
throw new Error(`more than one default secret account: ${hostDefaults.map(([name]) => name).join(', ')} (${host.fileName})`);
|
|
62
|
+
}
|
|
63
|
+
if (hostDefaults.length > 0) {
|
|
64
|
+
for (const account of Object.values(catalog.accounts)) {
|
|
65
|
+
account.default = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// a provider an account of the host names is only checked when a secret uses it:
|
|
69
|
+
// a project that doesn't declare it, and reads no secret, loads anyway
|
|
70
|
+
for (const [name, account] of accounts) {
|
|
71
|
+
catalog.accounts[name] = toAccount(name, account, host.fileName);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
return catalog;
|
|
64
75
|
}
|
|
65
76
|
exports.collectSecretCatalog = collectSecretCatalog;
|
|
77
|
+
function toProvider(name, provider, envs, declaredIn) {
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
type: 'command',
|
|
81
|
+
command: provider.command.map((part) => applyEnvs(part, envs)),
|
|
82
|
+
env: provider.env ?? {},
|
|
83
|
+
timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? DEFAULT_TIMEOUT),
|
|
84
|
+
declaredIn,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function toAccount(name, account, declaredIn) {
|
|
88
|
+
return { name, default: account.default ?? false, providers: account.providers, declaredIn };
|
|
89
|
+
}
|
|
66
90
|
// A tag or branch can be moved, and a moved file runs with the credentials of a
|
|
67
91
|
// service account: providers and accounts from a git include are pinned.
|
|
68
92
|
function requirePinnedSource(file) {
|
|
@@ -23,10 +23,22 @@ const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
|
|
|
23
23
|
* GET /v1/definitions/<hash> one version, with `arguments`: per task null when
|
|
24
24
|
* a run of it may pass arguments, else why not
|
|
25
25
|
* GET /v1/definitions/<hash>/files its files
|
|
26
|
+
* POST /v1/definitions/plan what publishing a bundle would do: plans it with this
|
|
27
|
+
* server's policy and stores nothing. For anybody with
|
|
28
|
+
* a `builds` level, `run` too
|
|
26
29
|
* POST /v1/definitions/<hash>/yank pull it back (body: optional {reason})
|
|
27
30
|
* POST /v1/definitions/<hash>/unyank let it go on
|
|
28
31
|
*/
|
|
29
32
|
async function handleDefinitions(config, store, proposals, log, principal, req, res, url) {
|
|
33
|
+
if (url.pathname === '/v1/definitions/plan') {
|
|
34
|
+
if (req.method !== 'POST') {
|
|
35
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await plan(config, store, log, principal, req, res);
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
30
42
|
const match = /^\/v1\/definitions(?:\/([^/]+)(\/files|\/yank|\/unyank)?)?$/.exec(url.pathname);
|
|
31
43
|
if (!match) {
|
|
32
44
|
return false;
|
|
@@ -124,6 +136,55 @@ async function yank(config, store, log, principal, hash, undo, req, res) {
|
|
|
124
136
|
log(`${undo ? 'unyank' : 'yank'} ${hash} iss=${principal.iss} sub=${principal.sub}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
|
|
125
137
|
(0, http_1.send)(res, 200, changed);
|
|
126
138
|
}
|
|
139
|
+
// Answers what POST /v1/definitions would: the version with its plan, and whether the
|
|
140
|
+
// caller would publish or propose it. A developer checks a build file against the policy
|
|
141
|
+
// before the account that publishes does. Nothing is stored, nothing is logged but the call.
|
|
142
|
+
async function plan(config, store, log, principal, req, res) {
|
|
143
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
144
|
+
if (grant.builds === null) {
|
|
145
|
+
(0, http_1.send)(res, 403, { error: 'you may not check build files, it needs builds: run, propose or publish' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const parsed = bundle_1.bundleSchema.safeParse(await (0, http_1.readJson)(req, MAX_PUBLISH_BYTES));
|
|
149
|
+
if (!parsed.success) {
|
|
150
|
+
const issue = parsed.error.issues[0];
|
|
151
|
+
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
// like a proposal: a caller who may not publish does not make the server fetch
|
|
156
|
+
const { version, existing } = await store.prepare(parsed.data, principal, {
|
|
157
|
+
noRemote: (0, policy_1.mayPublish)(grant) ? undefined : 'check',
|
|
158
|
+
});
|
|
159
|
+
const missing = (version.accounts ?? []).filter((account) => !(0, policy_1.canUse)(grant, account));
|
|
160
|
+
const outcome = existing
|
|
161
|
+
? 'already published'
|
|
162
|
+
: (0, policy_1.mayPublish)(grant) && missing.length === 0
|
|
163
|
+
? 'published'
|
|
164
|
+
: (0, policy_1.mayPublish)(grant) || (0, policy_1.mayPropose)(grant)
|
|
165
|
+
? 'proposed'
|
|
166
|
+
: null;
|
|
167
|
+
log(`plan ${version.hash} iss=${principal.iss} sub=${principal.sub} tasks=${version.tasks.length}`);
|
|
168
|
+
(0, http_1.send)(res, 200, {
|
|
169
|
+
hash: version.hash,
|
|
170
|
+
tasks: version.tasks,
|
|
171
|
+
services: version.services,
|
|
172
|
+
plan: version.plan,
|
|
173
|
+
accounts: version.accounts,
|
|
174
|
+
outcome,
|
|
175
|
+
// a yanked version is not checked again: it is the one that was published
|
|
176
|
+
...(existing?.yanked ? { yanked: true } : {}),
|
|
177
|
+
...(missing.length > 0 ? { missing } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (e instanceof definitions_1.DefinitionError) {
|
|
182
|
+
(0, http_1.send)(res, 400, { error: `the build file does not load: ${e.message}` });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
127
188
|
async function publish(config, store, proposals, log, principal, req, res) {
|
|
128
189
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
129
190
|
if (!(0, policy_1.mayPublish)(grant) && !(0, policy_1.mayPropose)(grant)) {
|
|
@@ -151,7 +212,7 @@ async function publish(config, store, proposals, log, principal, req, res) {
|
|
|
151
212
|
// a version can't run before it is approved, but it has to load, as it would
|
|
152
213
|
// have when published, so the approver is not asked about a broken one
|
|
153
214
|
const { version, existing } = await store.prepare(parsed.data, principal, {
|
|
154
|
-
noRemote:
|
|
215
|
+
noRemote: (0, policy_1.mayPublish)(grant) ? undefined : 'proposal',
|
|
155
216
|
annotations,
|
|
156
217
|
});
|
|
157
218
|
if (existing) {
|
package/dist/server/executor.js
CHANGED
|
@@ -238,6 +238,7 @@ class HostExecutor {
|
|
|
238
238
|
}
|
|
239
239
|
: {}),
|
|
240
240
|
...(placed ? { kubernetes: placed.kubernetes } : {}),
|
|
241
|
+
...(!placed && this.options.capabilityRuntime ? { capabilityRuntime: this.options.capabilityRuntime } : {}),
|
|
241
242
|
}, cwd, { ...this.processEnvironment(home, cache), ...placed?.env }, context, abortCtrl.signal);
|
|
242
243
|
}
|
|
243
244
|
finally {
|