hammerkit 1.27.0-next.8 → 2.0.0-next.1

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 (129) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +76 -17
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-http-route.js +1 -1
  36. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +138 -18
  37. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  38. package/dist/kubernetes/ensure-persistent-data.js +212 -10
  39. package/dist/kubernetes/follow-pod-logs.js +182 -0
  40. package/dist/kubernetes/get-service-ip.js +2 -2
  41. package/dist/kubernetes/job-placement.js +36 -12
  42. package/dist/kubernetes/job-secrets.js +9 -6
  43. package/dist/kubernetes/kept-claims.js +22 -10
  44. package/dist/kubernetes/kubernetes-instance.js +1 -0
  45. package/dist/kubernetes/network-policy.js +16 -8
  46. package/dist/kubernetes/remove-persistent-data.js +3 -1
  47. package/dist/kubernetes/remove-stale-items.js +46 -0
  48. package/dist/kubernetes/resources.js +25 -3
  49. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  50. package/dist/kubernetes/sandbox-runner.js +628 -0
  51. package/dist/kubernetes/secrets.js +7 -11
  52. package/dist/kubernetes/service-rollouts.js +74 -0
  53. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  54. package/dist/kubernetes/volumes.js +96 -11
  55. package/dist/main.js +2 -0
  56. package/dist/optimizer/calculate-checksum.js +3 -2
  57. package/dist/optimizer/check-key.js +94 -0
  58. package/dist/optimizer/check-keys.js +64 -0
  59. package/dist/optimizer/get-work-cache-stats.js +17 -3
  60. package/dist/optimizer/work-task-cache-description.js +49 -1
  61. package/dist/planner/service-definition.js +9 -4
  62. package/dist/planner/utils/append-work-dependencies.js +5 -3
  63. package/dist/planner/utils/append-work-service.js +18 -1
  64. package/dist/planner/utils/append-work-task.js +13 -3
  65. package/dist/planner/utils/get-work-runtime.js +6 -1
  66. package/dist/planner/utils/normalize-path.js +19 -1
  67. package/dist/planner/utils/parse-work-mount.js +7 -1
  68. package/dist/planner/utils/parse-work-port.js +4 -1
  69. package/dist/planner/validate.js +22 -0
  70. package/dist/planner/work-network.js +3 -1
  71. package/dist/planner/work-runtime-aggregate.js +32 -0
  72. package/dist/planner/work-runtime-kubernetes.js +153 -26
  73. package/dist/planner/work-task.js +6 -1
  74. package/dist/program.js +27 -9
  75. package/dist/remote/device-login.js +7 -2
  76. package/dist/remote/host-config.js +69 -3
  77. package/dist/remote/remote-commands.js +33 -7
  78. package/dist/remote/remote-status.js +112 -0
  79. package/dist/remote/remote-work-commands.js +562 -45
  80. package/dist/remote/server-client.js +15 -2
  81. package/dist/remote/token-source.js +64 -26
  82. package/dist/run-program.js +9 -0
  83. package/dist/schema/build-file-base-task-schema.js +3 -3
  84. package/dist/schema/build-file-container-service-schema.js +10 -0
  85. package/dist/schema/build-file-container-task-schema.js +16 -1
  86. package/dist/schema/build-file-include-schema.js +18 -4
  87. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  88. package/dist/schema/build-file-task-schema.js +5 -1
  89. package/dist/schema/capabilities-schema.js +56 -0
  90. package/dist/schema/reference-parser.js +119 -11
  91. package/dist/schema/removed-properties.js +20 -0
  92. package/dist/schema/schema-parser.js +94 -41
  93. package/dist/schema/work-scope-parser.js +33 -10
  94. package/dist/secrets/command-secret-provider.js +3 -1
  95. package/dist/secrets/github-app-token-command.js +3 -1
  96. package/dist/secrets/secret-catalog.js +43 -19
  97. package/dist/server/accounts-api.js +4 -1
  98. package/dist/server/approvals-api.js +1 -1
  99. package/dist/server/capacity-format.js +36 -0
  100. package/dist/server/capacity.js +300 -0
  101. package/dist/server/checks-api.js +83 -0
  102. package/dist/server/checks.js +116 -0
  103. package/dist/server/dashboard.js +196 -0
  104. package/dist/server/definitions-api.js +66 -3
  105. package/dist/server/endpoints.js +794 -0
  106. package/dist/server/executor.js +164 -9
  107. package/dist/server/kubernetes-backend.js +478 -70
  108. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  109. package/dist/server/overview.js +271 -0
  110. package/dist/server/plan.js +170 -121
  111. package/dist/server/policy.js +34 -2
  112. package/dist/server/proposals-api.js +17 -8
  113. package/dist/server/repo-access.js +5 -0
  114. package/dist/server/run-logs.js +4 -0
  115. package/dist/server/run-plan.js +38 -4
  116. package/dist/server/run-request.js +58 -3
  117. package/dist/server/run-worker.js +262 -13
  118. package/dist/server/runs.js +969 -36
  119. package/dist/server/sandbox-pod.js +28 -0
  120. package/dist/server/sandbox-storage.js +23 -0
  121. package/dist/server/sandboxes.js +8 -0
  122. package/dist/server/server-config.js +246 -10
  123. package/dist/server/server.js +429 -32
  124. package/dist/server/service-runs.js +82 -0
  125. package/dist/server/timings.js +132 -0
  126. package/dist/server/wait-reason.js +21 -0
  127. package/dist/utils/tar.js +28 -1
  128. package/dist/utils/terminal-prompt.js +53 -0
  129. package/package.json +8 -2
