vyft 0.3.0-alpha → 0.4.1-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/LICENSE +191 -21
- package/README.md +3 -49
- package/dist/commands/context/add.d.ts +4 -0
- package/dist/commands/context/add.d.ts.map +1 -0
- package/dist/commands/context/add.js +98 -0
- package/dist/commands/context/add.js.map +1 -0
- package/dist/commands/context/index.d.ts +4 -0
- package/dist/commands/context/index.d.ts.map +1 -0
- package/dist/commands/context/index.js +12 -0
- package/dist/commands/context/index.js.map +1 -0
- package/dist/commands/context/list.d.ts +4 -0
- package/dist/commands/context/list.d.ts.map +1 -0
- package/dist/commands/context/list.js +25 -0
- package/dist/commands/context/list.js.map +1 -0
- package/dist/commands/context/remove.d.ts +4 -0
- package/dist/commands/context/remove.d.ts.map +1 -0
- package/dist/commands/context/remove.js +36 -0
- package/dist/commands/context/remove.js.map +1 -0
- package/dist/commands/context/use.d.ts +4 -0
- package/dist/commands/context/use.d.ts.map +1 -0
- package/dist/commands/context/use.js +32 -0
- package/dist/commands/context/use.js.map +1 -0
- package/dist/commands/deploy.d.ts +4 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +55 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/destroy.d.ts +4 -0
- package/dist/commands/destroy.d.ts.map +1 -0
- package/dist/commands/destroy.js +70 -0
- package/dist/commands/destroy.js.map +1 -0
- package/dist/commands/diff.d.ts +4 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +52 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/local/detect.d.ts +7 -0
- package/dist/commands/local/detect.d.ts.map +1 -0
- package/dist/commands/local/detect.js +146 -0
- package/dist/commands/local/detect.js.map +1 -0
- package/dist/commands/local/dev.d.ts +4 -0
- package/dist/commands/local/dev.d.ts.map +1 -0
- package/dist/commands/local/dev.js +339 -0
- package/dist/commands/local/dev.js.map +1 -0
- package/dist/commands/local/down.d.ts +4 -0
- package/dist/commands/local/down.d.ts.map +1 -0
- package/dist/commands/local/down.js +61 -0
- package/dist/commands/local/down.js.map +1 -0
- package/dist/commands/local/index.d.ts +4 -0
- package/dist/commands/local/index.d.ts.map +1 -0
- package/dist/commands/local/index.js +12 -0
- package/dist/commands/local/index.js.map +1 -0
- package/dist/commands/local/reset.d.ts +4 -0
- package/dist/commands/local/reset.d.ts.map +1 -0
- package/dist/commands/local/reset.js +67 -0
- package/dist/commands/local/reset.js.map +1 -0
- package/dist/commands/local/up.d.ts +4 -0
- package/dist/commands/local/up.d.ts.map +1 -0
- package/dist/commands/local/up.js +58 -0
- package/dist/commands/local/up.js.map +1 -0
- package/dist/commands/refresh.d.ts +4 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/refresh.js +39 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -0
- package/dist/contexts.d.ts +21 -0
- package/dist/contexts.d.ts.map +1 -0
- package/dist/contexts.js +72 -0
- package/dist/contexts.js.map +1 -0
- package/dist/index.d.ts +2 -8
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -6
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +7 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +6 -0
- package/dist/lib.js.map +1 -0
- package/dist/providers.d.ts +9 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +41 -0
- package/dist/providers.js.map +1 -0
- package/dist/runtime.d.ts +18 -9
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +154 -0
- package/dist/runtime.js.map +1 -0
- package/dist/runtime.test.d.ts +2 -0
- package/dist/runtime.test.d.ts.map +1 -0
- package/dist/runtime.test.js +119 -0
- package/dist/runtime.test.js.map +1 -0
- package/dist/utils/fs.d.ts +4 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +33 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/pm.d.ts +3 -0
- package/dist/utils/pm.d.ts.map +1 -0
- package/dist/utils/pm.js +17 -0
- package/dist/utils/pm.js.map +1 -0
- package/dist/utils/prompts.d.ts +2 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +6 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/templates.d.ts +3 -0
- package/dist/utils/templates.d.ts.map +1 -0
- package/dist/utils/templates.js +48 -0
- package/dist/utils/templates.js.map +1 -0
- package/package.json +31 -46
- package/templates/bun/index.ts +8 -0
- package/templates/bun/package.json +15 -0
- package/templates/bun/tsconfig.json +15 -0
- package/templates/bun/vyft.config.ts +3 -0
- package/dist/build.d.ts +0 -11
- package/dist/build.js +0 -39
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -398
- package/dist/context.d.ts +0 -39
- package/dist/context.js +0 -101
- package/dist/docker.d.ts +0 -62
- package/dist/docker.js +0 -876
- package/dist/exec.d.ts +0 -2
- package/dist/exec.js +0 -28
- package/dist/init.d.ts +0 -1
- package/dist/init.js +0 -117
- package/dist/interpolate.d.ts +0 -13
- package/dist/interpolate.js +0 -19
- package/dist/logger.d.ts +0 -2
- package/dist/logger.js +0 -10
- package/dist/proxy.d.ts +0 -16
- package/dist/proxy.js +0 -0
- package/dist/resource.d.ts +0 -174
- package/dist/resource.js +0 -45
- package/dist/services/index.d.ts +0 -24
- package/dist/services/index.js +0 -20
- package/dist/services/minio.d.ts +0 -36
- package/dist/services/minio.js +0 -53
- package/dist/services/mongo.d.ts +0 -28
- package/dist/services/mongo.js +0 -45
- package/dist/services/mysql.d.ts +0 -28
- package/dist/services/mysql.js +0 -44
- package/dist/services/nats.d.ts +0 -26
- package/dist/services/nats.js +0 -38
- package/dist/services/postgres.d.ts +0 -28
- package/dist/services/postgres.js +0 -45
- package/dist/services/rabbitmq.d.ts +0 -28
- package/dist/services/rabbitmq.js +0 -44
- package/dist/services/redis.d.ts +0 -28
- package/dist/services/redis.js +0 -49
- package/dist/services/storage.d.ts +0 -39
- package/dist/services/storage.js +0 -94
- package/dist/swarm/factories.d.ts +0 -16
- package/dist/swarm/factories.js +0 -63
- package/dist/swarm/index.d.ts +0 -20
- package/dist/swarm/index.js +0 -5
- package/dist/swarm/proxy.d.ts +0 -24
- package/dist/swarm/proxy.js +0 -339
- package/dist/swarm/types.d.ts +0 -26
- package/dist/swarm/types.js +0 -0
- package/dist/symbols.d.ts +0 -13
- package/dist/symbols.js +0 -2
- package/templates/fullstack/apps/api/Dockerfile +0 -22
- package/templates/fullstack/apps/api/package.json +0 -26
- package/templates/fullstack/apps/api/src/auth.ts +0 -21
- package/templates/fullstack/apps/api/src/db.ts +0 -16
- package/templates/fullstack/apps/api/src/index.ts +0 -17
- package/templates/fullstack/apps/api/src/router.ts +0 -11
- package/templates/fullstack/apps/api/src/schema.ts +0 -11
- package/templates/fullstack/apps/api/tsconfig.json +0 -8
- package/templates/fullstack/apps/web/index.html +0 -12
- package/templates/fullstack/apps/web/package.json +0 -21
- package/templates/fullstack/apps/web/src/app.tsx +0 -8
- package/templates/fullstack/apps/web/src/main.tsx +0 -9
- package/templates/fullstack/apps/web/tsconfig.json +0 -7
- package/templates/fullstack/apps/web/vite.config.ts +0 -14
- package/templates/fullstack/compose.yaml +0 -14
- package/templates/fullstack/dockerignore +0 -7
- package/templates/fullstack/gitignore +0 -3
- package/templates/fullstack/package.json +0 -18
- package/templates/fullstack/pnpm-workspace.yaml +0 -2
- package/templates/fullstack/tsconfig.json +0 -11
- package/templates/fullstack/vyft.config.ts +0 -22
package/dist/docker.js
DELETED
|
@@ -1,876 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { PassThrough } from "node:stream";
|
|
4
|
-
import { log } from "@clack/prompts";
|
|
5
|
-
import Docker from "dockerode";
|
|
6
|
-
import tar from "tar-fs";
|
|
7
|
-
import { buildStatic } from "./build.js";
|
|
8
|
-
import { logger as defaultLogger } from "./logger.js";
|
|
9
|
-
import { isInterpolation, isReference } from "./resource.js";
|
|
10
|
-
import { CaddyProxy } from "./swarm/proxy.js";
|
|
11
|
-
function secretMount(id, name) {
|
|
12
|
-
return {
|
|
13
|
-
SecretID: id,
|
|
14
|
-
SecretName: name,
|
|
15
|
-
File: { Name: name, UID: "0", GID: "0", Mode: 0o444 },
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
function parseDuration(duration) {
|
|
19
|
-
const match = duration.match(/^(\d+)(ms|s|m|h)$/);
|
|
20
|
-
if (!match || !match[1] || !match[2]) {
|
|
21
|
-
throw new Error(`Invalid duration format: ${duration}`);
|
|
22
|
-
}
|
|
23
|
-
const value = parseInt(match[1], 10);
|
|
24
|
-
const unit = match[2];
|
|
25
|
-
const multipliers = {
|
|
26
|
-
ms: 1_000_000,
|
|
27
|
-
s: 1_000_000_000,
|
|
28
|
-
m: 60_000_000_000,
|
|
29
|
-
h: 3_600_000_000_000,
|
|
30
|
-
};
|
|
31
|
-
return value * multipliers[unit];
|
|
32
|
-
}
|
|
33
|
-
function parseMemory(memory) {
|
|
34
|
-
const match = memory.match(/^(\d+)(B|KB|MB|GB)$/i);
|
|
35
|
-
if (!match || !match[1] || !match[2]) {
|
|
36
|
-
throw new Error(`Invalid memory format: ${memory}`);
|
|
37
|
-
}
|
|
38
|
-
const value = parseInt(match[1], 10);
|
|
39
|
-
const unit = match[2].toUpperCase();
|
|
40
|
-
const multipliers = {
|
|
41
|
-
B: 1,
|
|
42
|
-
KB: 1024,
|
|
43
|
-
MB: 1024 * 1024,
|
|
44
|
-
GB: 1024 * 1024 * 1024,
|
|
45
|
-
};
|
|
46
|
-
return value * multipliers[unit];
|
|
47
|
-
}
|
|
48
|
-
export function parseRoute(route) {
|
|
49
|
-
const slashIndex = route.indexOf("/");
|
|
50
|
-
if (slashIndex === -1) {
|
|
51
|
-
return { host: route };
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
host: route.slice(0, slashIndex),
|
|
55
|
-
path: route.slice(slashIndex),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
export class DockerClient {
|
|
59
|
-
docker;
|
|
60
|
-
localDocker;
|
|
61
|
-
isRemote;
|
|
62
|
-
project;
|
|
63
|
-
secretValues = new Map();
|
|
64
|
-
log;
|
|
65
|
-
proxy;
|
|
66
|
-
verbose = false;
|
|
67
|
-
constructor(project, opts) {
|
|
68
|
-
this.localDocker = new Docker({ socketPath: "/var/run/docker.sock" });
|
|
69
|
-
const host = opts?.host;
|
|
70
|
-
if (host)
|
|
71
|
-
process.env.DOCKER_HOST = host;
|
|
72
|
-
this.docker = new Docker();
|
|
73
|
-
this.isRemote = !!host;
|
|
74
|
-
this.project = project;
|
|
75
|
-
this.log = (opts?.parentLogger ?? defaultLogger).child({
|
|
76
|
-
component: "docker",
|
|
77
|
-
project,
|
|
78
|
-
});
|
|
79
|
-
this.proxy = new CaddyProxy(this.docker, this.log);
|
|
80
|
-
if (host)
|
|
81
|
-
this.log.debug({ host }, "using remote endpoint");
|
|
82
|
-
}
|
|
83
|
-
async ensureNetwork(name, labels = {}) {
|
|
84
|
-
const networks = await this.docker.listNetworks({
|
|
85
|
-
filters: JSON.stringify({ name: [name] }),
|
|
86
|
-
});
|
|
87
|
-
const exactMatch = networks.some((n) => n.Name === name);
|
|
88
|
-
if (!exactMatch) {
|
|
89
|
-
this.log.debug({ network: name }, "creating network");
|
|
90
|
-
await this.docker.createNetwork({
|
|
91
|
-
Name: name,
|
|
92
|
-
Driver: "overlay",
|
|
93
|
-
Attachable: true,
|
|
94
|
-
Labels: labels,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async ensureInfrastructure() {
|
|
99
|
-
const start = performance.now();
|
|
100
|
-
// Ensure per-project network for inter-service DNS
|
|
101
|
-
await this.ensureNetwork(`${this.project}-internal`, {
|
|
102
|
-
"com.docker.stack.namespace": this.project,
|
|
103
|
-
"vyft.managed": "true",
|
|
104
|
-
"vyft.project": this.project,
|
|
105
|
-
});
|
|
106
|
-
await this.proxy.ensure();
|
|
107
|
-
const durationMs = Math.round(performance.now() - start);
|
|
108
|
-
this.log.info({ durationMs }, "infrastructure ready");
|
|
109
|
-
}
|
|
110
|
-
async listManagedResources() {
|
|
111
|
-
const resources = [];
|
|
112
|
-
const [services, volumes, secrets] = await Promise.all([
|
|
113
|
-
this.docker.listServices({
|
|
114
|
-
filters: JSON.stringify({
|
|
115
|
-
label: ["vyft.managed=true", `vyft.project=${this.project}`],
|
|
116
|
-
}),
|
|
117
|
-
}),
|
|
118
|
-
this.docker.listVolumes({
|
|
119
|
-
filters: JSON.stringify({
|
|
120
|
-
label: ["vyft.managed=true", `vyft.project=${this.project}`],
|
|
121
|
-
}),
|
|
122
|
-
}),
|
|
123
|
-
this.docker.listSecrets({
|
|
124
|
-
filters: JSON.stringify({
|
|
125
|
-
label: ["vyft.managed=true", `vyft.project=${this.project}`],
|
|
126
|
-
}),
|
|
127
|
-
}),
|
|
128
|
-
]);
|
|
129
|
-
for (const svc of services) {
|
|
130
|
-
const labels = svc.Spec?.Labels || {};
|
|
131
|
-
resources.push({
|
|
132
|
-
id: svc.Spec?.Name || "",
|
|
133
|
-
type: labels["vyft.type"] || "service",
|
|
134
|
-
route: labels["vyft.route"],
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
for (const vol of volumes.Volumes || []) {
|
|
138
|
-
if (vol.Labels?.["vyft.static"])
|
|
139
|
-
continue;
|
|
140
|
-
resources.push({
|
|
141
|
-
id: vol.Name,
|
|
142
|
-
type: "volume",
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
for (const secret of secrets) {
|
|
146
|
-
const secretLabels = secret.Spec?.Labels || {};
|
|
147
|
-
resources.push({
|
|
148
|
-
id: secret.Spec?.Name || "",
|
|
149
|
-
type: "secret",
|
|
150
|
-
derived: secretLabels["vyft.derived"] === "true",
|
|
151
|
-
parentService: secretLabels["vyft.parent-service"],
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
return resources;
|
|
155
|
-
}
|
|
156
|
-
async create(resource) {
|
|
157
|
-
switch (resource.type) {
|
|
158
|
-
case "volume":
|
|
159
|
-
return this.createVolume(resource);
|
|
160
|
-
case "secret":
|
|
161
|
-
return this.createSecret(resource);
|
|
162
|
-
case "service":
|
|
163
|
-
return this.createService(resource);
|
|
164
|
-
case "site":
|
|
165
|
-
return this.createStatic(resource);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
async exists(resource) {
|
|
169
|
-
switch (resource.type) {
|
|
170
|
-
case "volume":
|
|
171
|
-
return this.volumeExists(resource.id);
|
|
172
|
-
case "secret":
|
|
173
|
-
return this.secretExists(resource.id);
|
|
174
|
-
case "service":
|
|
175
|
-
case "site":
|
|
176
|
-
return this.serviceExists(resource.id);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
async remove(resource) {
|
|
180
|
-
switch (resource.type) {
|
|
181
|
-
case "volume":
|
|
182
|
-
return this.removeVolume(resource.id);
|
|
183
|
-
case "secret":
|
|
184
|
-
return this.removeSecret(resource.id);
|
|
185
|
-
case "service":
|
|
186
|
-
return this.removeService(resource.id);
|
|
187
|
-
case "site":
|
|
188
|
-
return this.removeStatic(resource.id);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
async createVolume(volume) {
|
|
192
|
-
const start = performance.now();
|
|
193
|
-
this.log.debug({ volumeName: volume.id }, "creating volume");
|
|
194
|
-
const config = volume.config;
|
|
195
|
-
const labels = {
|
|
196
|
-
"com.docker.stack.namespace": this.project,
|
|
197
|
-
"vyft.managed": "true",
|
|
198
|
-
"vyft.project": this.project,
|
|
199
|
-
"vyft.type": "volume",
|
|
200
|
-
};
|
|
201
|
-
if (config.size) {
|
|
202
|
-
labels["vyft.size"] = config.size;
|
|
203
|
-
}
|
|
204
|
-
await this.docker.createVolume({
|
|
205
|
-
Name: volume.id,
|
|
206
|
-
Labels: labels,
|
|
207
|
-
});
|
|
208
|
-
const durationMs = Math.round(performance.now() - start);
|
|
209
|
-
this.log.debug({ volumeName: volume.id, durationMs }, "volume created");
|
|
210
|
-
log.step(`Created ${volume.id}`);
|
|
211
|
-
}
|
|
212
|
-
async volumeExists(id) {
|
|
213
|
-
const start = performance.now();
|
|
214
|
-
try {
|
|
215
|
-
await this.docker.getVolume(id).inspect();
|
|
216
|
-
const durationMs = Math.round(performance.now() - start);
|
|
217
|
-
this.log.debug({ volumeName: id, exists: true, durationMs }, "volume exists check");
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
catch (err) {
|
|
221
|
-
const durationMs = Math.round(performance.now() - start);
|
|
222
|
-
this.log.debug({ err, volumeName: id, exists: false, durationMs }, "volume exists check");
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
async removeVolume(id) {
|
|
227
|
-
const start = performance.now();
|
|
228
|
-
this.log.debug({ volumeName: id }, "removing volume");
|
|
229
|
-
await this.docker.getVolume(id).remove();
|
|
230
|
-
const durationMs = Math.round(performance.now() - start);
|
|
231
|
-
this.log.debug({ volumeName: id, durationMs }, "volume removed");
|
|
232
|
-
log.step(`Removed ${id}`);
|
|
233
|
-
}
|
|
234
|
-
async createSecret(secret) {
|
|
235
|
-
const start = performance.now();
|
|
236
|
-
const length = secret.config.length || 32;
|
|
237
|
-
this.log.debug({ secretName: secret.id, length }, "creating secret");
|
|
238
|
-
const value = randomBytes(length).toString("base64url");
|
|
239
|
-
this.secretValues.set(secret.id, value);
|
|
240
|
-
await this.storeSecretData(secret.id, value, {
|
|
241
|
-
"vyft.length": length.toString(),
|
|
242
|
-
});
|
|
243
|
-
const durationMs = Math.round(performance.now() - start);
|
|
244
|
-
this.log.debug({ secretName: secret.id, length, durationMs }, "secret created");
|
|
245
|
-
log.step(`Created ${secret.id}`);
|
|
246
|
-
}
|
|
247
|
-
async secretExists(id) {
|
|
248
|
-
const start = performance.now();
|
|
249
|
-
try {
|
|
250
|
-
await this.docker.getSecret(id).inspect();
|
|
251
|
-
const durationMs = Math.round(performance.now() - start);
|
|
252
|
-
this.log.debug({ secretName: id, exists: true, durationMs }, "secret exists check");
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
const durationMs = Math.round(performance.now() - start);
|
|
257
|
-
this.log.debug({ err, secretName: id, exists: false, durationMs }, "secret exists check");
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async removeSecret(id) {
|
|
262
|
-
const start = performance.now();
|
|
263
|
-
this.log.debug({ secretName: id }, "removing secret");
|
|
264
|
-
await this.docker.getSecret(id).remove();
|
|
265
|
-
const durationMs = Math.round(performance.now() - start);
|
|
266
|
-
this.log.debug({ secretName: id, durationMs }, "secret removed");
|
|
267
|
-
log.step(`Removed ${id}`);
|
|
268
|
-
}
|
|
269
|
-
async storeSecretData(name, value, labels = {}) {
|
|
270
|
-
this.log.debug({ secretName: name }, "storing secret data");
|
|
271
|
-
await this.docker.createSecret({
|
|
272
|
-
Name: name,
|
|
273
|
-
Data: Buffer.from(value).toString("base64"),
|
|
274
|
-
Labels: {
|
|
275
|
-
"com.docker.stack.namespace": this.project,
|
|
276
|
-
"vyft.managed": "true",
|
|
277
|
-
"vyft.project": this.project,
|
|
278
|
-
"vyft.type": "secret",
|
|
279
|
-
...labels,
|
|
280
|
-
},
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
async lookupSecretId(name) {
|
|
284
|
-
this.log.debug({ secretName: name }, "looking up secret id");
|
|
285
|
-
const secret = await this.docker.getSecret(name).inspect();
|
|
286
|
-
return secret.ID;
|
|
287
|
-
}
|
|
288
|
-
async createDerivedSecret(name, value, parentService) {
|
|
289
|
-
this.log.debug({ secretName: name, parentService }, "creating derived secret");
|
|
290
|
-
await this.storeSecretData(name, value, {
|
|
291
|
-
"vyft.derived": "true",
|
|
292
|
-
"vyft.parent-service": parentService,
|
|
293
|
-
});
|
|
294
|
-
log.step(`Created ${name}`);
|
|
295
|
-
}
|
|
296
|
-
async resolveEnv(serviceId, env) {
|
|
297
|
-
const envList = [];
|
|
298
|
-
const secrets = [];
|
|
299
|
-
for (const [key, value] of Object.entries(env)) {
|
|
300
|
-
if (typeof value === "string") {
|
|
301
|
-
envList.push(`${key}=${value}`);
|
|
302
|
-
}
|
|
303
|
-
else if (isReference(value)) {
|
|
304
|
-
const secretId = await this.lookupSecretId(value.id);
|
|
305
|
-
secrets.push(secretMount(secretId, value.id));
|
|
306
|
-
const envKey = key.endsWith("_FILE") ? key : `${key}_FILE`;
|
|
307
|
-
envList.push(`${envKey}=/run/secrets/${value.id}`);
|
|
308
|
-
}
|
|
309
|
-
else if (isInterpolation(value)) {
|
|
310
|
-
const derivedName = `${serviceId}-${key.toLowerCase().replace(/_/g, "-")}`;
|
|
311
|
-
// Check if all referenced secrets have their values available
|
|
312
|
-
let allValuesAvailable = true;
|
|
313
|
-
for (const v of value.values) {
|
|
314
|
-
if (isReference(v) && !this.secretValues.has(v.id)) {
|
|
315
|
-
allValuesAvailable = false;
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (allValuesAvailable) {
|
|
320
|
-
// Build the interpolated value and create/update the derived secret
|
|
321
|
-
const parts = [];
|
|
322
|
-
for (let i = 0; i < value.strings.length; i++) {
|
|
323
|
-
parts.push(value.strings[i]);
|
|
324
|
-
if (i < value.values.length) {
|
|
325
|
-
const v = value.values[i];
|
|
326
|
-
if (isReference(v)) {
|
|
327
|
-
parts.push(this.secretValues.get(v.id));
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
parts.push(v);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
await this.createDerivedSecret(derivedName, parts.join(""), serviceId);
|
|
335
|
-
}
|
|
336
|
-
else if (!(await this.secretExists(derivedName))) {
|
|
337
|
-
// Source secret values not in memory and derived secret doesn't exist
|
|
338
|
-
const missing = value.values
|
|
339
|
-
.filter((v) => isReference(v) && !this.secretValues.has(v.id))
|
|
340
|
-
.map((v) => v.id);
|
|
341
|
-
throw new Error(`Secret(s) ${missing.join(", ")} value not available and derived secret "${derivedName}" does not exist — destroy and redeploy to regenerate`);
|
|
342
|
-
}
|
|
343
|
-
const derivedId = await this.lookupSecretId(derivedName);
|
|
344
|
-
secrets.push(secretMount(derivedId, derivedName));
|
|
345
|
-
const envKey = key.endsWith("_FILE") ? key : `${key}_FILE`;
|
|
346
|
-
envList.push(`${envKey}=/run/secrets/${derivedName}`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
this.log.debug({ serviceId, envCount: envList.length, secretMountCount: secrets.length }, "env resolved");
|
|
350
|
-
return { envList, secrets };
|
|
351
|
-
}
|
|
352
|
-
async createService(service) {
|
|
353
|
-
const start = performance.now();
|
|
354
|
-
await this.ensureNetwork(`${this.project}-internal`, {
|
|
355
|
-
"com.docker.stack.namespace": this.project,
|
|
356
|
-
"vyft.managed": "true",
|
|
357
|
-
"vyft.project": this.project,
|
|
358
|
-
});
|
|
359
|
-
const config = service.config;
|
|
360
|
-
let imageName;
|
|
361
|
-
if (typeof config.image === "object") {
|
|
362
|
-
const { context = ".", dockerfile = "Dockerfile" } = config.image;
|
|
363
|
-
const buildTag = `${service.id}:latest`;
|
|
364
|
-
this.log.debug({ resourceId: service.id, context, dockerfile }, "image build started");
|
|
365
|
-
const buildStart = performance.now();
|
|
366
|
-
const tarStream = tar.pack(path.resolve(context));
|
|
367
|
-
const stream = await this.localDocker.buildImage(tarStream, {
|
|
368
|
-
t: buildTag,
|
|
369
|
-
dockerfile,
|
|
370
|
-
});
|
|
371
|
-
await new Promise((resolve, reject) => {
|
|
372
|
-
this.localDocker.modem.followProgress(stream, (err) => (err ? reject(err) : resolve()), (event) => {
|
|
373
|
-
if (event.error)
|
|
374
|
-
log.error(event.error);
|
|
375
|
-
else if (this.verbose && event.stream)
|
|
376
|
-
process.stdout.write(event.stream);
|
|
377
|
-
this.log.trace({ event }, "build progress");
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
const buildDurationMs = Math.round(performance.now() - buildStart);
|
|
381
|
-
this.log.debug({
|
|
382
|
-
resourceId: service.id,
|
|
383
|
-
image: buildTag,
|
|
384
|
-
durationMs: buildDurationMs,
|
|
385
|
-
}, "image build completed");
|
|
386
|
-
const images = await this.localDocker.listImages({
|
|
387
|
-
filters: JSON.stringify({ reference: [buildTag] }),
|
|
388
|
-
});
|
|
389
|
-
if (images.length === 0) {
|
|
390
|
-
throw new Error(`Image ${buildTag} was not built successfully`);
|
|
391
|
-
}
|
|
392
|
-
// Tag with content hash so the orchestrator detects the new image
|
|
393
|
-
const shortHash = images[0].Id.replace("sha256:", "").slice(0, 12);
|
|
394
|
-
imageName = `${service.id}:${shortHash}`;
|
|
395
|
-
await this.localDocker.getImage(buildTag).tag({
|
|
396
|
-
repo: service.id,
|
|
397
|
-
tag: shortHash,
|
|
398
|
-
});
|
|
399
|
-
if (this.isRemote) {
|
|
400
|
-
this.log.debug({ image: imageName }, "transferring image to remote");
|
|
401
|
-
const transferStart = performance.now();
|
|
402
|
-
const imageStream = await this.localDocker.getImage(imageName).get();
|
|
403
|
-
await this.docker.loadImage(imageStream);
|
|
404
|
-
const transferDurationMs = Math.round(performance.now() - transferStart);
|
|
405
|
-
this.log.debug({ image: imageName, durationMs: transferDurationMs }, "image transferred to remote");
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
imageName = config.image;
|
|
410
|
-
}
|
|
411
|
-
// Build labels for service and container
|
|
412
|
-
const serviceLabels = {
|
|
413
|
-
"com.docker.stack.namespace": this.project,
|
|
414
|
-
"vyft.managed": "true",
|
|
415
|
-
"vyft.project": this.project,
|
|
416
|
-
"vyft.type": "service",
|
|
417
|
-
};
|
|
418
|
-
const containerLabels = {
|
|
419
|
-
"com.docker.stack.namespace": this.project,
|
|
420
|
-
"vyft.managed": "true",
|
|
421
|
-
"vyft.project": this.project,
|
|
422
|
-
};
|
|
423
|
-
if (config.route) {
|
|
424
|
-
serviceLabels["vyft.route"] = config.route;
|
|
425
|
-
}
|
|
426
|
-
const resolved = config.env
|
|
427
|
-
? await this.resolveEnv(service.id, config.env)
|
|
428
|
-
: undefined;
|
|
429
|
-
const containerSpec = {
|
|
430
|
-
Image: imageName,
|
|
431
|
-
Labels: containerLabels,
|
|
432
|
-
Command: config.command,
|
|
433
|
-
Env: resolved?.envList,
|
|
434
|
-
Mounts: config.volumes?.map(({ volume, mount }) => ({
|
|
435
|
-
Type: "volume",
|
|
436
|
-
Source: volume.id,
|
|
437
|
-
Target: mount,
|
|
438
|
-
})),
|
|
439
|
-
Secrets: resolved?.secrets.length ? resolved.secrets : undefined,
|
|
440
|
-
};
|
|
441
|
-
if (config.healthCheck) {
|
|
442
|
-
containerSpec.HealthCheck = {
|
|
443
|
-
Test: ["CMD", ...config.healthCheck.command],
|
|
444
|
-
Interval: config.healthCheck.interval
|
|
445
|
-
? parseDuration(config.healthCheck.interval)
|
|
446
|
-
: undefined,
|
|
447
|
-
Timeout: config.healthCheck.timeout
|
|
448
|
-
? parseDuration(config.healthCheck.timeout)
|
|
449
|
-
: undefined,
|
|
450
|
-
Retries: config.healthCheck.retries,
|
|
451
|
-
StartPeriod: config.healthCheck.startPeriod
|
|
452
|
-
? parseDuration(config.healthCheck.startPeriod)
|
|
453
|
-
: undefined,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
const networks = [
|
|
457
|
-
{ Target: `${this.project}-internal` },
|
|
458
|
-
];
|
|
459
|
-
if (config.route) {
|
|
460
|
-
networks.push({ Target: "vyft-network" });
|
|
461
|
-
}
|
|
462
|
-
const taskTemplate = {
|
|
463
|
-
ContainerSpec: containerSpec,
|
|
464
|
-
Networks: networks,
|
|
465
|
-
};
|
|
466
|
-
if (config.resources) {
|
|
467
|
-
taskTemplate.Resources = {
|
|
468
|
-
Limits: {
|
|
469
|
-
MemoryBytes: config.resources.memory
|
|
470
|
-
? parseMemory(config.resources.memory)
|
|
471
|
-
: undefined,
|
|
472
|
-
NanoCPUs: config.resources.cpus
|
|
473
|
-
? config.resources.cpus * 1e9
|
|
474
|
-
: undefined,
|
|
475
|
-
},
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
if (config.restartPolicy) {
|
|
479
|
-
const conditionMap = {
|
|
480
|
-
none: "none",
|
|
481
|
-
"on-failure": "on-failure",
|
|
482
|
-
any: "any",
|
|
483
|
-
};
|
|
484
|
-
taskTemplate.RestartPolicy = {
|
|
485
|
-
Condition: conditionMap[config.restartPolicy],
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
const serviceSpec = {
|
|
489
|
-
Name: service.id,
|
|
490
|
-
Labels: serviceLabels,
|
|
491
|
-
TaskTemplate: taskTemplate,
|
|
492
|
-
Mode: { Replicated: { Replicas: config.replicas ?? 1 } },
|
|
493
|
-
// Only publish ports directly if no route (internal service or direct access)
|
|
494
|
-
EndpointSpec: !config.route && config.port
|
|
495
|
-
? {
|
|
496
|
-
Ports: [
|
|
497
|
-
{
|
|
498
|
-
Protocol: "tcp",
|
|
499
|
-
TargetPort: config.port,
|
|
500
|
-
PublishedPort: config.port,
|
|
501
|
-
PublishMode: "ingress",
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
}
|
|
505
|
-
: undefined,
|
|
506
|
-
};
|
|
507
|
-
this.log.trace({ serviceSpec }, "docker create service spec");
|
|
508
|
-
const existing = await this.serviceExists(service.id);
|
|
509
|
-
if (existing) {
|
|
510
|
-
this.log.debug({ resourceId: service.id }, "updating existing service");
|
|
511
|
-
const svc = this.docker.getService(service.id);
|
|
512
|
-
const info = await svc.inspect();
|
|
513
|
-
await svc.update({
|
|
514
|
-
...serviceSpec,
|
|
515
|
-
version: info.Version.Index,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
this.log.debug({ resourceId: service.id }, "creating new service");
|
|
520
|
-
await this.docker.createService(serviceSpec);
|
|
521
|
-
}
|
|
522
|
-
if (config.route) {
|
|
523
|
-
const port = config.port || 3000;
|
|
524
|
-
await this.proxy.addRoute(service.id, config.route, {
|
|
525
|
-
host: service.id,
|
|
526
|
-
port,
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
const durationMs = Math.round(performance.now() - start);
|
|
530
|
-
this.log.info({
|
|
531
|
-
resourceId: service.id,
|
|
532
|
-
image: imageName,
|
|
533
|
-
hasRoute: !!config.route,
|
|
534
|
-
durationMs,
|
|
535
|
-
}, existing ? "service updated" : "service created");
|
|
536
|
-
log.step(`${existing ? "Updated" : "Created"} ${service.id}`);
|
|
537
|
-
}
|
|
538
|
-
async serviceExists(id) {
|
|
539
|
-
try {
|
|
540
|
-
await this.docker.getService(id).inspect();
|
|
541
|
-
return true;
|
|
542
|
-
}
|
|
543
|
-
catch (err) {
|
|
544
|
-
this.log.debug({ err, resourceId: id }, "service does not exist");
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
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
|
-
}
|
|
565
|
-
async removeService(id) {
|
|
566
|
-
const start = performance.now();
|
|
567
|
-
this.log.debug({ resourceId: id }, "removing service");
|
|
568
|
-
await this.proxy.removeRoute(id);
|
|
569
|
-
await this.docker.getService(id).remove();
|
|
570
|
-
const maxWait = 60000;
|
|
571
|
-
const interval = 2000;
|
|
572
|
-
let waited = 0;
|
|
573
|
-
this.log.debug({ resourceId: id, maxWait }, "container cleanup polling started");
|
|
574
|
-
while (waited < maxWait) {
|
|
575
|
-
const containers = await this.docker.listContainers({
|
|
576
|
-
all: true,
|
|
577
|
-
filters: JSON.stringify({
|
|
578
|
-
label: [`com.docker.swarm.service.name=${id}`],
|
|
579
|
-
}),
|
|
580
|
-
});
|
|
581
|
-
if (containers.length === 0)
|
|
582
|
-
break;
|
|
583
|
-
if (waited >= maxWait * 0.8) {
|
|
584
|
-
this.log.warn({ resourceId: id, waited, maxWait, remaining: containers.length }, "cleanup polling nearing timeout");
|
|
585
|
-
}
|
|
586
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
587
|
-
waited += interval;
|
|
588
|
-
}
|
|
589
|
-
const durationMs = Math.round(performance.now() - start);
|
|
590
|
-
this.log.info({ resourceId: id, durationMs }, "service removed");
|
|
591
|
-
log.step(`Removed ${id}`);
|
|
592
|
-
}
|
|
593
|
-
async pullImage(image) {
|
|
594
|
-
this.log.debug({ image }, "pulling image");
|
|
595
|
-
const stream = await this.docker.pull(image);
|
|
596
|
-
await new Promise((resolve, reject) => {
|
|
597
|
-
this.docker.modem.followProgress(stream, (err) => err ? reject(err) : resolve());
|
|
598
|
-
});
|
|
599
|
-
this.log.debug({ image }, "image pulled");
|
|
600
|
-
}
|
|
601
|
-
async createStatic(static_) {
|
|
602
|
-
const start = performance.now();
|
|
603
|
-
await this.ensureNetwork(`${this.project}-internal`, {
|
|
604
|
-
"com.docker.stack.namespace": this.project,
|
|
605
|
-
"vyft.managed": "true",
|
|
606
|
-
"vyft.project": this.project,
|
|
607
|
-
});
|
|
608
|
-
this.log.debug({ resourceId: static_.id, route: static_.config.route }, "creating static site");
|
|
609
|
-
this.log.debug({ resourceId: static_.id }, "static build starting");
|
|
610
|
-
const { outputPath } = await buildStatic(static_.config.build.cwd, {
|
|
611
|
-
output: static_.config.build.output,
|
|
612
|
-
command: static_.config.build.command,
|
|
613
|
-
env: static_.config.build.env,
|
|
614
|
-
}, this.log);
|
|
615
|
-
const volumeName = `${static_.id}-files`;
|
|
616
|
-
if (!(await this.volumeExists(volumeName))) {
|
|
617
|
-
this.log.debug({ volumeName }, "creating static volume");
|
|
618
|
-
await this.docker.createVolume({
|
|
619
|
-
Name: volumeName,
|
|
620
|
-
Labels: {
|
|
621
|
-
"com.docker.stack.namespace": this.project,
|
|
622
|
-
"vyft.managed": "true",
|
|
623
|
-
"vyft.project": this.project,
|
|
624
|
-
"vyft.type": "volume",
|
|
625
|
-
"vyft.static": static_.id,
|
|
626
|
-
},
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
this.log.debug({ resourceId: static_.id, outputPath }, "tar copy started");
|
|
630
|
-
await this.pullImage("alpine:latest");
|
|
631
|
-
const tarStream = tar.pack(outputPath);
|
|
632
|
-
const container = await this.docker.createContainer({
|
|
633
|
-
Image: "alpine:latest",
|
|
634
|
-
Cmd: ["sleep", "3600"],
|
|
635
|
-
HostConfig: {
|
|
636
|
-
Mounts: [{ Type: "volume", Source: volumeName, Target: "/data" }],
|
|
637
|
-
},
|
|
638
|
-
});
|
|
639
|
-
await container.start();
|
|
640
|
-
// Clear old files before copying new ones
|
|
641
|
-
await container.exec({
|
|
642
|
-
Cmd: ["sh", "-c", "rm -rf /data/*"],
|
|
643
|
-
AttachStdout: true,
|
|
644
|
-
AttachStderr: true,
|
|
645
|
-
});
|
|
646
|
-
await container.putArchive(tarStream, { path: "/data" });
|
|
647
|
-
await container.remove({ force: true });
|
|
648
|
-
this.log.debug({ resourceId: static_.id }, "tar copy completed");
|
|
649
|
-
const serviceLabels = {
|
|
650
|
-
"com.docker.stack.namespace": this.project,
|
|
651
|
-
"vyft.managed": "true",
|
|
652
|
-
"vyft.project": this.project,
|
|
653
|
-
"vyft.type": "site",
|
|
654
|
-
"vyft.route": static_.config.route,
|
|
655
|
-
};
|
|
656
|
-
const containerLabels = {
|
|
657
|
-
"com.docker.stack.namespace": this.project,
|
|
658
|
-
"vyft.managed": "true",
|
|
659
|
-
"vyft.project": this.project,
|
|
660
|
-
};
|
|
661
|
-
const command = static_.config.spa === false
|
|
662
|
-
? ["caddy", "file-server", "--root", "/srv", "--listen", ":80"]
|
|
663
|
-
: [
|
|
664
|
-
"sh",
|
|
665
|
-
"-c",
|
|
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",
|
|
667
|
-
];
|
|
668
|
-
const serviceSpec = {
|
|
669
|
-
Name: static_.id,
|
|
670
|
-
Labels: serviceLabels,
|
|
671
|
-
TaskTemplate: {
|
|
672
|
-
ContainerSpec: {
|
|
673
|
-
Image: "caddy:latest",
|
|
674
|
-
Command: command,
|
|
675
|
-
Mounts: [
|
|
676
|
-
{ Type: "volume", Source: volumeName, Target: "/srv" },
|
|
677
|
-
],
|
|
678
|
-
Labels: containerLabels,
|
|
679
|
-
},
|
|
680
|
-
Networks: [
|
|
681
|
-
{ Target: `${this.project}-internal` },
|
|
682
|
-
{ Target: "vyft-network" },
|
|
683
|
-
],
|
|
684
|
-
},
|
|
685
|
-
Mode: { Replicated: { Replicas: 1 } },
|
|
686
|
-
};
|
|
687
|
-
const existing = await this.serviceExists(static_.id);
|
|
688
|
-
if (existing) {
|
|
689
|
-
this.log.debug({ resourceId: static_.id }, "updating static site service");
|
|
690
|
-
const svc = this.docker.getService(static_.id);
|
|
691
|
-
const info = await svc.inspect();
|
|
692
|
-
await svc.update({
|
|
693
|
-
...serviceSpec,
|
|
694
|
-
version: info.Version.Index,
|
|
695
|
-
TaskTemplate: {
|
|
696
|
-
...serviceSpec.TaskTemplate,
|
|
697
|
-
ForceUpdate: (info.Spec?.TaskTemplate?.ForceUpdate ?? 0) + 1,
|
|
698
|
-
},
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
else {
|
|
702
|
-
this.log.debug({ resourceId: static_.id }, "creating caddy service for static site");
|
|
703
|
-
await this.docker.createService(serviceSpec);
|
|
704
|
-
}
|
|
705
|
-
await this.proxy.addRoute(static_.id, static_.config.route, {
|
|
706
|
-
host: static_.id,
|
|
707
|
-
port: 80,
|
|
708
|
-
});
|
|
709
|
-
const durationMs = Math.round(performance.now() - start);
|
|
710
|
-
this.log.info({ resourceId: static_.id, route: static_.config.route, durationMs }, existing ? "site updated" : "site created");
|
|
711
|
-
log.step(`${existing ? "Updated" : "Created"} ${static_.id}`);
|
|
712
|
-
}
|
|
713
|
-
async removeStatic(id) {
|
|
714
|
-
const start = performance.now();
|
|
715
|
-
this.log.debug({ resourceId: id }, "removing static site");
|
|
716
|
-
await this.proxy.removeRoute(id);
|
|
717
|
-
await this.docker.getService(id).remove();
|
|
718
|
-
const maxWait = 30000;
|
|
719
|
-
const interval = 2000;
|
|
720
|
-
let waited = 0;
|
|
721
|
-
this.log.debug({ resourceId: id, maxWait }, "static cleanup polling started");
|
|
722
|
-
while (waited < maxWait) {
|
|
723
|
-
const containers = await this.docker.listContainers({
|
|
724
|
-
all: true,
|
|
725
|
-
filters: JSON.stringify({
|
|
726
|
-
label: [`com.docker.swarm.service.name=${id}`],
|
|
727
|
-
}),
|
|
728
|
-
});
|
|
729
|
-
if (containers.length === 0)
|
|
730
|
-
break;
|
|
731
|
-
if (waited >= maxWait * 0.8) {
|
|
732
|
-
this.log.warn({ resourceId: id, waited, maxWait, remaining: containers.length }, "cleanup polling nearing timeout");
|
|
733
|
-
}
|
|
734
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
735
|
-
waited += interval;
|
|
736
|
-
}
|
|
737
|
-
const volumeName = `${id}-files`;
|
|
738
|
-
try {
|
|
739
|
-
this.log.debug({ volumeName }, "removing static volume");
|
|
740
|
-
await this.docker.getVolume(volumeName).remove();
|
|
741
|
-
}
|
|
742
|
-
catch (err) {
|
|
743
|
-
this.log.debug({ err, volumeName }, "static volume removal failed (may not exist)");
|
|
744
|
-
}
|
|
745
|
-
const durationMs = Math.round(performance.now() - start);
|
|
746
|
-
this.log.info({ resourceId: id, durationMs }, "site removed");
|
|
747
|
-
log.step(`Removed ${id}`);
|
|
748
|
-
}
|
|
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
|
-
}
|
|
771
|
-
const tail = options.tail ?? 100;
|
|
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) {
|
|
824
|
-
const stream = await container.logs({
|
|
825
|
-
stdout: true,
|
|
826
|
-
stderr: true,
|
|
827
|
-
follow: true,
|
|
828
|
-
tail,
|
|
829
|
-
timestamps: true,
|
|
830
|
-
});
|
|
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
|
-
}
|
|
838
|
-
});
|
|
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 });
|
|
843
|
-
}
|
|
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();
|
|
852
|
-
}
|
|
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();
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
async removeProjectNetwork() {
|
|
867
|
-
const name = `${this.project}-internal`;
|
|
868
|
-
try {
|
|
869
|
-
await this.docker.getNetwork(name).remove();
|
|
870
|
-
this.log.debug({ network: name }, "project network removed");
|
|
871
|
-
}
|
|
872
|
-
catch {
|
|
873
|
-
this.log.debug({ network: name }, "project network removal skipped");
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|