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.
Files changed (54) hide show
  1. package/build.schema.json +92 -2
  2. package/dist/cli.js +25 -0
  3. package/dist/executer/environment-mock.js +4 -1
  4. package/dist/executer/execute-command.js +7 -1
  5. package/dist/index.js +4 -1
  6. package/dist/planner/service-definition.js +3 -1
  7. package/dist/planner/utils/append-work-service.js +1 -1
  8. package/dist/planner/utils/append-work-task.js +2 -1
  9. package/dist/planner/work-item-status.js +3 -2
  10. package/dist/planner/work-secret.js +87 -10
  11. package/dist/program.js +4 -0
  12. package/dist/remote/bundle.js +59 -0
  13. package/dist/remote/collect-bundle.js +41 -0
  14. package/dist/remote/credentials.js +93 -0
  15. package/dist/remote/device-login.js +177 -0
  16. package/dist/remote/host-config.js +170 -0
  17. package/dist/remote/location.js +46 -0
  18. package/dist/remote/remote-commands.js +154 -0
  19. package/dist/remote/remote-work-commands.js +152 -0
  20. package/dist/remote/server-client.js +88 -0
  21. package/dist/remote/source-manifest.js +79 -0
  22. package/dist/remote/text-diff.js +89 -0
  23. package/dist/schema/build-file-base-task-schema.js +6 -0
  24. package/dist/schema/build-file-container-service-schema.js +2 -0
  25. package/dist/schema/build-file-schema.js +6 -0
  26. package/dist/schema/build-file-secret-provider-schema.js +36 -0
  27. package/dist/schema/build-file-secret-schema.js +6 -3
  28. package/dist/schema/reference-parser.js +4 -0
  29. package/dist/schema/runner-schema.js +13 -0
  30. package/dist/secrets/command-secret-provider.js +108 -0
  31. package/dist/secrets/prefetch-secrets.js +27 -0
  32. package/dist/secrets/provider-values.js +47 -0
  33. package/dist/secrets/secret-catalog.js +78 -0
  34. package/dist/secrets/secret-provider.js +19 -0
  35. package/dist/server/approvals-api.js +99 -0
  36. package/dist/server/definitions-api.js +119 -0
  37. package/dist/server/definitions.js +248 -0
  38. package/dist/server/discovery.js +9 -0
  39. package/dist/server/executor.js +238 -0
  40. package/dist/server/git-checkout.js +63 -0
  41. package/dist/server/http.js +39 -0
  42. package/dist/server/oauth.js +226 -0
  43. package/dist/server/policy.js +90 -0
  44. package/dist/server/proposals-api.js +123 -0
  45. package/dist/server/proposals.js +126 -0
  46. package/dist/server/run-request.js +67 -0
  47. package/dist/server/runs.js +279 -0
  48. package/dist/server/server-command.js +43 -0
  49. package/dist/server/server-config.js +233 -0
  50. package/dist/server/server.js +338 -0
  51. package/dist/server/sources-api.js +106 -0
  52. package/dist/server/sources.js +258 -0
  53. package/dist/utils/redact.js +75 -19
  54. 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": "^(env|file):.+$"
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 or file:path, injected as env or a read-only file"
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) => `${s.source.type}:${(0, work_secret_1.getSecretName)(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
- function statusConsole(writable) {
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: (0, redact_1.redact)(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 redact_1 = require("../utils/redact");
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
- (0, redact_1.registerSecretValue)(value);
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
- (0, redact_1.registerSecretValue)(value);
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
- (0, redact_1.registerSecretValue)(value);
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
- (0, redact_1.registerSecretValue)(value);
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