@wpelevator/tug 0.4.0 → 0.5.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/README.md CHANGED
@@ -6,15 +6,15 @@ Deploy Docker Compose projects to one or more ordinary Linux VPSes through a sma
6
6
 
7
7
  ## Features
8
8
 
9
- - Control API with bearer-token auth, scoped `read`/`deploy`/`admin` API tokens for CI and teammates, an audit trail naming who acted, and SQLite metadata.
10
- - Nodes join over SSH (`node-install`) or enroll themselves with a single `docker run` (`node-enroll`), with per-node agent and registry tokens. Agents only dial out, so nodes behind NAT work.
11
- - Agents pull desired state from control, apply a generated per-node Compose file, run TCP/HTTP/Docker health checks and report status. An outbound WebSocket wakes them as soon as control queues work and carries live `exec`, followed logs, `ps` and `inspect` sessions, with long-poll and periodic-poll fallbacks for ordinary work.
9
+ - Control API with bearer-token auth, scoped `read`/`deploy`/`admin` API tokens for CI and teammates, an audit trail naming who acted, and SQLite metadata through Node's built-in driver.
10
+ - Nodes join over SSH (`node-install`) or enroll themselves with a single `docker run` (`node-enroll`), with one agent token per node. Agents only dial out, so nodes behind NAT work.
11
+ - Agents pull desired state from control, reconcile containers through the Docker Engine API, run TCP/HTTP/Docker health checks and report status. An outbound WebSocket wakes them as soon as control queues work and carries live `exec`, followed logs, `ps` and `inspect` sessions, with long-poll and periodic-poll fallbacks for ordinary work.
12
12
  - `x-tug` placement of services and volumes on nodes, with immutable desired-state records per deployment.
13
- - Build application images on control from uploaded source, or locally with `deploy --build-local`. Agents download saved images directly from control by default; a bring-your-own registry is optional.
13
+ - Build application images on control from uploaded source, or locally with `deploy --build-local`. Agents download saved images directly from control. Published images are pinned to registry digests at deployment creation.
14
14
  - Secrets encrypted at rest, released only to the nodes whose services reference them, with targeted service recreation when only a secret changes.
15
15
  - Deployment convergence, per-project operation locks, cancellation, rollback, per-service rollout events and log fetching through the agents.
