vyft 0.2.0-alpha → 0.3.0-alpha
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 +5 -16
- package/dist/cli.js +170 -9
- package/dist/context.d.ts +39 -0
- package/dist/context.js +101 -0
- package/dist/docker.d.ts +14 -9
- package/dist/docker.js +145 -317
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/init.js +19 -2
- package/dist/interpolate.d.ts +11 -0
- package/dist/interpolate.js +11 -0
- package/dist/proxy.d.ts +16 -0
- package/dist/proxy.js +0 -0
- package/dist/resource.d.ts +97 -1
- package/dist/resource.js +11 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/services/index.d.ts +24 -0
- package/dist/services/index.js +20 -0
- package/dist/services/minio.d.ts +36 -0
- package/dist/services/minio.js +53 -0
- package/dist/services/mongo.d.ts +28 -0
- package/dist/services/mongo.js +45 -0
- package/dist/services/mysql.d.ts +28 -0
- package/dist/services/mysql.js +44 -0
- package/dist/services/nats.d.ts +26 -0
- package/dist/services/nats.js +38 -0
- package/dist/services/postgres.d.ts +28 -0
- package/dist/services/postgres.js +45 -0
- package/dist/services/rabbitmq.d.ts +28 -0
- package/dist/services/rabbitmq.js +44 -0
- package/dist/services/redis.d.ts +28 -0
- package/dist/services/redis.js +49 -0
- package/dist/services/storage.d.ts +39 -0
- package/dist/services/storage.js +94 -0
- package/dist/swarm/factories.d.ts +9 -2
- package/dist/swarm/factories.js +9 -32
- package/dist/swarm/index.d.ts +11 -2
- package/dist/swarm/proxy.d.ts +24 -0
- package/dist/swarm/proxy.js +339 -0
- package/dist/swarm/types.d.ts +11 -21
- package/dist/symbols.d.ts +5 -0
- package/dist/symbols.js +1 -0
- package/package.json +2 -5
- package/templates/fullstack/vyft.config.ts +13 -28
package/dist/docker.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
1
|
import { randomBytes } from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { PassThrough } from "node:stream";
|
|
@@ -8,18 +7,7 @@ import tar from "tar-fs";
|
|
|
8
7
|
import { buildStatic } from "./build.js";
|
|
9
8
|
import { logger as defaultLogger } from "./logger.js";
|
|
10
9
|
import { isInterpolation, isReference } from "./resource.js";
|
|
11
|
-
|
|
12
|
-
function resolveDockerHost() {
|
|
13
|
-
if (process.env.DOCKER_HOST)
|
|
14
|
-
return undefined;
|
|
15
|
-
try {
|
|
16
|
-
const host = execSync("docker context inspect --format '{{.Endpoints.docker.Host}}'", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
17
|
-
if (host && host !== `unix:///var/run/docker.sock`)
|
|
18
|
-
return host;
|
|
19
|
-
}
|
|
20
|
-
catch { }
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
10
|
+
import { CaddyProxy } from "./swarm/proxy.js";
|
|
23
11
|
function secretMount(id, name) {
|
|
24
12
|
return {
|
|
25
13
|
SecretID: id,
|
|
@@ -67,18 +55,6 @@ export function parseRoute(route) {
|
|
|
67
55
|
path: route.slice(slashIndex),
|
|
68
56
|
};
|
|
69
57
|
}
|
|
70
|
-
export function buildCaddyRoute(resourceId, route, handler) {
|
|
71
|
-
const { host, path } = parseRoute(route);
|
|
72
|
-
const match = { host: [host] };
|
|
73
|
-
if (path)
|
|
74
|
-
match.path = [path];
|
|
75
|
-
return {
|
|
76
|
-
"@id": `vyft-${resourceId}`,
|
|
77
|
-
match: [match],
|
|
78
|
-
terminal: true,
|
|
79
|
-
handle: [handler],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
58
|
export class DockerClient {
|
|
83
59
|
docker;
|
|
84
60
|
localDocker;
|
|
@@ -86,21 +62,23 @@ export class DockerClient {
|
|
|
86
62
|
project;
|
|
87
63
|
secretValues = new Map();
|
|
88
64
|
log;
|
|
65
|
+
proxy;
|
|
89
66
|
verbose = false;
|
|
90
|
-
constructor(project,
|
|
67
|
+
constructor(project, opts) {
|
|
91
68
|
this.localDocker = new Docker({ socketPath: "/var/run/docker.sock" });
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
process.env.DOCKER_HOST =
|
|
69
|
+
const host = opts?.host;
|
|
70
|
+
if (host)
|
|
71
|
+
process.env.DOCKER_HOST = host;
|
|
95
72
|
this.docker = new Docker();
|
|
96
|
-
this.isRemote = !!
|
|
73
|
+
this.isRemote = !!host;
|
|
97
74
|
this.project = project;
|
|
98
|
-
this.log = (parentLogger ?? defaultLogger).child({
|
|
75
|
+
this.log = (opts?.parentLogger ?? defaultLogger).child({
|
|
99
76
|
component: "docker",
|
|
100
77
|
project,
|
|
101
78
|
});
|
|
102
|
-
|
|
103
|
-
|
|
79
|
+
this.proxy = new CaddyProxy(this.docker, this.log);
|
|
80
|
+
if (host)
|
|
81
|
+
this.log.debug({ host }, "using remote endpoint");
|
|
104
82
|
}
|
|
105
83
|
async ensureNetwork(name, labels = {}) {
|
|
106
84
|
const networks = await this.docker.listNetworks({
|
|
@@ -125,247 +103,9 @@ export class DockerClient {
|
|
|
125
103
|
"vyft.managed": "true",
|
|
126
104
|
"vyft.project": this.project,
|
|
127
105
|
});
|
|
128
|
-
this.
|
|
129
|
-
const exists = await this.serviceExists(PROXY_SERVICE_NAME);
|
|
130
|
-
if (exists) {
|
|
131
|
-
this.log.debug("proxy already exists, verifying config structure");
|
|
132
|
-
try {
|
|
133
|
-
const raw = await this.caddyApiRequest("GET", "/config/");
|
|
134
|
-
const config = JSON.parse(raw);
|
|
135
|
-
if (!config?.apps?.http?.servers?.main) {
|
|
136
|
-
this.log.debug("proxy config missing servers.main, re-seeding");
|
|
137
|
-
await this.seedCaddyConfig();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
this.log.debug("proxy config check failed, re-seeding");
|
|
142
|
-
await this.seedCaddyConfig();
|
|
143
|
-
}
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
// Create shared proxy network
|
|
147
|
-
await this.ensureNetwork("vyft-network", { "vyft.infrastructure": "true" });
|
|
148
|
-
this.log.debug("creating proxy service");
|
|
149
|
-
await this.docker.createService({
|
|
150
|
-
Name: PROXY_SERVICE_NAME,
|
|
151
|
-
Labels: { "vyft.infrastructure": "true" },
|
|
152
|
-
TaskTemplate: {
|
|
153
|
-
ContainerSpec: {
|
|
154
|
-
Image: "caddy:latest",
|
|
155
|
-
Command: ["caddy", "run", "--resume"],
|
|
156
|
-
Mounts: [
|
|
157
|
-
{
|
|
158
|
-
Type: "volume",
|
|
159
|
-
Source: "vyft-proxy-config",
|
|
160
|
-
Target: "/config",
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
Type: "volume",
|
|
164
|
-
Source: "vyft-proxy-data",
|
|
165
|
-
Target: "/data",
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
Labels: { "vyft.infrastructure": "true" },
|
|
169
|
-
},
|
|
170
|
-
Networks: [{ Target: "vyft-network" }],
|
|
171
|
-
},
|
|
172
|
-
EndpointSpec: {
|
|
173
|
-
Ports: [
|
|
174
|
-
{
|
|
175
|
-
Protocol: "tcp",
|
|
176
|
-
TargetPort: 80,
|
|
177
|
-
PublishedPort: 80,
|
|
178
|
-
PublishMode: "ingress",
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
Protocol: "tcp",
|
|
182
|
-
TargetPort: 443,
|
|
183
|
-
PublishedPort: 443,
|
|
184
|
-
PublishMode: "ingress",
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
|
-
},
|
|
188
|
-
Mode: { Replicated: { Replicas: 1 } },
|
|
189
|
-
});
|
|
190
|
-
log.step("Created vyft-proxy");
|
|
191
|
-
await this.seedCaddyConfig();
|
|
106
|
+
await this.proxy.ensure();
|
|
192
107
|
const durationMs = Math.round(performance.now() - start);
|
|
193
|
-
this.log.info({
|
|
194
|
-
}
|
|
195
|
-
async findProxyContainer() {
|
|
196
|
-
const containers = await this.docker.listContainers({
|
|
197
|
-
filters: JSON.stringify({
|
|
198
|
-
label: [`com.docker.swarm.service.name=${PROXY_SERVICE_NAME}`],
|
|
199
|
-
}),
|
|
200
|
-
});
|
|
201
|
-
if (containers.length === 0) {
|
|
202
|
-
throw new Error("vyft-proxy container not found");
|
|
203
|
-
}
|
|
204
|
-
return this.docker.getContainer(containers[0].Id);
|
|
205
|
-
}
|
|
206
|
-
async caddyApiRequest(method, path, body) {
|
|
207
|
-
const start = performance.now();
|
|
208
|
-
this.log.debug({ method, path }, "caddy request started");
|
|
209
|
-
const container = await this.findProxyContainer();
|
|
210
|
-
let cmd;
|
|
211
|
-
if (method === "POST") {
|
|
212
|
-
const bodyStr = body ? JSON.stringify(body) : "";
|
|
213
|
-
cmd = [
|
|
214
|
-
"wget",
|
|
215
|
-
"-q",
|
|
216
|
-
"-O",
|
|
217
|
-
"-",
|
|
218
|
-
"--timeout=10",
|
|
219
|
-
"--header=Content-Type: application/json",
|
|
220
|
-
`--post-data=${bodyStr}`,
|
|
221
|
-
`http://127.0.0.1:2019${path}`,
|
|
222
|
-
];
|
|
223
|
-
}
|
|
224
|
-
else if (method === "DELETE") {
|
|
225
|
-
cmd = [
|
|
226
|
-
"curl",
|
|
227
|
-
"-s",
|
|
228
|
-
"--connect-timeout",
|
|
229
|
-
"5",
|
|
230
|
-
"--max-time",
|
|
231
|
-
"10",
|
|
232
|
-
"-X",
|
|
233
|
-
"DELETE",
|
|
234
|
-
`http://127.0.0.1:2019${path}`,
|
|
235
|
-
];
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
cmd = [
|
|
239
|
-
"wget",
|
|
240
|
-
"-q",
|
|
241
|
-
"-O",
|
|
242
|
-
"-",
|
|
243
|
-
"--timeout=10",
|
|
244
|
-
`http://127.0.0.1:2019${path}`,
|
|
245
|
-
];
|
|
246
|
-
}
|
|
247
|
-
this.log.trace({ cmd }, "caddy exec command");
|
|
248
|
-
const exec = await container.exec({
|
|
249
|
-
Cmd: cmd,
|
|
250
|
-
AttachStdout: true,
|
|
251
|
-
AttachStderr: true,
|
|
252
|
-
});
|
|
253
|
-
const stream = await exec.start({});
|
|
254
|
-
return new Promise((resolve, reject) => {
|
|
255
|
-
const stdout = [];
|
|
256
|
-
const stderr = [];
|
|
257
|
-
const stdoutStream = new PassThrough();
|
|
258
|
-
const stderrStream = new PassThrough();
|
|
259
|
-
stdoutStream.on("data", (chunk) => stdout.push(chunk));
|
|
260
|
-
stderrStream.on("data", (chunk) => stderr.push(chunk));
|
|
261
|
-
this.docker.modem.demuxStream(stream, stdoutStream, stderrStream);
|
|
262
|
-
stream.on("end", () => {
|
|
263
|
-
const output = Buffer.concat(stdout).toString();
|
|
264
|
-
const errOutput = Buffer.concat(stderr).toString();
|
|
265
|
-
const durationMs = Math.round(performance.now() - start);
|
|
266
|
-
this.log.trace({ stdoutBytes: output.length, stderrBytes: errOutput.length }, "caddy exec output");
|
|
267
|
-
if (errOutput) {
|
|
268
|
-
this.log.debug({ method, path, durationMs, stderr: errOutput }, "caddy request failed");
|
|
269
|
-
reject(new Error(`Caddy API ${method} ${path} failed: ${errOutput}`));
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
this.log.debug({ method, path, durationMs }, "caddy request completed");
|
|
273
|
-
resolve(output);
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
async seedCaddyConfig() {
|
|
279
|
-
const maxWait = 30000;
|
|
280
|
-
const interval = 2000;
|
|
281
|
-
let waited = 0;
|
|
282
|
-
this.log.debug("waiting for proxy container");
|
|
283
|
-
while (waited < maxWait) {
|
|
284
|
-
try {
|
|
285
|
-
await this.findProxyContainer();
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
catch (err) {
|
|
289
|
-
this.log.debug({ err, waited, maxWait }, "proxy container not ready");
|
|
290
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
291
|
-
waited += interval;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const baseConfig = {
|
|
295
|
-
logging: {
|
|
296
|
-
logs: {
|
|
297
|
-
default: {
|
|
298
|
-
level: "INFO",
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
apps: {
|
|
303
|
-
http: {
|
|
304
|
-
servers: {
|
|
305
|
-
main: {
|
|
306
|
-
listen: [":443", ":80"],
|
|
307
|
-
routes: [],
|
|
308
|
-
logs: {},
|
|
309
|
-
},
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
const maxAttempts = 5;
|
|
315
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
316
|
-
try {
|
|
317
|
-
this.log.debug({ attempt: attempt + 1, max: maxAttempts }, "seeding caddy config");
|
|
318
|
-
await this.caddyApiRequest("POST", "/load", baseConfig);
|
|
319
|
-
this.log.debug("caddy config seeded");
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
catch (err) {
|
|
323
|
-
this.log.debug({ err, attempt: attempt + 1, max: maxAttempts }, "seed attempt failed");
|
|
324
|
-
if (attempt >= Math.floor(maxAttempts * 0.8)) {
|
|
325
|
-
this.log.warn({ attempt: attempt + 1, max: maxAttempts }, "seed retry nearing limit");
|
|
326
|
-
}
|
|
327
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
this.log.error("caddy config seed exhausted retries");
|
|
331
|
-
throw new Error("Failed to seed Caddy config after retries");
|
|
332
|
-
}
|
|
333
|
-
async addRoute(resourceId, route, handler) {
|
|
334
|
-
this.log.debug({ resourceId, route }, "adding route");
|
|
335
|
-
const caddyRoute = buildCaddyRoute(resourceId, route, handler);
|
|
336
|
-
// Delete existing route for idempotency
|
|
337
|
-
try {
|
|
338
|
-
await this.caddyApiRequest("DELETE", `/id/vyft-${resourceId}`);
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
this.log.debug({ err, resourceId }, "idempotent route delete failed");
|
|
342
|
-
}
|
|
343
|
-
const maxAttempts = 5;
|
|
344
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
345
|
-
try {
|
|
346
|
-
await this.caddyApiRequest("POST", "/config/apps/http/servers/main/routes", caddyRoute);
|
|
347
|
-
this.log.debug({ resourceId, route }, "route added");
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
catch (err) {
|
|
351
|
-
this.log.debug({ err, resourceId, attempt: attempt + 1, max: maxAttempts }, "route add attempt failed");
|
|
352
|
-
if (attempt === maxAttempts - 1) {
|
|
353
|
-
this.log.error({ resourceId, route }, "route add exhausted retries");
|
|
354
|
-
throw new Error(`Failed to add route for ${resourceId}`);
|
|
355
|
-
}
|
|
356
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
async removeRoute(resourceId) {
|
|
361
|
-
this.log.debug({ resourceId }, "removing route");
|
|
362
|
-
try {
|
|
363
|
-
await this.caddyApiRequest("DELETE", `/id/vyft-${resourceId}`);
|
|
364
|
-
this.log.debug({ resourceId }, "route removed");
|
|
365
|
-
}
|
|
366
|
-
catch (err) {
|
|
367
|
-
this.log.debug({ err, resourceId }, "route removal failed (may not exist)");
|
|
368
|
-
}
|
|
108
|
+
this.log.info({ durationMs }, "infrastructure ready");
|
|
369
109
|
}
|
|
370
110
|
async listManagedResources() {
|
|
371
111
|
const resources = [];
|
|
@@ -781,9 +521,9 @@ export class DockerClient {
|
|
|
781
521
|
}
|
|
782
522
|
if (config.route) {
|
|
783
523
|
const port = config.port || 3000;
|
|
784
|
-
await this.addRoute(service.id, config.route, {
|
|
785
|
-
|
|
786
|
-
|
|
524
|
+
await this.proxy.addRoute(service.id, config.route, {
|
|
525
|
+
host: service.id,
|
|
526
|
+
port,
|
|
787
527
|
});
|
|
788
528
|
}
|
|
789
529
|
const durationMs = Math.round(performance.now() - start);
|
|
@@ -805,10 +545,27 @@ export class DockerClient {
|
|
|
805
545
|
return false;
|
|
806
546
|
}
|
|
807
547
|
}
|
|
548
|
+
async waitForHealthy(id, timeoutMs = 120000) {
|
|
549
|
+
const start = performance.now();
|
|
550
|
+
const interval = 2000;
|
|
551
|
+
this.log.debug({ resourceId: id }, "waiting for service to be healthy");
|
|
552
|
+
while (performance.now() - start < timeoutMs) {
|
|
553
|
+
const tasks = await this.docker.listTasks({
|
|
554
|
+
filters: { service: [id], "desired-state": ["running"] },
|
|
555
|
+
});
|
|
556
|
+
const healthy = tasks.some((t) => t.Status.State === "running");
|
|
557
|
+
if (healthy) {
|
|
558
|
+
this.log.debug({ resourceId: id }, "service is healthy");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
562
|
+
}
|
|
563
|
+
throw new Error(`Service ${id} did not become healthy within ${timeoutMs}ms`);
|
|
564
|
+
}
|
|
808
565
|
async removeService(id) {
|
|
809
566
|
const start = performance.now();
|
|
810
567
|
this.log.debug({ resourceId: id }, "removing service");
|
|
811
|
-
await this.removeRoute(id);
|
|
568
|
+
await this.proxy.removeRoute(id);
|
|
812
569
|
await this.docker.getService(id).remove();
|
|
813
570
|
const maxWait = 60000;
|
|
814
571
|
const interval = 2000;
|
|
@@ -901,13 +658,13 @@ export class DockerClient {
|
|
|
901
658
|
"vyft.managed": "true",
|
|
902
659
|
"vyft.project": this.project,
|
|
903
660
|
};
|
|
904
|
-
const command = static_.config.spa
|
|
905
|
-
? [
|
|
661
|
+
const command = static_.config.spa === false
|
|
662
|
+
? ["caddy", "file-server", "--root", "/srv", "--listen", ":80"]
|
|
663
|
+
: [
|
|
906
664
|
"sh",
|
|
907
665
|
"-c",
|
|
908
666
|
"printf ':80 {\\nroot * /srv\\ntry_files {path} /index.html\\nfile_server\\n}\\n' > /etc/caddy/Caddyfile && caddy run --config /etc/caddy/Caddyfile --adapter caddyfile",
|
|
909
|
-
]
|
|
910
|
-
: ["caddy", "file-server", "--root", "/srv", "--listen", ":80"];
|
|
667
|
+
];
|
|
911
668
|
const serviceSpec = {
|
|
912
669
|
Name: static_.id,
|
|
913
670
|
Labels: serviceLabels,
|
|
@@ -945,9 +702,9 @@ export class DockerClient {
|
|
|
945
702
|
this.log.debug({ resourceId: static_.id }, "creating caddy service for static site");
|
|
946
703
|
await this.docker.createService(serviceSpec);
|
|
947
704
|
}
|
|
948
|
-
await this.addRoute(static_.id, static_.config.route, {
|
|
949
|
-
|
|
950
|
-
|
|
705
|
+
await this.proxy.addRoute(static_.id, static_.config.route, {
|
|
706
|
+
host: static_.id,
|
|
707
|
+
port: 80,
|
|
951
708
|
});
|
|
952
709
|
const durationMs = Math.round(performance.now() - start);
|
|
953
710
|
this.log.info({ resourceId: static_.id, route: static_.config.route, durationMs }, existing ? "site updated" : "site created");
|
|
@@ -956,7 +713,7 @@ export class DockerClient {
|
|
|
956
713
|
async removeStatic(id) {
|
|
957
714
|
const start = performance.now();
|
|
958
715
|
this.log.debug({ resourceId: id }, "removing static site");
|
|
959
|
-
await this.removeRoute(id);
|
|
716
|
+
await this.proxy.removeRoute(id);
|
|
960
717
|
await this.docker.getService(id).remove();
|
|
961
718
|
const maxWait = 30000;
|
|
962
719
|
const interval = 2000;
|
|
@@ -989,10 +746,81 @@ export class DockerClient {
|
|
|
989
746
|
this.log.info({ resourceId: id, durationMs }, "site removed");
|
|
990
747
|
log.step(`Removed ${id}`);
|
|
991
748
|
}
|
|
992
|
-
async
|
|
993
|
-
const
|
|
749
|
+
async *serviceLogs(options) {
|
|
750
|
+
const allServices = await this.docker.listServices({
|
|
751
|
+
filters: JSON.stringify({
|
|
752
|
+
label: ["vyft.managed=true", `vyft.project=${this.project}`],
|
|
753
|
+
}),
|
|
754
|
+
});
|
|
755
|
+
const prefix = `${this.project}-`;
|
|
756
|
+
let matched = allServices;
|
|
757
|
+
if (options.services && options.services.length > 0) {
|
|
758
|
+
const filter = new Set(options.services);
|
|
759
|
+
matched = allServices.filter((svc) => {
|
|
760
|
+
const name = svc.Spec?.Name || "";
|
|
761
|
+
const short = name.startsWith(prefix)
|
|
762
|
+
? name.slice(prefix.length)
|
|
763
|
+
: name;
|
|
764
|
+
return filter.has(short) || filter.has(name);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (matched.length === 0) {
|
|
768
|
+
log.warn("No matching services found");
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
994
771
|
const tail = options.tail ?? 100;
|
|
995
|
-
|
|
772
|
+
// Collect all containers with their short service name
|
|
773
|
+
const targets = [];
|
|
774
|
+
for (const svc of matched) {
|
|
775
|
+
const name = svc.Spec?.Name || "";
|
|
776
|
+
const short = name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
777
|
+
const containers = await this.docker.listContainers({
|
|
778
|
+
filters: JSON.stringify({
|
|
779
|
+
label: [`com.docker.swarm.service.name=${name}`],
|
|
780
|
+
}),
|
|
781
|
+
});
|
|
782
|
+
for (const cInfo of containers) {
|
|
783
|
+
targets.push({ container: this.docker.getContainer(cInfo.Id), short });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!options.follow) {
|
|
787
|
+
for (const { container, short } of targets) {
|
|
788
|
+
const buf = await container.logs({
|
|
789
|
+
stdout: true,
|
|
790
|
+
stderr: true,
|
|
791
|
+
follow: false,
|
|
792
|
+
tail,
|
|
793
|
+
timestamps: true,
|
|
794
|
+
});
|
|
795
|
+
let offset = 0;
|
|
796
|
+
while (offset + 8 <= buf.length) {
|
|
797
|
+
const type = buf[offset];
|
|
798
|
+
const size = buf.readUInt32BE(offset + 4);
|
|
799
|
+
offset += 8;
|
|
800
|
+
const text = buf.subarray(offset, offset + size).toString();
|
|
801
|
+
yield {
|
|
802
|
+
service: short,
|
|
803
|
+
stream: type === 2 ? "stderr" : "stdout",
|
|
804
|
+
text,
|
|
805
|
+
};
|
|
806
|
+
offset += size;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// Follow mode: merge all container streams concurrently
|
|
812
|
+
const buffer = [];
|
|
813
|
+
let notify = null;
|
|
814
|
+
let active = targets.length;
|
|
815
|
+
const push = (entry) => {
|
|
816
|
+
buffer.push(entry);
|
|
817
|
+
if (notify) {
|
|
818
|
+
const n = notify;
|
|
819
|
+
notify = null;
|
|
820
|
+
n();
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
for (const { container, short } of targets) {
|
|
996
824
|
const stream = await container.logs({
|
|
997
825
|
stdout: true,
|
|
998
826
|
stderr: true,
|
|
@@ -1000,38 +828,38 @@ export class DockerClient {
|
|
|
1000
828
|
tail,
|
|
1001
829
|
timestamps: true,
|
|
1002
830
|
});
|
|
1003
|
-
const
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
const buf = await container.logs({
|
|
1014
|
-
stdout: true,
|
|
1015
|
-
stderr: true,
|
|
1016
|
-
follow: false,
|
|
1017
|
-
tail,
|
|
1018
|
-
timestamps: true,
|
|
831
|
+
const stdoutPT = new PassThrough();
|
|
832
|
+
const stderrPT = new PassThrough();
|
|
833
|
+
stdoutPT.on("data", (chunk) => {
|
|
834
|
+
for (const line of chunk.toString().split("\n")) {
|
|
835
|
+
if (line)
|
|
836
|
+
push({ service: short, stream: "stdout", text: line });
|
|
837
|
+
}
|
|
1019
838
|
});
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const type = buf[offset];
|
|
1025
|
-
const size = buf.readUInt32BE(offset + 4);
|
|
1026
|
-
offset += 8;
|
|
1027
|
-
const payload = buf.subarray(offset, offset + size);
|
|
1028
|
-
if (type === 2) {
|
|
1029
|
-
process.stderr.write(payload);
|
|
839
|
+
stderrPT.on("data", (chunk) => {
|
|
840
|
+
for (const line of chunk.toString().split("\n")) {
|
|
841
|
+
if (line)
|
|
842
|
+
push({ service: short, stream: "stderr", text: line });
|
|
1030
843
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
844
|
+
});
|
|
845
|
+
this.docker.modem.demuxStream(stream, stdoutPT, stderrPT);
|
|
846
|
+
stream.on("end", () => {
|
|
847
|
+
active--;
|
|
848
|
+
if (notify && active === 0) {
|
|
849
|
+
const n = notify;
|
|
850
|
+
notify = null;
|
|
851
|
+
n();
|
|
1033
852
|
}
|
|
1034
|
-
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
while (active > 0 || buffer.length > 0) {
|
|
856
|
+
if (buffer.length === 0) {
|
|
857
|
+
await new Promise((r) => {
|
|
858
|
+
notify = r;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
while (buffer.length > 0) {
|
|
862
|
+
yield buffer.shift();
|
|
1035
863
|
}
|
|
1036
864
|
}
|
|
1037
865
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,7 @@ export { interpolate } from "./interpolate.js";
|
|
|
2
2
|
export type { EnvValue, HealthCheckConfig, Interpolation, Reference, Resource, ResourceLimits, ResourceType, Secret, SecretConfig, Service, ServiceConfig, Site, SiteConfig, Volume, VolumeConfig, } from "./resource.js";
|
|
3
3
|
export { isInterpolation, isReference, isSecret, validateId, validateRoute, } from "./resource.js";
|
|
4
4
|
export type { Runtime } from "./runtime.js";
|
|
5
|
+
export type { BackupConfig, Bucket, Minio, MinioConfig, Mongo, MongoConfig, Mysql, MysqlConfig, Nats, NatsConfig, Postgres, PostgresConfig, Rabbitmq, RabbitmqConfig, Redis, RedisConfig, Storage, StorageConfig, } from "./services/index.js";
|
|
5
6
|
export type { RuntimeMeta, RuntimeRef } from "./symbols.js";
|
|
6
7
|
export { VYFT_RUNTIME } from "./symbols.js";
|
|
8
|
+
export declare const service: (id: string, config: import("./swarm/types.js").SwarmServiceConfig) => import("./resource.js").Service, secret: (id: string, config?: import("./resource.js").SecretConfig) => import("./resource.js").Secret, volume: (id: string, config?: import("./resource.js").VolumeConfig) => import("./resource.js").Volume, site: (id: string, config: import("./resource.js").SiteConfig) => import("./resource.js").Site, postgres: (id: string, config?: import("./index.js").PostgresConfig) => import("./index.js").Postgres, mysql: (id: string, config?: import("./index.js").MysqlConfig) => import("./index.js").Mysql, redis: (id: string, config?: import("./index.js").RedisConfig) => import("./index.js").Redis, rabbitmq: (id: string, config?: import("./index.js").RabbitmqConfig) => import("./index.js").Rabbitmq, nats: (id: string, config?: import("./index.js").NatsConfig) => import("./index.js").Nats, mongo: (id: string, config?: import("./index.js").MongoConfig) => import("./index.js").Mongo, minio: (id: string, config?: import("./index.js").MinioConfig) => import("./index.js").Minio, storage: (id: string, config?: import("./index.js").StorageConfig) => import("./index.js").Storage;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { interpolate } from "./interpolate.js";
|
|
2
2
|
export { isInterpolation, isReference, isSecret, validateId, validateRoute, } from "./resource.js";
|
|
3
3
|
export { VYFT_RUNTIME } from "./symbols.js";
|
|
4
|
+
import { createFactories } from "./swarm/factories.js";
|
|
5
|
+
const factories = createFactories({});
|
|
6
|
+
export const { service, secret, volume, site, postgres, mysql, redis, rabbitmq, nats, mongo, minio, storage, } = factories;
|
package/dist/init.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -90,11 +91,27 @@ export async function init(directory) {
|
|
|
90
91
|
await writeFile(destPath, content);
|
|
91
92
|
log.step(relativePath);
|
|
92
93
|
}
|
|
94
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
95
|
+
const pm = ua.startsWith("pnpm")
|
|
96
|
+
? "pnpm"
|
|
97
|
+
: ua.startsWith("bun")
|
|
98
|
+
? "bun"
|
|
99
|
+
: "npm";
|
|
100
|
+
const install = await confirm({
|
|
101
|
+
message: `Install dependencies with ${pm}?`,
|
|
102
|
+
});
|
|
103
|
+
if (isCancel(install))
|
|
104
|
+
return;
|
|
105
|
+
if (install) {
|
|
106
|
+
log.step(`Running ${pm} install...`);
|
|
107
|
+
execSync(`${pm} install`, { cwd: targetDir, stdio: "inherit" });
|
|
108
|
+
}
|
|
93
109
|
outro("Project created");
|
|
94
110
|
console.log();
|
|
95
111
|
console.log("Next steps:");
|
|
96
112
|
console.log(` cd ${path.relative(process.cwd(), targetDir)}`);
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
if (!install)
|
|
114
|
+
console.log(` ${pm} install`);
|
|
115
|
+
console.log(` ${pm} dev`);
|
|
99
116
|
console.log();
|
|
100
117
|
}
|
package/dist/interpolate.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import type { Interpolation, Reference } from "./resource.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tagged template literal for building strings that contain {@link Reference} values
|
|
4
|
+
* (e.g. secrets) which are resolved at deploy time.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const connStr = interpolate`postgres://user:${dbPassword}@${db.host}:5432/app`;
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @throws If none of the interpolated values are a {@link Reference}.
|
|
12
|
+
*/
|
|
2
13
|
export declare function interpolate(strings: TemplateStringsArray, ...values: Array<Reference | string>): Interpolation;
|
package/dist/interpolate.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { isReference } from "./resource.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tagged template literal for building strings that contain {@link Reference} values
|
|
4
|
+
* (e.g. secrets) which are resolved at deploy time.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const connStr = interpolate`postgres://user:${dbPassword}@${db.host}:5432/app`;
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @throws If none of the interpolated values are a {@link Reference}.
|
|
12
|
+
*/
|
|
2
13
|
export function interpolate(strings, ...values) {
|
|
3
14
|
const hasRef = values.some(isReference);
|
|
4
15
|
if (!hasRef) {
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ReverseProxy {
|
|
2
|
+
/** Ensure the proxy service is running and configured. */
|
|
3
|
+
ensure(): Promise<void>;
|
|
4
|
+
/** Register or update a route for a resource. */
|
|
5
|
+
addRoute(resourceId: string, route: string, target: {
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
/** Remove a route for a resource. No-op if it doesn't exist. */
|
|
10
|
+
removeRoute(resourceId: string): Promise<void>;
|
|
11
|
+
/** Stream proxy access logs. */
|
|
12
|
+
logs(options: {
|
|
13
|
+
follow?: boolean;
|
|
14
|
+
tail?: number;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
}
|
package/dist/proxy.js
ADDED
|
File without changes
|