package/dist/program.js CHANGED
@@ -111,6 +111,18 @@ function getListJson(tasks, services) {
111
111
  })),
112
112
  };
113
113
  }
114
+ // `up` and `down` select by name or by label, not both: a name outside the labels would
115
+ // silently select nothing
116
+ function parseWorkNameScope(names, options) {
117
+ const scope = parseWorkLabelScope(options);
118
+ if (names.length === 0) {
119
+ return scope;
120
+ }
121
+ if (scope.filterLabels || scope.excludeLabels) {
122
+ throw new Error('name tasks and services or select them with --filter/--exclude, not both');
123
+ }
124
+ return { ...scope, names };
125
+ }
114
126
  function parseWorkLabelScope(options) {
115
127
  const scope = {
116
128
  environmentName: null,
@@ -587,9 +599,9 @@ async function getProgram(environment, argv) {
587
599
  }
588
600
  });
589
601
  program
590
- .command('up')
591
- .description("start services(s), with the arguments after -- appended to the selected service's cmd")
592
- .usage('[options] [-- args...]')
602
+ .command('up [names...]')
603
+ .description("start services(s), all or the named tasks and services, with the arguments after -- appended to the selected service's cmd")
604
+ .usage('[options] [names...] [-- args...]')
593
605
  .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
594
606
  .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
595
607
  .addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
@@ -608,11 +620,12 @@ async function getProgram(environment, argv) {
608
620
  .choices(['checksum', 'modify-date', 'none']))
609
621
  .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
610
622
  .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
623
+ .addOption(new commander_1.Option('--max-timeout <duration>', 'fail any task after this long, also one with a longer timeout of its own (e.g. 2m)').argParser(units_1.parseDuration))
611
624
  .addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