16
16
  - Opt-in zero-downtime rolling rollouts for ingress-fronted services (`x-tug.rollout`) and automatic rollback of failed deployments (`x-tug.rollback: auto`). See [Compose extensions](docs/compose.md#rolling-rollouts).
17
- - Per-node registry pull tokens enforced by an htpasswd-backed registry you provide, with rotation and revocation.
17
+ - Private published images use encrypted project credentials for digest resolution and Engine API pulls.
18
18
  - Optional WireGuard mesh between nodes, health-aware cross-node service names and a managed Caddy ingress with automatic HTTPS, each opt-in. See [Networking](docs/networking.md).
19
19
  - Private-by-default published ports: generated Compose binds them to the overlay IP, loopback or the node address, never every interface unless you write `0.0.0.0`. See [Networking](docs/networking.md#published-ports-private-by-default).
20
20
  - Control-plane backup and restore, and `tug doctor` diagnostics.
@@ -31,7 +31,6 @@ Deploy Docker Compose projects to one or more ordinary Linux VPSes through a sma
31
31
  | Control API (role-checked) |
32
32
  | SQLite: desired states, secrets |
33
33
  | image builds, saved images |
34
- | registry (optional) |
35
34
  +-------------------------------------+
36
35
  ^ ^
37
36
  | agents dial out: work, status,
@@ -72,7 +71,7 @@ The CLI installs the matching `ghcr.io/wpelevator/tug:<version>` image on your h
72
71
 
73
72
  - [Architecture](docs/architecture.md): components, reconciliation, workflow diagrams, secrets and runtime paths.
74
73
  - [Compose extensions](docs/compose.md): `x-tug` placement, health checks, volumes and limitations.
75
- - [Control plane](docs/control-plane.md): install, TLS, registry credentials, backup and restore.
74
+ - [Control plane](docs/control-plane.md): install, TLS, image delivery, backup and restore.
76
75
  - [Backups](docs/backups.md): interim WordPress backup sidecar with restic, per-node and NFS variants, and a restore drill until `x-tug.backup` (M11).
77
76
  - [Access control](docs/access-control.md): scoped API tokens, roles, audit trail and a GitHub Actions deploy example.
78
77
  - [Networking](docs/networking.md): WireGuard mesh, service DNS, ingress and TLS, firewall notes.
@@ -80,7 +79,7 @@ The CLI installs the matching `ghcr.io/wpelevator/tug:<version>` image on your h
80
79
  - [CLI reference](docs/cli.md): every `tug` command and option.
81
80
  - [Configuration](docs/configuration.md): CLI, control and agent environment variables.
82
81
  - [Development](docs/development.md): local setup, CI, Compose stack, host mode and the integration test.
83
- - [Testing](docs/testing.md): test layers, the numbers rollouts are held to, and the backlog of mocked tests.
82
+ - [Testing](docs/testing.md): test layers, rollout metrics, mocked test backlog, and the [Caddy/PHP and static-file benchmark](docs/testing.md#caddy-proxy-overhead-benchmark).
84
83
  - [Observability](docs/observability.md): what can be observed today, and the proposed logs, metrics and alerting.
85
84
  - [Releases](docs/releases.md): changesets, npm and Docker image publishing.
86
85
  - [Standalone binary](docs/standalone-binary.md): proposed Bun-compiled CLI binaries and Homebrew distribution.
package/dist/agent.d.ts CHANGED
@@ -93,8 +93,6 @@ interface Creds {
93
93
  agentToken: string;
94
94
  nodeName?: string;
95
95
  controlUrl?: string;
96
- registry?: string | null;
97
- registryToken?: string | null;
98
96
  }
99
97
  export declare function loadOrEnroll(): Promise<Creds>;
100
98
  export declare function main(): Promise<void>;
package/dist/agent.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs/promises';
3
+ import { imageLocation } from "./registry.js";
3
4
  import { createReadStream, createWriteStream } from 'node:fs';
4
5
  import { Readable } from 'node:stream';
5
6
  import { pipeline } from 'node:stream/promises';
6
7
  import { validateImageArchive, verifyDownload } from "./artifacts.js";
7
8
  import path from 'node:path';
8
- import { spawn, spawnSync } from 'node:child_process';
9
+ import { spawnSync } from 'node:child_process';
9
10
  import net from 'node:net';
10
11
  import dns from 'node:dns/promises';
11
12
  import { WebSocket } from 'ws';
@@ -837,7 +838,7 @@ export async function reconcile(desiredState, ctx = defaultContext(), endpoints
837
838
  const sec = await api(ctx, `/agent/v1/secrets?project=${encodeURIComponent(desiredState.project)}&services=${encodeURIComponent(serviceNames.join(','))}`);
838
839
  const currentSecretVersions = serviceSecretVersions(desiredState.compose, serviceNames, sec.versions || {});
839
840
  const db = await agentDb(ctx.stateDir);
840
- const previousRow = await db.get('SELECT version, revision FROM applied_state WHERE project = ?', desiredState.project);
841
+ const previousRow = db.get('SELECT version, revision FROM applied_state WHERE project = ?', desiredState.project);
841
842
  const rolling = serviceNames.filter((service) => isRolling(desiredState.compose.services[service]));
842
843
  const rollingSet = new Set(rolling);
843
844
  const mainServices = serviceNames.filter((service) => !rollingSet.has(service));
@@ -882,7 +883,7 @@ export async function reconcile(desiredState, ctx = defaultContext(), endpoints
882
883
  .digest('hex')
883
884
  .slice(0, 16);
884
885
  const knownEvents = mainServices.length
885
- ? await db.get('SELECT fingerprint, phase FROM event_state WHERE project = ?', desiredState.project)
886
+ ? db.get('SELECT fingerprint, phase FROM event_state WHERE project = ?', desiredState.project)
886
887
  : undefined;
887
888
  const announce = knownEvents?.fingerprint !== eventFingerprint;
888
889
  const wasFailed = !announce && knownEvents?.phase === 'failed';
@@ -894,7 +895,7 @@ export async function reconcile(desiredState, ctx = defaultContext(), endpoints
894
895
  if (artifact)
895
896
  await ensureArtifact(ctx, artifact);
896
897
  },
897
- pullImage: (service) => ctx.docker.pullImage(plan.specs[service].body.Image),
898
+ pullImage: (service) => ctx.docker.pullImage(plan.specs[service].body.Image, sec.registryAuth?.[imageLocation(plan.specs[service].body.Image).host]),
898
899
  emit,
899
900
  };
900
901
  let failing;
@@ -938,13 +939,13 @@ export async function reconcile(desiredState, ctx = defaultContext(), endpoints
938
939
  },
939
940
  });
940
941
  if (mainServices.length && (announce || wasFailed))
941
- await markEvents('serving');
942
+ markEvents('serving');
942
943
  }
943
944
  catch (e) {
944
945
  if (mainServices.length && !wasFailed) {
945
946
  // Announce a failure once; retries of the same failure stay quiet until it recovers or the inputs change.
946
947
  await emit(failing ?? mainServices.join(', '), 'failed', errMessage(e), 'error');
947
- await markEvents('failed');
948
+ markEvents('failed');
948
949
  }
949
950
  // Rolling services are untouched by a failure here, so keep reporting their current revision as serving.
950
951
  if (!rolling.length)
@@ -974,7 +975,7 @@ export async function reconcile(desiredState, ctx = defaultContext(), endpoints
974
975
  for (const service of serviceNames)
975
976
  serviceStates[service].rolloutState = 'serving';
976
977
  }
977
- await db.run(`INSERT INTO applied_state(project, version, revision, applied_at, service_states_json, secret_versions_json)
978
+ db.run(`INSERT INTO applied_state(project, version, revision, applied_at, service_states_json, secret_versions_json)
978
979
  VALUES(?,?,?,?,?,?)
979
980
  ON CONFLICT(project) DO UPDATE SET version=excluded.version, revision=excluded.revision, applied_at=excluded.applied_at,
980
981
  service_states_json=excluded.service_states_json, secret_versions_json=excluded.secret_versions_json`, desiredState.project, desiredState.version, desiredState.revision || null, now(), JSON.stringify(serviceStates), JSON.stringify(currentSecretVersions));
@@ -1122,19 +1123,6 @@ async function readJoinKey() {
1122
1123
  }
1123
1124
  return null;
1124
1125
  }
1125
- function dockerLogin(registry, user, password) {
1126
- return new Promise((resolve, reject) => {
1127
- const p = spawn('docker', ['login', registry, '-u', user, '--password-stdin'], {
1128
- stdio: ['pipe', 'ignore', 'pipe'],
1129
- });
1130
- let err = '';
1131
- p.stderr.on('data', (d) => (err += d));
1132
- p.on('error', reject);
1133
- p.on('close', (code) => code === 0 ? resolve() : reject(new Error(err.trim() || `exit ${code}`)));
1134
- p.stdin.write(password);
1135
- p.stdin.end();
1136
- });
1137
- }
1138
1126
  async function enroll(joinKey) {
1139
1127
  let decoded;
1140
1128
  try {
@@ -1184,8 +1172,6 @@ async function enroll(joinKey) {
1184
1172
  agentToken: out.agentToken,
1185
1173
  nodeName: out.node?.name || NODE_NAME,
1186
1174
  controlUrl,
1187
- registry: out.registry || null,
1188
- registryToken: out.registryToken || null,
1189
1175
  };
1190
1176
  await saveCreds(creds);
1191
1177
  if (wgPublicKey)
@@ -1209,14 +1195,6 @@ export async function loadOrEnroll() {
1209
1195
  NODE_NAME = creds.nodeName || NODE_NAME;
1210
1196
  if (!CONTROL_URL_FROM_ENV && creds.controlUrl)
1211
1197
  CONTROL_URL = creds.controlUrl;
1212
- if (APPLY && creds.registry && creds.registryToken) {
1213
- try {
1214
- await dockerLogin(creds.registry, NODE_NAME, creds.registryToken);
1215
- }
1216
- catch (e) {
1217
- console.error(`registry login failed (pulls from ${creds.registry} may fail): ${errMessage(e)}`);
1218
- }
1219
- }
1220
1198
  return creds;
1221
1199
  }
1222
1200
  export async function main() {
package/dist/build.d.ts CHANGED
@@ -12,11 +12,10 @@ export interface BuildSpec {
12
12
  }
13
13
  export declare function run(cmd: string, args: string[], opts?: RunOptions): Promise<RunResult>;
14
14
  export declare function unpackContext(tarball: string, dest: string): Promise<void>;
15
- export interface BuildAndPushOptions {
15
+ export interface BuildImageOptions {
16
16
  contextDir: string;
17
17
  service: string;
18
18
  buildSpec: string | BuildSpec;
19
19
  imageRef: string;
20
- push?: boolean;
21
20
  }
22
- export declare function buildAndPush({ contextDir, service, buildSpec, imageRef, push, }: BuildAndPushOptions): Promise<void>;
21
+ export declare function buildImage({ contextDir, service, buildSpec, imageRef, }: BuildImageOptions): Promise<void>;
package/dist/build.js CHANGED
@@ -33,7 +33,7 @@ export async function unpackContext(tarball, dest) {
33
33
  });
34
34
  await tar.x({ file: tarball, cwd: dest, strict: true });
35
35
  }
36
- export async function buildAndPush({ contextDir, service, buildSpec, imageRef, push = true, }) {
36
+ export async function buildImage({ contextDir, service, buildSpec, imageRef, }) {
37
37
  const buildPath = typeof buildSpec === 'string' ? buildSpec : buildSpec?.context || '.';
38
38
  const dockerfilePath = typeof buildSpec === 'object' && buildSpec.dockerfile
39
39
  ? path.resolve(contextDir, buildPath, buildSpec.dockerfile)
@@ -48,6 +48,4 @@ export async function buildAndPush({ contextDir, service, buildSpec, imageRef, p
48
48
  if (!(await exists(fullContext)))
49
49
  throw new Error(`build context not found for ${service}: ${buildPath}`);
50
50
  await run('docker', ['build', ...dockerfile, '-t', imageRef, fullContext]);
51
- if (push)
52
- await run('docker', ['push', imageRef]);
53
51
  }
@@ -1,8 +1,8 @@
1
- import { buildAndPush } from './build.ts';
1
+ import { buildImage } from './build.ts';
2
2
  import { saveImageArchive } from './artifacts.ts';
3
3
  import type { ComposeFile, LocalImageUpload } from './protocol.ts';
4
4
  export interface LocalBuildDependencies {
5
- build: typeof buildAndPush;
5
+ build: typeof buildImage;
6
6
  save: typeof saveImageArchive;
7
7
  nonce: () => string;
8
8
  remove: (imageRef: string) => Promise<void>;
package/dist/cli-build.js CHANGED
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import * as tar from 'tar';
5
- import { buildAndPush, run } from "./build.js";
5
+ import { buildImage, run } from "./build.js";
6
6
  import { saveImageArchive } from "./artifacts.js";
7
7
  /** Credential exclusions are mandatory and cannot be re-included by source ignore rules. */
8
8
  export function sourceArchiveFilter(entry, configPaths, ignores) {
@@ -18,7 +18,7 @@ export async function createSourceArchive(contextDir, archive, configFiles) {
18
18
  const configPaths = configFiles
19
19
  .map((file) => path.relative(contextDir, file).split(path.sep).join('/'))
20
20
  .filter((file) => file !== '..' && !file.startsWith('../') && !path.isAbsolute(file));
21
- const ignores = ['node_modules', 'data', 'registry-data'];
21
+ const ignores = ['node_modules', 'data'];
22
22
  try {
23
23
  ignores.push(...(await fs.readFile(path.join(contextDir, '.dockerignore'), 'utf8'))
24
24
  .split(/\r?\n/)
@@ -38,7 +38,7 @@ export async function createSourceArchive(contextDir, archive, configFiles) {
38
38
  }
39
39
  /** Build only application build services; callers own temporary files and upload transport. */
40
40
  export async function prepareLocalImages(compose, contextDir, directory, dependencies = {
41
- build: buildAndPush,
41
+ build: buildImage,
42
42
  save: saveImageArchive,
43
43
  nonce: randomUUID,
44
44
  remove: async (imageRef) => {
@@ -58,7 +58,6 @@ export async function prepareLocalImages(compose, contextDir, directory, depende
58
58
  service,
59
59
  buildSpec: spec.build,
60
60
  imageRef,
61
- push: false,
62
61
  });
63
62
  const archive = await dependencies.save(imageRef, path.join(directory, `${field}.tar.gz`));
64
63
  images.push({ service, imageRef, ...archive, field });
package/dist/cli.js CHANGED
@@ -201,12 +201,6 @@ program
201
201
  });
202
202
  console.log(JSON.stringify(r, null, 2));
203
203
  });
204
- program
205
- .command('node-rotate-registry-token <name>')
206
- .description('issue a new registry pull token for a node, invalidating the old one')
207
- .action(async (name) => {
208
- console.log(JSON.stringify(await request('POST', `/api/v1/nodes/${encodeURIComponent(name)}/rotate-registry-token`, {}), null, 2));
209
- });
210
204
  program
211
205
  .command('node-rotate-wg-key <name>')
212
206
  .description('generate a new WireGuard keypair on the node; peers pick up the new public key on their next poll')
@@ -234,12 +228,6 @@ program
234
228
  enabled: !o.disable,
235
229
  }), null, 2));
236
230
  });
237
- program
238
- .command('node-revoke-registry-token <name>')
239
- .description("revoke a node's registry pull token, blocking future pulls until rotated")
240
- .action(async (name) => {
241
- console.log(JSON.stringify(await request('POST', `/api/v1/nodes/${encodeURIComponent(name)}/revoke-registry-token`, {}), null, 2));
242
- });
243
231
  program
244
232
  .command('scale <service> <replicas>')
245
233
  .description('scale a service to N replicas across matching nodes')
@@ -263,9 +251,6 @@ program
263
251
  .option('--token <token>', 'operator token')
264
252
  .option('--port <port>', 'published API port', '8080')
265
253
  .option('--mesh', 'enable the WireGuard mesh on control (TUG_MESH=true); install nodes with --mesh too')
266
- .option('--registry <host>', 'registry host used in desired images; bring your own registry, Tug does not provision one')
267
- .option('--registry-auth-path <path>', 'container path to an htpasswd file tug-control should manage credentials in, if your registry uses one')
268
- .option('--registry-container <name>', "docker container name to restart when a node's registry credentials change, if your registry uses an htpasswd file")
269
254
  .action(async (sshTarget, o) => {
270
255
  const operatorToken = o.token || process.env.TUG_TOKEN || token('tug_op');
271
256
  if (o.installDocker)
@@ -274,16 +259,12 @@ program
274
259
  const image = resolveImage(o.image, o.buildLocalImage);
275
260
  if (o.buildLocalImage)
276
261
  await transferTugImage(sshTarget, image);
277
- const registryHost = o.registry || '';
278
262
  const yamlValue = (value) => JSON.stringify(value);
279
263
  const compose = await loadTemplate(templatesDir, 'control-compose.yml', {
280
264
  IMAGE: yamlValue(image),
281
265
  OPERATOR_TOKEN: yamlValue(operatorToken),
282
- REGISTRY_HOST: yamlValue(registryHost),
283
266
  PORT: o.port,
284
267
  MESH: yamlValue(o.mesh ? 'true' : 'false'),
285
- REGISTRY_AUTH_PATH: yamlValue(o.registryAuthPath || ''),
286
- REGISTRY_CONTAINER: yamlValue(o.registryContainer || ''),
287
268
  });
288
269
  const remote = await loadTemplate(templatesDir, 'control-install.sh', {
289
270
  COMPOSE: compose,
@@ -295,7 +276,6 @@ program
295
276
  sshTarget,
296
277
  operatorToken,
297
278
  url: `http://${remoteHost(sshTarget)}:${o.port}`,
298
- registry: registryHost,
299
279
  }, null, 2));
