hammerkit 1.22.0 → 1.24.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/build.schema.json +92 -2
- package/dist/cli.js +25 -0
- package/dist/executer/environment-mock.js +4 -1
- package/dist/executer/execute-command.js +7 -1
- package/dist/index.js +4 -1
- package/dist/planner/service-definition.js +3 -1
- package/dist/planner/utils/append-work-service.js +1 -1
- package/dist/planner/utils/append-work-task.js +2 -1
- package/dist/planner/work-item-status.js +3 -2
- package/dist/planner/work-secret.js +87 -10
- package/dist/program.js +4 -0
- package/dist/remote/bundle.js +59 -0
- package/dist/remote/collect-bundle.js +41 -0
- package/dist/remote/credentials.js +93 -0
- package/dist/remote/device-login.js +177 -0
- package/dist/remote/host-config.js +170 -0
- package/dist/remote/location.js +46 -0
- package/dist/remote/remote-commands.js +154 -0
- package/dist/remote/remote-work-commands.js +152 -0
- package/dist/remote/server-client.js +88 -0
- package/dist/remote/source-manifest.js +79 -0
- package/dist/remote/text-diff.js +89 -0
- package/dist/schema/build-file-base-task-schema.js +6 -0
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/build-file-schema.js +6 -0
- package/dist/schema/build-file-secret-provider-schema.js +36 -0
- package/dist/schema/build-file-secret-schema.js +6 -3
- package/dist/schema/reference-parser.js +4 -0
- package/dist/schema/runner-schema.js +13 -0
- package/dist/secrets/command-secret-provider.js +108 -0
- package/dist/secrets/prefetch-secrets.js +27 -0
- package/dist/secrets/provider-values.js +47 -0
- package/dist/secrets/secret-catalog.js +78 -0
- package/dist/secrets/secret-provider.js +19 -0
- package/dist/server/approvals-api.js +99 -0
- package/dist/server/definitions-api.js +119 -0
- package/dist/server/definitions.js +248 -0
- package/dist/server/discovery.js +9 -0
- package/dist/server/executor.js +238 -0
- package/dist/server/git-checkout.js +63 -0
- package/dist/server/http.js +39 -0
- package/dist/server/oauth.js +226 -0
- package/dist/server/policy.js +90 -0
- package/dist/server/proposals-api.js +123 -0
- package/dist/server/proposals.js +126 -0
- package/dist/server/run-request.js +67 -0
- package/dist/server/runs.js +279 -0
- package/dist/server/server-command.js +43 -0
- package/dist/server/server-config.js +233 -0
- package/dist/server/server.js +338 -0
- package/dist/server/sources-api.js +106 -0
- package/dist/server/sources.js +258 -0
- package/dist/utils/redact.js +75 -19
- package/package.json +1 -1
package/build.schema.json
CHANGED
|
@@ -204,7 +204,10 @@
|
|
|
204
204
|
"properties": {
|
|
205
205
|
"from": {
|
|
206
206
|
"type": "string",
|
|
207
|
-
"pattern": "^
|
|
207
|
+
"pattern": "^[A-Za-z][A-Za-z0-9_-]*:.+$"
|
|
208
|
+
},
|
|
209
|
+
"account": {
|
|
210
|
+
"type": "string"
|
|
208
211
|
},
|
|
209
212
|
"env": {
|
|
210
213
|
"type": "string",
|
|
@@ -221,9 +224,12 @@
|
|
|
221
224
|
"from"
|
|
222
225
|
],
|
|
223
226
|
"additionalProperties": false,
|
|
224
|
-
"description": "secret from env:NAME
|
|
227
|
+
"description": "secret from env:NAME, file:path or <provider>:<ref>, injected as env or a read-only file"
|
|
225
228
|
}
|
|
226
229
|
},
|
|
230
|
+
"account": {
|
|
231
|
+
"type": "string"
|
|
232
|
+
},
|
|
227
233
|
"resources": {
|
|
228
234
|
"type": "object",
|
|
229
235
|
"properties": {
|
|
@@ -255,6 +261,10 @@
|
|
|
255
261
|
"additionalProperties": false,
|
|
256
262
|
"description": "cpu and memory requests and limits"
|
|
257
263
|
},
|
|
264
|
+
"runner": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"minLength": 1
|
|
267
|
+
},
|
|
258
268
|
"image": {
|
|
259
269
|
"type": "string"
|
|
260
270
|
},
|
|
@@ -319,9 +329,15 @@
|
|
|
319
329
|
"secrets": {
|
|
320
330
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/secrets"
|
|
321
331
|
},
|
|
332
|
+
"account": {
|
|
333
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/account"
|
|
334
|
+
},
|
|
322
335
|
"resources": {
|
|
323
336
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/resources"
|
|
324
337
|
},
|
|
338
|
+
"runner": {
|
|
339
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/runner"
|
|
340
|
+
},
|
|
325
341
|
"platform": {
|
|
326
342
|
"type": "object",
|
|
327
343
|
"properties": {
|
|
@@ -446,6 +462,9 @@
|
|
|
446
462
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/secrets/items"
|
|
447
463
|
}
|
|
448
464
|
},
|
|
465
|
+
"account": {
|
|
466
|
+
"type": "string"
|
|
467
|
+
},
|
|
449
468
|
"resources": {
|
|
450
469
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/resources",
|
|
451
470
|
"description": "cpu and memory requests and limits"
|
|
@@ -748,8 +767,79 @@
|
|
|
748
767
|
"additionalProperties": false
|
|
749
768
|
}
|
|
750
769
|
},
|
|
770
|
+
"secretProviders": {
|
|
771
|
+
"type": "object",
|
|
772
|
+
"additionalProperties": {
|
|
773
|
+
"type": "object",
|
|
774
|
+
"properties": {
|
|
775
|
+
"command": {
|
|
776
|
+
"type": "array",
|
|
777
|
+
"items": {
|
|
778
|
+
"type": "string"
|
|
779
|
+
},
|
|
780
|
+
"minItems": 1,
|
|
781
|
+
"description": "command and arguments, no shell; {{ref}} is replaced by the secret reference"
|
|
782
|
+
},
|
|
783
|
+
"env": {
|
|
784
|
+
"type": "object",
|
|
785
|
+
"additionalProperties": {
|
|
786
|
+
"type": "string"
|
|
787
|
+
},
|
|
788
|
+
"description": "static environment of the command, next to PATH and the account env"
|
|
789
|
+
},
|
|
790
|
+
"timeout": {
|
|
791
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/timeout",
|
|
792
|
+
"description": "how long the command may run, 30s when omitted"
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
"required": [
|
|
796
|
+
"command"
|
|
797
|
+
],
|
|
798
|
+
"additionalProperties": false,
|
|
799
|
+
"description": "secret provider, a command that prints the secret to stdout"
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
"secretAccounts": {
|
|
803
|
+
"type": "object",
|
|
804
|
+
"additionalProperties": {
|
|
805
|
+
"type": "object",
|
|
806
|
+
"properties": {
|
|
807
|
+
"default": {
|
|
808
|
+
"type": "boolean",
|
|
809
|
+
"description": "the account for secrets and tasks that name none"
|
|
810
|
+
},
|
|
811
|
+
"providers": {
|
|
812
|
+
"type": "object",
|
|
813
|
+
"additionalProperties": {
|
|
814
|
+
"type": "object",
|
|
815
|
+
"properties": {
|
|
816
|
+
"env": {
|
|
817
|
+
"type": "object",
|
|
818
|
+
"additionalProperties": {
|
|
819
|
+
"type": "string"
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
"required": [
|
|
824
|
+
"env"
|
|
825
|
+
],
|
|
826
|
+
"additionalProperties": false
|
|
827
|
+
},
|
|
828
|
+
"description": "per provider, the environment that makes its command act as this account"
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
"required": [
|
|
832
|
+
"providers"
|
|
833
|
+
],
|
|
834
|
+
"additionalProperties": false,
|
|
835
|
+
"description": "service account the secret providers are read as"
|
|
836
|
+
}
|
|
837
|
+
},
|
|
751
838
|
"labels": {
|
|
752
839
|
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/labels"
|
|
840
|
+
},
|
|
841
|
+
"runner": {
|
|
842
|
+
"$ref": "#/definitions/hammerkit/properties/tasks/additionalProperties/anyOf/0/properties/runner"
|
|
753
843
|
}
|
|
754
844
|
},
|
|
755
845
|
"additionalProperties": false,
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ const check_for_loop_1 = require("./executer/scheduler/check-for-loop");
|
|
|
13
13
|
const state_1 = require("./executer/state");
|
|
14
14
|
const get_scheduler_execute_result_1 = require("./executer/get-scheduler-execute-result");
|
|
15
15
|
const reset_work_tree_1 = require("./executer/reset-work-tree");
|
|
16
|
+
const location_1 = require("./remote/location");
|
|
16
17
|
const execute_work_tree_1 = require("./executer/execute-work-tree");
|
|
17
18
|
const package_1 = require("./docker/package");
|
|
18
19
|
const explain_1 = require("./cache/explain");
|
|
@@ -21,6 +22,7 @@ const work_graph_serializer_1 = require("./planner/work-graph-serializer");
|
|
|
21
22
|
const dry_run_1 = require("./executer/dry-run");
|
|
22
23
|
const cache_sync_1 = require("./cache/cache-sync");
|
|
23
24
|
const cache_inventory_1 = require("./cache/cache-inventory");
|
|
25
|
+
const prefetch_secrets_1 = require("./secrets/prefetch-secrets");
|
|
24
26
|
const isCliTask = (val) => val.type === 'task';
|
|
25
27
|
exports.isCliTask = isCliTask;
|
|
26
28
|
const isCliService = (val) => val.type === 'service';
|
|
@@ -30,6 +32,11 @@ class Cli {
|
|
|
30
32
|
this.workTree = workTree;
|
|
31
33
|
this.environment = environment;
|
|
32
34
|
}
|
|
35
|
+
// Starts the run's secrets: the values that make up a task id are fetched
|
|
36
|
+
// before anything asks for an id, see prefetchSecrets.
|
|
37
|
+
async prepareSecrets(newRun = false) {
|
|
38
|
+
await (0, prefetch_secrets_1.prefetchSecrets)(this.workTree, this.environment, newRun);
|
|
39
|
+
}
|
|
33
40
|
setup(type, options) {
|
|
34
41
|
const logMode = options?.logMode ?? (ci_1.isCI ? 'live' : 'interactive');
|
|
35
42
|
const workTree = (0, reset_work_tree_1.resetWorkTree)(this.workTree, type);
|
|
@@ -43,7 +50,15 @@ class Cli {
|
|
|
43
50
|
return {
|
|
44
51
|
state: processWorkTree,
|
|
45
52
|
start: async () => {
|
|
53
|
+
if (type !== 'down') {
|
|
54
|
+
await this.prepareSecrets(true);
|
|
55
|
+
}
|
|
46
56
|
(0, check_for_loop_1.checkForLoop)(workTree);
|
|
57
|
+
const location = await (0, location_1.resolveRunLocation)(workTree, this.environment);
|
|
58
|
+
if (location.type === 'server') {
|
|
59
|
+
throw new Error(`this run is meant for server ${location.name}, but running on a server is not available yet. ` +
|
|
60
|
+
'Remove the runner, or run locally with: hammerkit remote use local');
|
|
61
|
+
}
|
|
47
62
|
const capacity = (0, plan_work_tasks_1.hasError)(workTree) ? null : await (0, resource_budget_1.getResourceCapacity)(workTree, this.environment);
|
|
48
63
|
if (capacity && type !== 'down' && (options?.resourceCheck ?? true)) {
|
|
49
64
|
rejectOversizedWork(workTree, capacity, type === 'up');
|
|
@@ -100,15 +115,19 @@ class Cli {
|
|
|
100
115
|
return await run.start();
|
|
101
116
|
}
|
|
102
117
|
async clean(options) {
|
|
118
|
+
await this.prepareSecrets();
|
|
103
119
|
await (0, event_cache_1.cleanCache)(this.workTree, this.environment, options);
|
|
104
120
|
}
|
|
105
121
|
async restore(path) {
|
|
122
|
+
await this.prepareSecrets();
|
|
106
123
|
await (0, event_cache_1.restoreCache)(this.environment, path, this.workTree);
|
|
107
124
|
}
|
|
108
125
|
async package(options) {
|
|
126
|
+
await this.prepareSecrets();
|
|
109
127
|
await (0, package_1.packageWorkTree)(this.workTree, this.environment, options);
|
|
110
128
|
}
|
|
111
129
|
async store(path) {
|
|
130
|
+
await this.prepareSecrets();
|
|
112
131
|
await (0, event_cache_1.storeCache)(this.environment, path, this.workTree);
|
|
113
132
|
}
|
|
114
133
|
services() {
|
|
@@ -132,6 +151,7 @@ class Cli {
|
|
|
132
151
|
return [...(0, plan_work_tasks_1.iterateWorkTasks)(this.workTree)].map((task) => (0, describe_task_1.describeTask)(task, this.environment.cwd));
|
|
133
152
|
}
|
|
134
153
|
async explain(options) {
|
|
154
|
+
await this.prepareSecrets();
|
|
135
155
|
return (0, explain_1.explainWorkTree)(this.workTree, options?.cacheDefault ?? 'checksum', this.environment);
|
|
136
156
|
}
|
|
137
157
|
// Serialize the in-scope work graph for rendering (mermaid/dot). Pure read of
|
|
@@ -142,25 +162,30 @@ class Cli {
|
|
|
142
162
|
// Ordered execution plan with predicted cache decisions, reusing the explain
|
|
143
163
|
// engine — executes nothing, starts nothing, performs no cache push/pull.
|
|
144
164
|
async dryRun(options) {
|
|
165
|
+
await this.prepareSecrets();
|
|
145
166
|
return (0, dry_run_1.planDryRun)(this.workTree, options?.cacheDefault ?? 'checksum', this.environment);
|
|
146
167
|
}
|
|
147
168
|
// Move cache entries between the tasks' own caches and a named remote without
|
|
148
169
|
// executing anything (`cache pull` / `cache push`).
|
|
149
170
|
async syncCache(options) {
|
|
171
|
+
await this.prepareSecrets();
|
|
150
172
|
return (0, cache_sync_1.syncCache)(this.workTree, options, this.environment);
|
|
151
173
|
}
|
|
152
174
|
// Entries of a named cache (default: the local `default` cache).
|
|
153
175
|
async listCache(cacheName) {
|
|
176
|
+
await this.prepareSecrets();
|
|
154
177
|
return (0, cache_inventory_1.listCache)(this.workTree, cacheName, this.environment);
|
|
155
178
|
}
|
|
156
179
|
retentionPolicy(cacheName, overrides) {
|
|
157
180
|
return (0, cache_inventory_1.retentionPolicyOf)(this.workTree, cacheName, overrides);
|
|
158
181
|
}
|
|
159
182
|
async pruneCache(cacheName, policy, dryRun = false) {
|
|
183
|
+
await this.prepareSecrets();
|
|
160
184
|
return (0, cache_inventory_1.pruneCache)(this.workTree, cacheName, policy, { dryRun }, this.environment);
|
|
161
185
|
}
|
|
162
186
|
// Apply declared retention to the local caches this build file uses.
|
|
163
187
|
async autoPrune() {
|
|
188
|
+
await this.prepareSecrets();
|
|
164
189
|
return (0, cache_inventory_1.autoPrune)(this.workTree, this.environment);
|
|
165
190
|
}
|
|
166
191
|
task(name) {
|
|
@@ -5,14 +5,17 @@ const work_item_status_1 = require("../planner/work-item-status");
|
|
|
5
5
|
const get_file_context_1 = require("../file/get-file-context");
|
|
6
6
|
const log_1 = require("../log");
|
|
7
7
|
const empty_writable_1 = require("../utils/empty-writable");
|
|
8
|
+
const redact_1 = require("../utils/redact");
|
|
8
9
|
function environmentMock(cwd) {
|
|
10
|
+
const secrets = (0, redact_1.createSecretRegistry)();
|
|
9
11
|
return {
|
|
10
12
|
cwd,
|
|
11
13
|
file: (0, get_file_context_1.getFileContext)(cwd),
|
|
12
14
|
console: (0, log_1.consoleContext)((0, empty_writable_1.emptyWritable)()),
|
|
13
15
|
abortCtrl: new AbortController(),
|
|
14
16
|
processEnvs: {},
|
|
15
|
-
status: (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)()),
|
|
17
|
+
status: (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets),
|
|
18
|
+
secrets,
|
|
16
19
|
stdout: (0, empty_writable_1.emptyWritable)(),
|
|
17
20
|
stderr: (0, empty_writable_1.emptyWritable)(),
|
|
18
21
|
stdoutColumns: 80,
|
|
@@ -12,11 +12,17 @@ const environment_config_1 = require("../utils/environment-config");
|
|
|
12
12
|
function getStopTimeout() {
|
|
13
13
|
return (0, environment_config_1.getEnvironmentConfig)('HAMMERKIT_STOP_TIMEOUT_MS', 10000);
|
|
14
14
|
}
|
|
15
|
+
// powershell takes about half a minute to start when it has to work out its module
|
|
16
|
+
// path itself, so the one of the process is passed on
|
|
17
|
+
function shellEnvs(windows, environment) {
|
|
18
|
+
const value = environment.processEnvs['PSModulePath'];
|
|
19
|
+
return windows && value !== undefined ? { PSModulePath: value } : {};
|
|
20
|
+
}
|
|
15
21
|
async function executeCommand(status, abortSignal, cwd, command, envs, environment) {
|
|
16
22
|
const windows = (0, os_1.platform)() === 'win32';
|
|
17
23
|
return new Promise((resolve, reject) => {
|
|
18
24
|
const ps = (0, child_process_1.spawn)(command, {
|
|
19
|
-
env: { ...envs, PATH: environment.processEnvs['PATH'] },
|
|
25
|
+
env: { ...envs, ...shellEnvs(windows, environment), PATH: environment.processEnvs['PATH'] },
|
|
20
26
|
cwd,
|
|
21
27
|
shell: windows ? 'powershell.exe' : true,
|
|
22
28
|
// its own process group, so an abort reaches every process the command
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const log_1 = require("./log");
|
|
5
5
|
const get_file_context_1 = require("./file/get-file-context");
|
|
6
6
|
const work_item_status_1 = require("./planner/work-item-status");
|
|
7
|
+
const redact_1 = require("./utils/redact");
|
|
7
8
|
const empty_writable_1 = require("./utils/empty-writable");
|
|
8
9
|
const run_program_1 = require("./run-program");
|
|
9
10
|
const abort_on_signals_1 = require("./utils/abort-on-signals");
|
|
@@ -19,13 +20,15 @@ process.on('beforeExit', () => {
|
|
|
19
20
|
process.exitCode = 1;
|
|
20
21
|
}
|
|
21
22
|
});
|
|
23
|
+
const secrets = (0, redact_1.createSecretRegistry)();
|
|
22
24
|
(0, run_program_1.runProgram)({
|
|
23
25
|
cwd: process.cwd(),
|
|
24
26
|
abortCtrl,
|
|
25
27
|
processEnvs: process.env,
|
|
26
28
|
file: (0, get_file_context_1.getFileContext)(process.cwd()),
|
|
27
29
|
console: (0, log_1.consoleContext)(process.stdout),
|
|
28
|
-
status: (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)()),
|
|
30
|
+
status: (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets),
|
|
31
|
+
secrets,
|
|
29
32
|
stdout: process.stdout,
|
|
30
33
|
stderr: process.stderr,
|
|
31
34
|
stdoutColumns: process.stdout.columns,
|
|
@@ -26,7 +26,9 @@ function getServiceDefinitionHash(item) {
|
|
|
26
26
|
src: service.src.map((s) => s.absolutePath),
|
|
27
27
|
healthcheck: service.healthcheck?.cmd.cmd ?? null,
|
|
28
28
|
// where secrets go, never their values: a rotated value needs a `down`
|
|
29
|
-
secrets: service.secrets.map((s) =>
|
|
29
|
+
secrets: service.secrets.map((s) => s.source.type === 'provider'
|
|
30
|
+
? `provider:${(0, work_secret_1.getSecretName)(s)}:${(0, work_secret_1.describeSecretSource)(s)}`
|
|
31
|
+
: `${s.source.type}:${(0, work_secret_1.getSecretName)(s)}`),
|
|
30
32
|
// the init task's id covers its image, commands, envs and mounts: a changed
|
|
31
33
|
// init recreates the service, so it runs again
|
|
32
34
|
init: service.init ? service.init.task.id() : null,
|
|
@@ -114,7 +114,7 @@ function parseService(workTree, service, environment, context) {
|
|
|
114
114
|
caching,
|
|
115
115
|
// set once the service is planned, see appendServiceInit
|
|
116
116
|
init: null,
|
|
117
|
-
secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment),
|
|
117
|
+
secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment, context.secrets, service.schema.account),
|
|
118
118
|
resources: (0, work_resources_1.parseWorkResources)(service.schema.resources),
|
|
119
119
|
};
|
|
120
120
|
}
|
|
@@ -70,8 +70,9 @@ function parseTask(cwd, task, environment, context) {
|
|
|
70
70
|
shell: task.schema.shell ? (0, template_value_1.templateValue)(task.schema.shell, envs) : '/bin/sh',
|
|
71
71
|
continuous: task.schema.continuous ?? false,
|
|
72
72
|
timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
|
|
73
|
-
secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment),
|
|
73
|
+
secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment, context.secrets, task.schema.account),
|
|
74
74
|
resources: (0, work_resources_1.parseWorkResources)(task.schema.resources),
|
|
75
|
+
runner: task.schema.runner ?? task.scope.schema.runner ?? null,
|
|
75
76
|
};
|
|
76
77
|
if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
|
|
77
78
|
return {
|
|
@@ -15,12 +15,13 @@ function logContext(type, item) {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
exports.logContext = logContext;
|
|
18
|
-
|
|
18
|
+
// `secrets` is the registry of the run (its Environment) whose output this console shows.
|
|
19
|
+
function statusConsole(writable, secrets = (0, redact_1.createSecretRegistry)()) {
|
|
19
20
|
const statusBuffer = new buffer_context_1.BufferContext(StatusBufferMax);
|
|
20
21
|
const consoleBuffer = new buffer_context_1.BufferContext(ConsoleBufferMax);
|
|
21
22
|
const emit = (0, emitter_1.emitter)();
|
|
22
23
|
function addMessage(context, unredacted) {
|
|
23
|
-
const message = { ...unredacted, message:
|
|
24
|
+
const message = { ...unredacted, message: secrets.redact(unredacted.message) };
|
|
24
25
|
emit.emit(message);
|
|
25
26
|
writable.write(JSON.stringify({ context, message }) + '\n');
|
|
26
27
|
if (message.type === 'console') {
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getSecretDigest = exports.resolveSecretValue = exports.parseWorkSecrets = exports.getPortableSecretName = exports.getSecretName = void 0;
|
|
3
|
+
exports.getSecretDigest = exports.resolveSecretValue = exports.parseWorkSecrets = exports.getPortableSecretName = exports.getSecretName = exports.describeSecretSource = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const fs_1 = require("fs");
|
|
7
7
|
const normalize_path_1 = require("./utils/normalize-path");
|
|
8
8
|
const template_value_1 = require("./utils/template-value");
|
|
9
9
|
const portable_path_1 = require("./utils/portable-path");
|
|
10
|
-
const
|
|
10
|
+
const abort_1 = require("../executer/abort");
|
|
11
|
+
const provider_values_1 = require("../secrets/provider-values");
|
|
12
|
+
// Where the value comes from, for messages and for what makes a service
|
|
13
|
+
// different: never the value.
|
|
14
|
+
function describeSecretSource(secret) {
|
|
15
|
+
const source = secret.source;
|
|
16
|
+
if (source.type === 'provider') {
|
|
17
|
+
return `${source.provider}:${source.ref} as ${source.account}`;
|
|
18
|
+
}
|
|
19
|
+
return source.type === 'env' ? `env:${source.name}` : `file:${source.path}`;
|
|
20
|
+
}
|
|
21
|
+
exports.describeSecretSource = describeSecretSource;
|
|
11
22
|
function getSecretName(secret) {
|
|
12
23
|
return secret.target.type === 'env' ? secret.target.name : secret.target.path;
|
|
13
24
|
}
|
|
@@ -18,15 +29,16 @@ function getPortableSecretName(projectRoot, secret) {
|
|
|
18
29
|
return secret.target.type === 'env' ? secret.target.name : (0, portable_path_1.portablePath)(projectRoot, secret.target.path);
|
|
19
30
|
}
|
|
20
31
|
exports.getPortableSecretName = getPortableSecretName;
|
|
21
|
-
function parseWorkSecrets(cwd, projectRoot, secrets, envs, environment
|
|
32
|
+
function parseWorkSecrets(cwd, projectRoot, secrets, envs, environment, catalog,
|
|
33
|
+
// the service account of the task or service the secrets belong to
|
|
34
|
+
account) {
|
|
22
35
|
return (secrets ?? []).map((secret) => {
|
|
23
36
|
const ref = secret.from.substring(secret.from.indexOf(':') + 1);
|
|
24
|
-
const source = secret.from.startsWith('env:')
|
|
25
|
-
? { type: 'env', name: ref }
|
|
26
|
-
: { type: 'file', path: (0, normalize_path_1.normalizePath)(cwd, (0, os_1.homedir)(), (0, template_value_1.templateValue)(ref, envs)) };
|
|
27
37
|
const target = secret.env
|
|
28
38
|
? { type: 'env', name: secret.env }
|
|
29
39
|
: { type: 'file', path: (0, normalize_path_1.normalizePath)(cwd, cwd, (0, template_value_1.templateValue)(secret.path ?? '', envs)) };
|
|
40
|
+
const name = target.type === 'env' ? target.name : secret.path;
|
|
41
|
+
const source = parseSecretSource(secret, ref, cwd, envs, catalog, account, `secret ${name}`);
|
|
30
42
|
const workSecret = {
|
|
31
43
|
source,
|
|
32
44
|
target,
|
|
@@ -37,20 +49,65 @@ function parseWorkSecrets(cwd, projectRoot, secrets, envs, environment) {
|
|
|
37
49
|
});
|
|
38
50
|
}
|
|
39
51
|
exports.parseWorkSecrets = parseWorkSecrets;
|
|
52
|
+
function parseSecretSource(secret, ref, cwd, envs, catalog, taskAccount, label) {
|
|
53
|
+
const kind = secret.from.substring(0, secret.from.indexOf(':'));
|
|
54
|
+
if (kind === 'env') {
|
|
55
|
+
return { type: 'env', name: ref };
|
|
56
|
+
}
|
|
57
|
+
if (kind === 'file') {
|
|
58
|
+
return { type: 'file', path: (0, normalize_path_1.normalizePath)(cwd, (0, os_1.homedir)(), (0, template_value_1.templateValue)(ref, envs)) };
|
|
59
|
+
}
|
|
60
|
+
const provider = catalog.providers[kind];
|
|
61
|
+
if (!provider) {
|
|
62
|
+
const declared = Object.keys(catalog.providers);
|
|
63
|
+
throw new Error(`${label}: unknown secret provider ${kind}, ${declared.length > 0 ? `declared: ${declared.join(', ')}` : 'none is declared'}`);
|
|
64
|
+
}
|
|
65
|
+
const providerRef = (0, template_value_1.templateValue)(ref, envs);
|
|
66
|
+
if (providerRef.length === 0 || providerRef.startsWith('-')) {
|
|
67
|
+
throw new Error(`${label}: a reference for ${kind} must not be empty or start with -`);
|
|
68
|
+
}
|
|
69
|
+
const accountName = secret.account ?? taskAccount ?? Object.values(catalog.accounts).find((a) => a.default)?.name;
|
|
70
|
+
if (!accountName) {
|
|
71
|
+
throw new Error(`${label}: ${kind}:${providerRef} needs an account, and none is named or marked default: true`);
|
|
72
|
+
}
|
|
73
|
+
const account = catalog.accounts[accountName];
|
|
74
|
+
if (!account) {
|
|
75
|
+
throw new Error(`${label}: unknown secret account ${accountName}, declared: ${Object.keys(catalog.accounts).join(', ') || 'none'}`);
|
|
76
|
+
}
|
|
77
|
+
const credentials = account.providers[kind];
|
|
78
|
+
if (!credentials) {
|
|
79
|
+
throw new Error(`${label}: account ${accountName} has no credentials for provider ${kind}`);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
type: 'provider',
|
|
83
|
+
provider: kind,
|
|
84
|
+
ref: providerRef,
|
|
85
|
+
account: accountName,
|
|
86
|
+
binding: { definition: provider, account: { name: accountName, env: credentials.env } },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
40
89
|
function readSecretValueSync(secret, environment) {
|
|
41
90
|
if (secret.source.type === 'env') {
|
|
42
91
|
const value = environment.processEnvs[secret.source.name];
|
|
43
92
|
if (value === undefined || value === null) {
|
|
44
93
|
throw new Error(`secret ${getSecretName(secret)}: environment variable ${secret.source.name} is not set`);
|
|
45
94
|
}
|
|
46
|
-
|
|
95
|
+
environment.secrets.register(value);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
if (secret.source.type === 'provider') {
|
|
99
|
+
const value = (0, provider_values_1.peekProviderValue)(secret.source.binding, secret.source.ref, environment);
|
|
100
|
+
if (value === undefined) {
|
|
101
|
+
throw new Error(`secret ${getSecretName(secret)}: ${describeSecretSource(secret)} was not fetched before its task id was computed`);
|
|
102
|
+
}
|
|
103
|
+
environment.secrets.register(value);
|
|
47
104
|
return value;
|
|
48
105
|
}
|
|
49
106
|
if (!(0, fs_1.existsSync)(secret.source.path)) {
|
|
50
107
|
throw new Error(`secret ${getSecretName(secret)}: file ${secret.source.path} does not exist`);
|
|
51
108
|
}
|
|
52
109
|
const value = (0, fs_1.readFileSync)(secret.source.path, 'utf8');
|
|
53
|
-
|
|
110
|
+
environment.secrets.register(value);
|
|
54
111
|
return value;
|
|
55
112
|
}
|
|
56
113
|
// The value, read when the task or service starts (or when a cache-affecting
|
|
@@ -61,14 +118,17 @@ async function resolveSecretValue(secret, environment) {
|
|
|
61
118
|
if (value === undefined || value === null) {
|
|
62
119
|
throw new Error(`secret ${getSecretName(secret)}: environment variable ${secret.source.name} is not set`);
|
|
63
120
|
}
|
|
64
|
-
|
|
121
|
+
environment.secrets.register(value);
|
|
65
122
|
return value;
|
|
66
123
|
}
|
|
124
|
+
if (secret.source.type === 'provider') {
|
|
125
|
+
return resolveProviderSecret(secret, secret.source, environment);
|
|
126
|
+
}
|
|
67
127
|
if (!(await environment.file.exists(secret.source.path))) {
|
|
68
128
|
throw new Error(`secret ${getSecretName(secret)}: file ${secret.source.path} does not exist`);
|
|
69
129
|
}
|
|
70
130
|
const value = await environment.file.read(secret.source.path);
|
|
71
|
-
|
|
131
|
+
environment.secrets.register(value);
|
|
72
132
|
return value;
|
|
73
133
|
}
|
|
74
134
|
exports.resolveSecretValue = resolveSecretValue;
|
|
@@ -80,4 +140,21 @@ function getSecretDigest(projectRoot, cwd, secret, value) {
|
|
|
80
140
|
return `sha256:${(0, crypto_1.createHash)('sha256').update(`${salt}\0${value}`).digest('hex')}`;
|
|
81
141
|
}
|
|
82
142
|
exports.getSecretDigest = getSecretDigest;
|
|
143
|
+
async function resolveProviderSecret(secret, source, environment) {
|
|
144
|
+
let value;
|
|
145
|
+
try {
|
|
146
|
+
value = await (0, provider_values_1.fetchProviderValue)(source.binding, source.ref, environment);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof abort_1.AbortError) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`secret ${getSecretName(secret)} (${describeSecretSource(secret)}): ${error instanceof Error ? error.message : error}`);
|
|
153
|
+
}
|
|
154
|
+
if (value.length === 0) {
|
|
155
|
+
throw new Error(`secret ${getSecretName(secret)} (${describeSecretSource(secret)}): the provider returned no value`);
|
|
156
|
+
}
|
|
157
|
+
environment.secrets.register(value);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
83
160
|
//# sourceMappingURL=work-secret.js.map
|
package/dist/program.js
CHANGED
|
@@ -25,6 +25,8 @@ const version_1 = require("./version");
|
|
|
25
25
|
const read_only_1 = require("./cache/read-only");
|
|
26
26
|
const units_1 = require("./utils/units");
|
|
27
27
|
const cache_inventory_1 = require("./cache/cache-inventory");
|
|
28
|
+
const remote_commands_1 = require("./remote/remote-commands");
|
|
29
|
+
const server_command_1 = require("./server/server-command");
|
|
28
30
|
async function createCli(fileName, environment, workScope) {
|
|
29
31
|
const { ctx, scope } = await (0, schema_parser_1.createParseContext)(fileName, environment);
|
|
30
32
|
const referencedScope = await (0, reference_parser_1.parseReferences)(ctx, scope, environment);
|
|
@@ -102,6 +104,7 @@ function parseWorkLabelScope(options) {
|
|
|
102
104
|
}
|
|
103
105
|
async function getProgram(environment, argv) {
|
|
104
106
|
const program = new commander_1.Command();
|
|
107
|
+
(0, server_command_1.registerServerCommand)(program, environment);
|
|
105
108
|
const parsed = (0, parse_set_arguments_1.parseSetArguments)(argv);
|
|
106
109
|
const args = parsed.args;
|
|
107
110
|
environment.processEnvs = { ...environment.processEnvs, ...parsed.values };
|
|
@@ -110,6 +113,7 @@ async function getProgram(environment, argv) {
|
|
|
110
113
|
if (fileIndex >= 0) {
|
|
111
114
|
args.splice(fileIndex, 2);
|
|
112
115
|
}
|
|
116
|
+
(0, remote_commands_1.registerRemoteCommands)(program, environment, fileName);
|
|
113
117
|
if (await environment.file.exists(fileName)) {
|
|
114
118
|
program
|
|
115
119
|
.command('ls')
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HASH_PATTERN = exports.hashBundle = exports.bundleSchema = exports.isBundlePath = exports.MAX_BUNDLE_BYTES = exports.MAX_BUNDLE_FILES = exports.DEFAULT_ENTRY = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
exports.DEFAULT_ENTRY = '.hammerkit.yaml';
|
|
7
|
+
exports.MAX_BUNDLE_FILES = 200;
|
|
8
|
+
exports.MAX_BUNDLE_BYTES = 1024 * 1024;
|
|
9
|
+
// A path inside the bundle: relative, forward slashes, nothing that climbs out.
|
|
10
|
+
function isBundlePath(path) {
|
|
11
|
+
return (path.length > 0 &&
|
|
12
|
+
path.length <= 200 &&
|
|
13
|
+
!path.startsWith('/') &&
|
|
14
|
+
!path.includes('\\') &&
|
|
15
|
+
!path.includes('\0') &&
|
|
16
|
+
!path.split('/').some((part) => part === '' || part === '.' || part === '..'));
|
|
17
|
+
}
|
|
18
|
+
exports.isBundlePath = isBundlePath;
|
|
19
|
+
exports.bundleSchema = zod_1.z
|
|
20
|
+
.object({
|
|
21
|
+
entry: zod_1.z.string().default(exports.DEFAULT_ENTRY),
|
|
22
|
+
files: zod_1.z.record(zod_1.z.string()),
|
|
23
|
+
})
|
|
24
|
+
.strict()
|
|
25
|
+
.superRefine((bundle, ctx) => {
|
|
26
|
+
const paths = Object.keys(bundle.files);
|
|
27
|
+
const bad = paths.find((path) => !isBundlePath(path));
|
|
28
|
+
if (bad !== undefined) {
|
|
29
|
+
ctx.addIssue({
|
|
30
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
31
|
+
path: ['files'],
|
|
32
|
+
message: `"${bad}" is not a path inside the bundle`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (paths.length > exports.MAX_BUNDLE_FILES) {
|
|
36
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['files'], message: `more than ${exports.MAX_BUNDLE_FILES} files` });
|
|
37
|
+
}
|
|
38
|
+
const bytes = paths.reduce((sum, path) => sum + Buffer.byteLength(bundle.files[path]), 0);
|
|
39
|
+
if (bytes > exports.MAX_BUNDLE_BYTES) {
|
|
40
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['files'], message: `larger than ${exports.MAX_BUNDLE_BYTES} bytes` });
|
|
41
|
+
}
|
|
42
|
+
if (!isBundlePath(bundle.entry) || !Object.prototype.hasOwnProperty.call(bundle.files, bundle.entry)) {
|
|
43
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['entry'], message: `${bundle.entry} is not one of the files` });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// The version of a bundle: the hash of its entry and of every file, in path order,
|
|
47
|
+
// so the same files always have the same version, wherever it is computed. It is
|
|
48
|
+
// the sha-256 of the JSON `[entry, [[path, sha-256 of the content], ...]]`.
|
|
49
|
+
function hashBundle(bundle) {
|
|
50
|
+
const files = Object.keys(bundle.files)
|
|
51
|
+
.sort()
|
|
52
|
+
.map((path) => [path, (0, crypto_1.createHash)('sha256').update(bundle.files[path]).digest('hex')]);
|
|
53
|
+
return `sha256:${(0, crypto_1.createHash)('sha256')
|
|
54
|
+
.update(JSON.stringify([bundle.entry, files]))
|
|
55
|
+
.digest('hex')}`;
|
|
56
|
+
}
|
|
57
|
+
exports.hashBundle = hashBundle;
|
|
58
|
+
exports.HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
59
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectBundle = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const resolve_git_source_1 = require("../includes/resolve-git-source");
|
|
6
|
+
const schema_parser_1 = require("../schema/schema-parser");
|
|
7
|
+
const bundle_1 = require("./bundle");
|
|
8
|
+
// The bundle of a build file on this machine: the file itself and every local
|
|
9
|
+
// file it includes or references, as the server reads them when it is published.
|
|
10
|
+
// The build file is read with the parser of a run, so what it pulls in is what a
|
|
11
|
+
// run would read. Git includes are not part of it (the server fetches them, at
|
|
12
|
+
// the commit they are pinned to), and neither is anything outside of the
|
|
13
|
+
// directory of the build file, which a server could not reproduce.
|
|
14
|
+
async function collectBundle(fileName, environment) {
|
|
15
|
+
if (!(await environment.file.exists(fileName))) {
|
|
16
|
+
throw new Error(`there is no build file at ${fileName}`);
|
|
17
|
+
}
|
|
18
|
+
const entryPath = (0, path_1.resolve)(fileName);
|
|
19
|
+
const root = (0, path_1.dirname)(entryPath);
|
|
20
|
+
const { ctx } = await (0, schema_parser_1.createParseContext)(entryPath, environment);
|
|
21
|
+
const paths = new Set([entryPath]);
|
|
22
|
+
for (const scope of Object.values(ctx.files)) {
|
|
23
|
+
if (!scope.remote) {
|
|
24
|
+
paths.add((0, path_1.resolve)(scope.fileName));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const files = {};
|
|
28
|
+
for (const path of [...paths].sort()) {
|
|
29
|
+
if (!(0, resolve_git_source_1.isWithin)(root, path)) {
|
|
30
|
+
throw new Error(`${path} is outside of ${root}: a published build file can only include files below its own directory`);
|
|
31
|
+
}
|
|
32
|
+
files[(0, path_1.relative)(root, path).split(path_1.sep).join('/')] = await environment.file.read(path);
|
|
33
|
+
}
|
|
34
|
+
const result = bundle_1.bundleSchema.safeParse({ entry: (0, path_1.relative)(root, entryPath).split(path_1.sep).join('/'), files });
|
|
35
|
+
if (!result.success) {
|
|
36
|
+
throw new Error(`the build file can't be published: ${result.error.issues[0].message}`);
|
|
37
|
+
}
|
|
38
|
+
return result.data;
|
|
39
|
+
}
|
|
40
|
+
exports.collectBundle = collectBundle;
|
|
41
|
+
//# sourceMappingURL=collect-bundle.js.map
|