612
- .action(async (options, command) => {
625
+ .action(async (names, options, command) => {
613
626
  try {
614
627
  const scope = {
615
- ...parseWorkLabelScope(options),
628
+ ...parseWorkNameScope(names, options),
616
629
  args: { values: passthrough, services: true, cache: keepsCache(command) },
617
630
  };
618
631
  const cli = await createCli(fileName, environment, scope);
@@ -626,6 +639,7 @@ async function getProgram(environment, argv) {
626
639
  wait: options.wait,
627
640
  cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
628
641
  timeout: options.timeout ?? null,
642
+ maxTimeout: options.maxTimeout ?? null,
629
643
  network: options.network ?? null,
630
644
  });
631
645
  if (!result.success) {
@@ -644,14 +658,14 @@ async function getProgram(environment, argv) {
644
658
  }
645
659
  });
646
660
  program
647
- .command('down')
648
- .description('stop services(s)')
661
+ .command('down [names...]')
662
+ .description('stop services(s), all or the named services and the services the named tasks need')
649
663
  .addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
650
664
  .addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
651
665
  .addOption(new commander_1.Option('--env <name>', 'environment'))
652
- .action(async (options) => {
666
+ .action(async (names, options) => {
653
667
  try {
654
- const scope = parseWorkLabelScope(options);
668
+ const scope = { ...parseWorkNameScope(names, options), stop: true };
655
669
  const cli = await createCli(fileName, environment, scope);
656
670
  const result = await cli.runDown();
657
671
  if (!result.success) {
@@ -688,8 +702,10 @@ async function getProgram(environment, argv) {
688
702
  .addOption(new commander_1.Option('--dry-run', 'print the execution plan with predicted cache hits/misses without running').default(false))
689
703
  .addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
690
704
  .addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
705
+ .addOption(new commander_1.Option('--max-timeout <duration>', 'fail any task after this long, also one with a longer timeout of its own (e.g. 2m)').argParser(units_1.parseDuration))
691
706
  .addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
692
707
  .addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
708
+ .addOption(new commander_1.Option('--no-restore', 'only look cache hits up, download their outputs only when a task that runs needs them'))
693
709
  .action(async (task, options, command) => {
694
710
  try {
695
711
  // For machine-readable JSON, suppress the human progress logger (which
@@ -722,8 +738,10 @@ async function getProgram(environment, argv) {
722
738
  explain: options.explain,
723
739
  cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
724
740
  timeout: options.timeout ?? null,
741
+ maxTimeout: options.maxTimeout ?? null,
725
742
  network: options.network ?? null,
726
743
  skipDeps: options.skipDeps !== false,
744
+ restore: options.restore !== false,
727
745
  });
728
746
  // Reporting only: the summary never changes the exit code or behavior.
729
747
  if (!options.watch) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deviceLogin = void 0;
3
+ exports.deviceLogin = exports.NoSignInError = void 0;
4
4
  const server_client_1 = require("./server-client");
5
5
  const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
6
6
  const DEFAULT_INTERVAL_SECONDS = 5;
@@ -8,6 +8,11 @@ const SLOW_DOWN_SECONDS = 5;
8
8
  const FETCH_TIMEOUT_MS = 10000;
9
9
  const MIN_INTERVAL_SECONDS = 1;
10
10
  const MAX_LIFETIME_SECONDS = 30 * 60;
11
+ // The server takes tokens of this issuer but offers no device sign-in for it: the token has
12
+ // to come from elsewhere, such as a token command.
13
+ class NoSignInError extends server_client_1.ServerError {
14
+ }
15
+ exports.NoSignInError = NoSignInError;
11
16
  const isLoopbackHost = (hostname) => ['localhost', '127.0.0.1', '[::1]'].includes(hostname);
12
17
  // An endpoint must use the scheme of the issuer: loopback http is for a test
13
18
  // identity provider on this machine only, never beside an https issuer.
@@ -89,7 +94,7 @@ async function deviceLogin(server, options) {
89
94
  throw new server_client_1.ServerError(`${server.url} does not accept tokens from ${server.issuer} (it accepts ${accepted})`);
90
95
  }
91
96
  if (!offered.clientId) {
92
- throw new server_client_1.ServerError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
97
+ throw new NoSignInError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
93
98
  }
94
99
  // What was pinned (by `remote add` or an earlier login) wins; the server can
95
100
  // only offer the same, never something else to sign in with.
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pinLogin = exports.setDefaultLocation = exports.removeServer = exports.addServer = exports.normalizeServerUrl = exports.validateServerName = exports.writeHostConfig = exports.readHostConfig = exports.getHostConfigPath = exports.hasServer = exports.hostConfigSchema = exports.hostServerSchema = exports.RESERVED_SERVER_NAMES = void 0;
3
+ exports.pinLogin = exports.setDefaultLocation = exports.removeServer = exports.addServer = exports.normalizeServerUrl = exports.validateServerName = exports.writeHostConfig = exports.readHostConfig = exports.getHostConfigPath = exports.getRegisteredServer = exports.getTokenCommand = exports.hasServer = exports.hostConfigSchema = exports.hostServerSchema = exports.RESERVED_SERVER_NAMES = void 0;
4
4
  const path_1 = require("path");
5
5
  const zod_1 = require("zod");
6
6
  const yaml_1 = require("yaml");
7
7
  const get_cache_directory_1 = require("../optimizer/get-cache-directory");
8
+ const build_file_secret_provider_schema_1 = require("../schema/build-file-secret-provider-schema");
8
9
  // Names with a meaning of their own in `run.on` / `--on`
9
10
  exports.RESERVED_SERVER_NAMES = ['local', 'auto'];
10
11
  const serverNamePattern = /^[a-z0-9][a-z0-9._-]*$/i;
@@ -28,12 +29,25 @@ exports.hostServerSchema = zod_1.z
28
29
  scope: zod_1.z.string().min(1).optional(),
29
30
  })
30
31
  .strict();
32
+ // A command that prints the token for a server instead of a login, see token-source.ts.
33
+ // It is bound to the url and issuer it was registered for, like a sign-in, and lives
34
+ // beside `servers` rather than in it: hammerkit before 1.27 refuses unknown keys of a
35
+ // server, but keeps unknown top-level keys. It is only ever read from the host config,
36
+ // a build file can't name one.
37
+ const tokenCommandSchema = zod_1.z
38
+ .object({ url: zod_1.z.string(), issuer: zod_1.z.string(), command: zod_1.z.string().trim().min(1) })
39
+ .passthrough();
31
40
  // Unknown top-level keys are kept when the file is written back, so a newer
32
41
  // hammerkit's settings survive an older one editing the file.
33
42
  exports.hostConfigSchema = zod_1.z
34
43
  .object({
35
44
  servers: zod_1.z.record(exports.hostServerSchema).default({}),
45
+ tokenCommands: zod_1.z.record(tokenCommandSchema).optional(),
36
46
  run: zod_1.z.object({ on: zod_1.z.string().optional() }).passthrough().default({}),
47
+ // how this machine reads secrets, beside what the build files declare (see
48
+ // collectSecretCatalog); a server reads only its own config
49
+ secretProviders: build_file_secret_provider_schema_1.buildFileSecretProvidersSchema.optional(),
50
+ secretAccounts: build_file_secret_provider_schema_1.buildFileSecretAccountsSchema.optional(),
37
51
  })
38
52
  .passthrough();
39
53
  // Names like "constructor" must not find inherited members.
@@ -41,6 +55,35 @@ function hasServer(servers, name) {
41
55
  return Object.prototype.hasOwnProperty.call(servers, name);
42
56
  }
43
57
  exports.hasServer = hasServer;
58
+ // The token command of a server, unless it was registered for another url or issuer (an
59
+ // older hammerkit that pointed the name elsewhere leaves it behind).
60
+ function getTokenCommand(config, name) {
61
+ const commands = config.tokenCommands ?? {};
62
+ const entry = Object.prototype.hasOwnProperty.call(commands, name) ? commands[name] : undefined;
63
+ const server = hasServer(config.servers, name) ? config.servers[name] : undefined;
64
+ return entry && server && entry.url === server.url && entry.issuer === server.issuer ? entry.command : undefined;
65
+ }
66
+ exports.getTokenCommand = getTokenCommand;
67
+ function getRegisteredServer(config, name) {
68
+ const tokenCommand = getTokenCommand(config, name);
69
+ return { ...config.servers[name], ...(tokenCommand ? { tokenCommand } : {}) };
70
+ }
71
+ exports.getRegisteredServer = getRegisteredServer;
72
+ function setTokenCommand(config, name, entry) {
73
+ const commands = { ...(config.tokenCommands ?? {}) };
74
+ if (entry) {
75
+ commands[name] = entry;
76
+ }
77
+ else {
78
+ delete commands[name];
79
+ }
80
+ if (Object.keys(commands).length > 0) {
81
+ config.tokenCommands = commands;
82
+ }
83
+ else {
84
+ delete config.tokenCommands;
85
+ }
86
+ }
44
87
  function getHostConfigPath(environment) {
45
88
  return environment.processEnvs.HAMMERKIT_CONFIG ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'config.yaml');
46
89
  }
@@ -121,16 +164,38 @@ async function addServer(environment, name, url, options) {
121
164
  server[key] = options[key];
122
165
  }
123
166
  }
167
+ if (typeof options.tokenCommand === 'string' && !options.tokenCommand.trim()) {
168
+ throw new Error('the token command must not be empty, use --no-token-command to remove it');
169
+ }
124
170
  const config = await readHostConfig(environment);
125
171
  const existing = hasServer(config.servers, name) ? config.servers[name] : undefined;
126
- const changed = (a, b) => a.url !== b.url || a.issuer !== b.issuer || PINNED.some((key) => a[key] !== b[key]);
172
+ const sameServer = (a, b) => a.url === b.url && a.issuer === b.issuer;
173
+ // Adding the same server again keeps what was pinned (by a login, too) unless it is given,
174
+ // so a later `remote add ... --token-command` needs no --force.
175
+ if (existing && sameServer(existing, server)) {
176
+ for (const key of PINNED) {
177
+ if (!server[key] && existing[key]) {
178
+ server[key] = existing[key];
179
+ }
180
+ }
181
+ }
182
+ const changed = (a, b) => !sameServer(a, b) || PINNED.some((key) => a[key] !== b[key]);
127
183
  if (existing && changed(existing, server) && !options.force) {
128
184
  throw new Error(`server "${name}" is already registered as ${existing.url} (issuer ${existing.issuer}); ` +
129
185
  `use --force to point it somewhere else`);
130
186
  }
187
+ // The command decides which identity's token goes to the server: changing it does not
188
+ // redirect anything, but a name pointed at another server does not keep the old one.
189
+ const kept = existing && sameServer(existing, server) ? getTokenCommand(config, name) : undefined;
190
+ const tokenCommand = typeof options.tokenCommand === 'string'
191
+ ? options.tokenCommand.trim()
192
+ : options.tokenCommand === undefined
193
+ ? kept
194
+ : undefined;
131
195
  config.servers[name] = server;
196
+ setTokenCommand(config, name, tokenCommand ? { url: server.url, issuer: server.issuer, command: tokenCommand } : undefined);
132
197
  await writeHostConfig(environment, config);
133
- return server;
198
+ return { ...server, ...(tokenCommand ? { tokenCommand } : {}) };
134
199
  }
135
200
  exports.addServer = addServer;
136
201
  async function removeServer(environment, name) {
@@ -139,6 +204,7 @@ async function removeServer(environment, name) {
139
204
  throw new Error(`server "${name}" is not registered`);
140
205
  }
141
206
  delete config.servers[name];
207
+ setTokenCommand(config, name, undefined);
142
208
  const resetDefault = config.run.on === name;
143
209
  if (resetDefault) {
144
210
  delete config.run.on;
@@ -40,6 +40,8 @@ passthrough = []) {
40
40
  .addOption(new commander_1.Option('--client-id <id>', 'OAuth client to sign in with (pinned; default: what the server offers)'))
41
41
  .addOption(new commander_1.Option('--audience <audience>', 'audience of the token (pinned; default: what the server offers)'))
42
42
  .addOption(new commander_1.Option('--scope <scope>', 'scopes to request (pinned; default: what the server offers)'))
43
+ .addOption(new commander_1.Option('--token-command <command>', 'a command that prints the token for this server, instead of remote login (e.g. of a service account)'))
44
+ .addOption(new commander_1.Option('--no-token-command', 'remove the token command, sign in with remote login again'))
43
45
  .addOption(new commander_1.Option('--force', 'point an already registered name at a different server').default(false))
44
46
  .action(guarded('Remote add was not successful', async (name, url, options) => {
45
47
  const server = await (0, host_config_1.addServer)(environment, name, url, {
@@ -47,9 +49,13 @@ passthrough = []) {
47
49
  clientId: options.clientId,
48
50
  audience: options.audience,
49
51
  scope: options.scope,
52
+ tokenCommand: options.tokenCommand,
50
53
  force: options.force,
51
54
  });
52
55
  environment.stdout.write(`added ${name}: ${server.url} (issuer ${server.issuer})\n`);
56
+ if (server.tokenCommand) {
57
+ environment.stdout.write(`tokens for ${name} come from: ${server.tokenCommand}\n`);
58
+ }
53
59
  }));
54
60
  remote
55
61
  .command('list')
@@ -62,9 +68,10 @@ passthrough = []) {
62
68
  return;
63
69
  }
64
70
  for (const name of names) {
65
- const server = config.servers[name];
71
+ const server = (0, host_config_1.getRegisteredServer)(config, name);
66
72
  const marker = config.run.on === name ? colors_1.default.green(' (default)') : '';
67
- environment.stdout.write(`${colors_1.default.bold(name)}${marker} ${server.url} issuer ${server.issuer}\n`);
73
+ const token = server.tokenCommand ? ` token from: ${server.tokenCommand}` : '';
74
+ environment.stdout.write(`${colors_1.default.bold(name)}${marker} ${server.url} issuer ${server.issuer}${token}\n`);
68
75
  }
69
76
  if (config.run.on === 'auto' || config.run.on === 'local') {
70
77
  environment.stdout.write(`default location: ${config.run.on}\n`);
@@ -95,17 +102,33 @@ passthrough = []) {
95
102
  ? `server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`
96
103
  : 'name a server, or set one as the default with: hammerkit remote use <name>');
97
104
  }
98
- return { name: chosen, server: config.servers[chosen] };
105
+ return { name: chosen, server: (0, host_config_1.getRegisteredServer)(config, chosen) };
99
106
  };
100
107
  remote
101
108
  .command('login <name>')
102
109
  .description('sign in to a registered server with your identity provider (device code, works headless)')
103
110
  .action(guarded('Remote login was not successful', async (name) => {
104
111
  const { server } = await getServer(name);
105
- const { pinned, ...result } = await (0, device_login_1.deviceLogin)(server, {
106
- prompt: (message) => environment.stdout.write(`${message}\n`),
107
- signal: environment.abortCtrl.signal,
108
- });
112
+ if (server.tokenCommand) {
113
+ // a sign-in would never be used: the command's token goes first
114
+ throw new Error(`${name} takes its tokens from its token command (${server.tokenCommand}), it needs no sign-in; ` +
115
+ `to sign in instead, remove it with: hammerkit remote add ${name} ${server.url} --issuer ${server.issuer} --no-token-command`);
116
+ }
117
+ let login;
118
+ try {
119
+ login = await (0, device_login_1.deviceLogin)(server, {
120
+ prompt: (message) => environment.stdout.write(`${message}\n`),
121
+ signal: environment.abortCtrl.signal,
122
+ });
123
+ }
124
+ catch (e) {
125
+ if (e instanceof device_login_1.NoSignInError) {
126
+ throw new Error(`${e.message}: register a command that prints one, such as the token of a service account, with: ` +
127
+ `hammerkit remote add ${name} ${server.url} --issuer ${server.issuer} --token-command '<command>'`);
128
+ }
129
+ throw e;
130
+ }
131
+ const { pinned, ...result } = login;
109
132
  await (0, host_config_1.pinLogin)(environment, name, pinned);
110
133
  await (0, credentials_1.saveToken)(environment, name, { url: server.url, issuer: server.issuer, ...result });
111
134
  try {
@@ -155,6 +178,9 @@ passthrough = []) {
155
178
  if (me.repos.length > 0) {
156
179
  environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
157
180
  }
181
+ if (me.observe) {
182
+ environment.stdout.write('you observe every run of the server, see them with: hammerkit remote runs --all\n');
183
+ }
158
184
  }));
159
185
  (0, remote_work_commands_1.registerWorkCommands)(remote, {
160
186
  environment,
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.describeTaskStats = exports.describeCapacity = exports.describeOverview = exports.describeServiceRun = exports.describeWork = exports.describeWait = void 0;
7
+ const colors_1 = __importDefault(require("colors"));
8
+ const capacity_format_1 = require("../server/capacity-format");
9
+ const timings_1 = require("../server/timings");
10
+ const wait_reason_1 = require("../server/wait-reason");
11
+ Object.defineProperty(exports, "describeWait", { enumerable: true, get: function () { return wait_reason_1.describeWait; } });
12
+ const server_client_1 = require("./server-client");
13
+ // What a run does, in a few words: its tasks, or the services it keeps up.
14
+ function describeWork(run) {
15
+ if (run.up) {
16
+ return `${(0, server_client_1.printable)(run.up.name)}: up ${run.up.services.map(server_client_1.printable).join(', ')}`;
17
+ }
18
+ return run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'by labels';
19
+ }
20
+ exports.describeWork = describeWork;
21
+ function ownerOf(run) {
22
+ return run.owner ? ` of ${(0, server_client_1.printable)(run.owner.sub)}` : '';
23
+ }
24
+ function describeMeter(name, meter) {
25
+ const full = meter.limit !== undefined && meter.used >= meter.limit;
26
+ const count = `${meter.used}${meter.limit !== undefined ? ` of ${meter.limit}` : ''}`;
27
+ return ` ${name.padEnd(9)} ${count.padEnd(9)} ${meter.runs.length > 0 ? meter.runs.map(server_client_1.printable).join(', ') : '-'}${full ? colors_1.default.yellow(' full') : ''}`;
28
+ }
29
+ // A service run on one line, then a line per endpoint: when it ends, when it sleeps.
30
+ function describeServiceRun(run) {
31
+ const times = [
32
+ run.state === 'up' || run.state === 'asleep' ? (run.expiresAt ? `until ${run.expiresAt}` : '') : '',
33
+ run.state === 'up' && run.sleepsAt ? `sleeps at ${run.sleepsAt}` : '',
34
+ run.wakes ? `woken ${run.wakes}×` : '',
35
+ ].filter(Boolean);
36
+ return [
37
+ ` ${(0, server_client_1.printable)(run.id)} ${(0, server_client_1.printable)(run.state)} ${describeWork(run)}${ownerOf(run)}${times.length > 0 ? ` ${(0, server_client_1.printable)(times.join(', '))}` : ''}`,
38
+ ...(run.up?.endpoints ?? []).map((endpoint) => ` ${(0, server_client_1.printable)(endpoint.service)}:${endpoint.port}${endpoint.url ? ` ${(0, server_client_1.printable)(endpoint.url)}` : ''}`),
39
+ ];
40
+ }
41
+ exports.describeServiceRun = describeServiceRun;
42
+ // What `hammerkit remote status` prints: the places, the queue, the caller's limits.
43
+ function describeOverview(server, overview, now = Date.now()) {
44
+ const lines = [
45
+ `${(0, server_client_1.printable)(server)}: signed in as ${(0, server_client_1.printable)(overview.you.sub)}${overview.you.observe ? ', you observe every run' : ''}`,
46
+ `places: ${overview.places.used} of ${overview.places.max} taken (runs.maxConcurrent)`,
47
+ ...overview.places.runs.map((run) => ` ${(0, server_client_1.printable)(run.id)} ${describeWork(run)}${ownerOf(run)}${run.startedAt ? ` since ${(0, server_client_1.printable)(run.startedAt)}, ${(0, timings_1.roughly)((0, timings_1.ranMs)(run, now) ?? 0)}` : ''}`),
48
+ ];
49
+ if (overview.places.others > 0) {
50
+ lines.push(` ${overview.places.others} ${overview.places.others === 1 ? 'run' : 'runs'} of others`);
51
+ }
52
+ // a place a run holds that is not running: a service run waking up, or a place not given back
53
+ const unaccounted = overview.places.used - overview.places.runs.length - overview.places.others;
54
+ if (unaccounted > 0) {
55
+ lines.push(` ${unaccounted} taken by no running run`);
56
+ }
57
+ lines.push(`queue: ${overview.queue.length === 0 ? 'empty' : overview.queue.length}`);
58
+ for (const entry of overview.queue) {
59
+ const waited = entry.run ? (0, timings_1.waitedMs)(entry.run, now) : undefined;
60
+ const what = entry.run
61
+ ? `${(0, server_client_1.printable)(entry.run.id)} ${describeWork(entry.run)}${ownerOf(entry.run)}${waited !== undefined ? ` waiting ${(0, timings_1.roughly)(waited)}` : ''}`
62
+ : 'a run of somebody else';
63
+ lines.push(` ${entry.position}. ${what} waits: ${(0, server_client_1.printable)((0, wait_reason_1.describeWait)(entry.waitsFor, entry.mine))}`);
64
+ }
65
+ lines.push(`waiting for an approval: ${overview.approvals.waiting} of at most ${overview.approvals.max}`);
66
+ for (const run of overview.approvals.runs) {
67
+ const accounts = (run.approvals ?? [])
68
+ .filter((approval) => !approval.approvedBy)
69
+ .map((approval) => approval.account);
70
+ lines.push(` ${(0, server_client_1.printable)(run.id)} ${describeWork(run)}${ownerOf(run)} needs ${accounts
71
+ .map(server_client_1.printable)
72
+ .join(', ')} until ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`);
73
+ }
74
+ lines.push(`service runs: ${overview.services.open}${overview.services.max !== undefined ? ` of ${overview.services.max}` : ''} places taken on the server`);
75
+ for (const run of overview.services.runs) {
76
+ lines.push(...describeServiceRun(run));
77
+ }
78
+ lines.push('your limits:', describeMeter('running', overview.limits.running), describeMeter('queued', overview.limits.queued), describeMeter('approval', overview.limits.waiting), describeMeter('services', overview.limits.services), ...(overview.limits.asleep ? [describeMeter('asleep', overview.limits.asleep)] : []));
79
+ return lines;
80
+ }
81
+ exports.describeOverview = describeOverview;
82
+ // The machines of the server and what runs take of them, for `remote capacity`; `short` for
83
+ // the two lines of `remote status`.
84
+ function describeCapacity(capacity, short = false) {
85
+ const { machines, runs } = capacity;
86
+ const resources = (value) => value ? `${(0, capacity_format_1.formatCpu)(value.cpu)} CPU, ${(0, capacity_format_1.formatMemory)(value.memory)}` : '?';
87
+ const kind = capacity.backend === 'kubernetes' ? (machines?.count === 1 ? 'node' : 'nodes') : 'machine';
88
+ const lines = [
89
+ `capacity: ${machines
90
+ ? `${machines.count} ${kind} with ${resources(machines.allocatable)}`
91
+ : 'what the machines have is not known'}${machines?.used ? `, ${resources(machines.used)} in use` : ''}${machines?.load ? `, load ${machines.load.map((load) => load.toFixed(2)).join(' ')}` : ''}`,
92
+ ` runs: ${runs.pods !== undefined ? `${(0, capacity_format_1.plural)(runs.pods, 'pod')}, ` : ''}${runs.containers !== undefined ? `${runs.containers} containers, ` : ''}${capacity.backend === 'kubernetes' ? `requested ${resources(runs.requested)}` : ''}${runs.used ? `${capacity.backend === 'kubernetes' ? ', ' : ''}in use ${resources(runs.used)}` : ''}`,
93
+ ];
94
+ if (short) {
95
+ return lines;
96
+ }
97
+ for (const group of capacity.groups) {
98
+ lines.push(` ${(0, server_client_1.printable)(group.name)} ${group.kind === 'service-run' ? 'service run' : 'caller'}${group.owner ? ` of ${(0, server_client_1.printable)(group.owner.sub)}` : ''}${group.run ? ` ${(0, server_client_1.printable)(group.run)}` : ''} ${(0, capacity_format_1.plural)(group.pods ?? 0, 'pod')} requested ${resources(group.requested)}${group.quota ? ` of ${resources(group.quota)}` : ''}${group.used ? ` in use ${resources(group.used)}` : ''}`);
99
+ }
100
+ for (const problem of capacity.problems) {
101
+ lines.push(colors_1.default.yellow(` not shown: ${(0, server_client_1.printable)(problem)}`));
102
+ }
103
+ return lines;
104
+ }
105
+ exports.describeCapacity = describeCapacity;
106
+ // What the past runs of each task took, for `remote tasks`.
107
+ function describeTaskStats(stats) {
108
+ const span = (ms) => (ms === undefined ? '-' : (0, timings_1.roughly)(ms));
109
+ return stats.map((entry) => `${(0, server_client_1.printable)(entry.task)} ${(0, server_client_1.printable)(entry.repo)} last ${(0, server_client_1.printable)(entry.last.status)} ${(0, server_client_1.printable)(entry.last.at)}${entry.last.durationMs !== undefined ? ` took ${(0, timings_1.roughly)(entry.last.durationMs)}` : ''} ${(0, capacity_format_1.plural)(entry.runs, 'run')}${entry.failed ? `, ${entry.failed} failed` : ''}${entry.cached ? `, ${entry.cached} cached` : ''} median ${span(entry.medianMs)}, 90% ${span(entry.p90Ms)}, waited ${span(entry.medianWaitMs)}`);
110
+ }
111
+ exports.describeTaskStats = describeTaskStats;
112
+ //# sourceMappingURL=remote-status.js.map