300
280
  });
301
281
  program
@@ -306,7 +286,6 @@ program
306
286
  .option('--public-address <addr>')
307
287
  .option('--label <label>', 'node label (repeatable)', collect, [])
308
288
  .option('--agent-control-url <url>', 'control URL reachable by the agent (default: resolved operator control URL)')
309
- .option('--registry <host>', 'registry host for docker login')
310
289
  .option('--http-only-dev', 'allow plaintext HTTP control URL for local/private development')
311
290
  .option('--mesh', 'join the WireGuard mesh (agent runs with host networking and NET_ADMIN; control needs TUG_MESH=true)')
312
291
  .option('--wg-port <port>', 'WireGuard UDP port checked by preflight', '51820')
@@ -338,17 +317,13 @@ program
338
317
  NODE_NAME: o.name,
339
318
  AGENT_TOKEN: registered.agentToken,
340
319
  })) + (o.mesh ? 'TUG_MESH=true\n' : '');
341
- const dockerLogin = o.registry
342
- ? `printf %s ${sh(registered.registryToken)} | docker login ${sh(o.registry)} --username ${sh(o.name)} --password-stdin`
343
- : 'true';
344
320
  const remote = await loadTemplate(templatesDir, 'node-install.sh', {
345
321
  ENV: env,
346
- DOCKER_LOGIN: dockerLogin,
347
322
  IMAGE_SH: sh(image),
348
323
  RUN_FLAGS: `${o.mesh ? MESH_RUN_FLAGS : HOST_NETWORK_FLAG} `,
349
324
  });
350
325
  runLocal('ssh', ['--', sshTarget, remote]);
351
- console.log(JSON.stringify({ ok: true, node: registered.node, registryToken: registered.registryToken }, null, 2));
326
+ console.log(JSON.stringify({ ok: true, node: registered.node }, null, 2));
352
327
  });
353
328
  program
354
329
  .command('node-enroll')
@@ -463,7 +438,6 @@ program
463
438
  .option('-f, --file <file>', 'compose file', 'docker-compose.yml')
464
439
  .option('--version <version>', 'display version (default: git-<Compose hash>)')
465
440
  .option('--build-local', 'build application images locally and upload them without source')
466
- .option('--no-build-upload', 'deploy published images without uploading source; conflicts with --build-local')
467
441
  .option('--no-wait', 'return after desired state is created')
468
442
  .option('--timeout <seconds>', 'wait timeout in seconds', '300')
469
443
  .action(async (o) => {
@@ -488,8 +462,7 @@ program
488
462
  await fs.rm(directory, { recursive: true, force: true });
489
463
  }
490
464
  }
491
- else if (o.buildUpload &&
492
- Object.values(compose.services).some((service) => service.build)) {
465
+ else if (Object.values(compose.services).some((service) => service.build)) {
493
466
  const tarball = await makeContext();
494
467
  try {
495
468
  const form = new FormData();
@@ -705,10 +678,6 @@ program.hook('preAction', async (_root, command) => {
705
678
  if (options[key] !== undefined)
706
679
  integerOption(String(options[key]), '--' + key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()), min, max);
707
680
  }
708
- if (options.buildLocal && options.buildUpload === false)
709
- throw new Error('--build-local and --no-build-upload cannot be combined');
710
- if ((options.registryAuthPath || options.registryContainer) && !options.registry)
711
- throw new Error('--registry-auth-path and --registry-container require --registry');
712
681
  if (command.name() === 'control-restore' && !options.confirm)
713
682
  throw new Error('control-restore requires --confirm');
714
683
  const positional = command.name() === 'exec' ? command.args.slice(0, 1) : command.args;
package/dist/compose.d.ts CHANGED
@@ -39,7 +39,6 @@ export interface BuildDesiredStateOptions {
39
39
  project: string;
40
40
  version: string;
41
41
  compose: ComposeFile;
42
- registry: string;
43
42
  revision?: string;
44
43
  defaultNode?: string | null;
45
44
  nodes?: Array<{
@@ -47,7 +46,7 @@ export interface BuildDesiredStateOptions {
47
46
  labels?: string[];
48
47
  }>;
49
48
  }
50
- export declare function buildDesiredState({ project, version, compose, registry, revision, defaultNode, nodes, }: BuildDesiredStateOptions): DesiredState;
49
+ export declare function buildDesiredState({ project, version, compose, revision, defaultNode, nodes, }: BuildDesiredStateOptions): DesiredState;
51
50
  /** `extra_hosts` entries (`name:ip`) for services that run on other nodes, from health-aware endpoints. */
52
51
  export declare function crossNodeHosts(project: string, endpoints: Record<string, ServiceEndpoint[]>, local: Set<string>): string[];
53
52
  /**
package/dist/compose.js CHANGED
@@ -305,7 +305,7 @@ export function validateCompose(compose, nodes = []) {
305
305
  if (errors.length)
306
306
  throw new Error(errors.join('; '));
307
307
  }
308
- export function buildDesiredState({ project, version, compose, registry, revision, defaultNode = null, nodes, }) {
308
+ export function buildDesiredState({ project, version, compose, revision, defaultNode = null, nodes, }) {
309
309
  const services = {};
310
310
  for (const [name, src] of Object.entries(compose.services || {})) {
311
311
  const lp = src['x-tug'] || {};
@@ -323,9 +323,7 @@ export function buildDesiredState({ project, version, compose, registry, revisio
323
323
  }
324
324
  placement = { ...placement, nodes: selected.map((node) => node.name) };
325
325
  }
326
- const image = src.build
327
- ? `${registry ? `${registry}/` : 'tug-build/'}${project}/${name}:${revision || version}`
328
- : src.image;
326
+ const image = src.build ? `tug-build/${project}/${name}:${revision || version}` : src.image;
329
327
  services[name] = {
330
328
  image,
331
329
  build: src.build || null,
package/dist/control.d.ts CHANGED
@@ -1,18 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import express from 'express';
3
+ import { resolveImage } from './registry.ts';
3
4
  import { saveImageArchive, inspectImage, validateImageArchive } from './artifacts.ts';
4
5
  import https from 'node:https';
5
6
  import http from 'node:http';
6
7
  import { openDb } from './db.ts';
7
8
  import type { NodeRow } from './db.ts';
8
- import { buildAndPush, run as runCommand } from './build.ts';
9
+ import { buildImage, run as runCommand } from './build.ts';
9
10
  declare module 'express-serve-static-core' {
10
11
  interface Request {
11
12
  node?: NodeRow;
12
13
  }
13
14
  }
14
15
  export interface ControlImageOperations {
15
- build: typeof buildAndPush;
16
+ resolve: typeof resolveImage;
17
+ build: typeof buildImage;
16
18
  save: typeof saveImageArchive;
17
19
  inspect: typeof inspectImage;
18
20
  validate: typeof validateImageArchive;