genbox 1.0.2 → 1.0.4
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/dist/commands/create.js +369 -130
- package/dist/commands/db-sync.js +364 -0
- package/dist/commands/destroy.js +5 -10
- package/dist/commands/init.js +669 -402
- package/dist/commands/profiles.js +333 -0
- package/dist/commands/push.js +140 -47
- package/dist/config-loader.js +529 -0
- package/dist/genbox-selector.js +5 -8
- package/dist/index.js +5 -1
- package/dist/profile-resolver.js +547 -0
- package/dist/scanner/compose-parser.js +441 -0
- package/dist/scanner/config-generator.js +620 -0
- package/dist/scanner/env-analyzer.js +503 -0
- package/dist/scanner/framework-detector.js +621 -0
- package/dist/scanner/index.js +424 -0
- package/dist/scanner/runtime-detector.js +330 -0
- package/dist/scanner/structure-detector.js +412 -0
- package/dist/scanner/types.js +7 -0
- package/dist/schema-v3.js +12 -0
- package/package.json +4 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Docker Compose Parser
|
|
4
|
+
*
|
|
5
|
+
* Parses docker-compose files to extract:
|
|
6
|
+
* - Services (applications, databases, infrastructure)
|
|
7
|
+
* - Port mappings
|
|
8
|
+
* - Environment variables
|
|
9
|
+
* - Dependencies between services
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.ComposeParser = void 0;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const yaml = __importStar(require("js-yaml"));
|
|
49
|
+
// Known database images
|
|
50
|
+
const DATABASE_PATTERNS = [
|
|
51
|
+
/^mongo(db)?/i,
|
|
52
|
+
/^postgres/i,
|
|
53
|
+
/^mysql/i,
|
|
54
|
+
/^mariadb/i,
|
|
55
|
+
/^cockroach/i,
|
|
56
|
+
/^cassandra/i,
|
|
57
|
+
/^dynamodb/i,
|
|
58
|
+
/^couchdb/i,
|
|
59
|
+
/^neo4j/i,
|
|
60
|
+
/^arangodb/i,
|
|
61
|
+
];
|
|
62
|
+
// Known cache images
|
|
63
|
+
const CACHE_PATTERNS = [
|
|
64
|
+
/^redis/i,
|
|
65
|
+
/^memcache/i,
|
|
66
|
+
/^hazelcast/i,
|
|
67
|
+
/^keydb/i,
|
|
68
|
+
];
|
|
69
|
+
// Known queue/message broker images
|
|
70
|
+
const QUEUE_PATTERNS = [
|
|
71
|
+
/^rabbitmq/i,
|
|
72
|
+
/^kafka/i,
|
|
73
|
+
/^nats/i,
|
|
74
|
+
/^pulsar/i,
|
|
75
|
+
/^activemq/i,
|
|
76
|
+
/^zeromq/i,
|
|
77
|
+
];
|
|
78
|
+
// Known infrastructure images
|
|
79
|
+
const INFRA_PATTERNS = [
|
|
80
|
+
/^nginx/i,
|
|
81
|
+
/^traefik/i,
|
|
82
|
+
/^caddy/i,
|
|
83
|
+
/^haproxy/i,
|
|
84
|
+
/^envoy/i,
|
|
85
|
+
/^mailhog/i,
|
|
86
|
+
/^localstack/i,
|
|
87
|
+
/^minio/i,
|
|
88
|
+
/^elasticsearch/i,
|
|
89
|
+
/^kibana/i,
|
|
90
|
+
/^grafana/i,
|
|
91
|
+
/^prometheus/i,
|
|
92
|
+
/^jaeger/i,
|
|
93
|
+
/^zipkin/i,
|
|
94
|
+
/^vault/i,
|
|
95
|
+
/^consul/i,
|
|
96
|
+
];
|
|
97
|
+
class ComposeParser {
|
|
98
|
+
/**
|
|
99
|
+
* Parse docker-compose files in the project
|
|
100
|
+
*/
|
|
101
|
+
async parse(root) {
|
|
102
|
+
const composeFiles = this.findComposeFiles(root);
|
|
103
|
+
if (composeFiles.length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const allServices = [];
|
|
107
|
+
for (const file of composeFiles) {
|
|
108
|
+
try {
|
|
109
|
+
const content = yaml.load(fs.readFileSync(file, 'utf8'));
|
|
110
|
+
if (content && content.services) {
|
|
111
|
+
for (const [name, config] of Object.entries(content.services)) {
|
|
112
|
+
const service = this.normalizeService(name, config, root);
|
|
113
|
+
allServices.push(service);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.warn(`Warning: Could not parse ${file}:`, err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Categorize services
|
|
122
|
+
const categorized = this.categorizeServices(allServices);
|
|
123
|
+
// Build port map
|
|
124
|
+
const portMap = new Map();
|
|
125
|
+
for (const service of allServices) {
|
|
126
|
+
for (const port of service.ports) {
|
|
127
|
+
portMap.set(port.host, service.name);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Build dependency graph
|
|
131
|
+
const dependencyGraph = new Map();
|
|
132
|
+
for (const service of allServices) {
|
|
133
|
+
dependencyGraph.set(service.name, service.dependsOn);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
files: composeFiles,
|
|
137
|
+
...categorized,
|
|
138
|
+
portMap,
|
|
139
|
+
dependencyGraph,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
findComposeFiles(root) {
|
|
143
|
+
const files = [];
|
|
144
|
+
const patterns = [
|
|
145
|
+
'docker-compose.yml',
|
|
146
|
+
'docker-compose.yaml',
|
|
147
|
+
'docker-compose.dev.yml',
|
|
148
|
+
'docker-compose.dev.yaml',
|
|
149
|
+
'docker-compose.local.yml',
|
|
150
|
+
'docker-compose.local.yaml',
|
|
151
|
+
'docker-compose.override.yml',
|
|
152
|
+
'docker-compose.override.yaml',
|
|
153
|
+
'docker-compose.secure.yml',
|
|
154
|
+
'docker-compose.secure.yaml',
|
|
155
|
+
'compose.yml',
|
|
156
|
+
'compose.yaml',
|
|
157
|
+
];
|
|
158
|
+
for (const pattern of patterns) {
|
|
159
|
+
const filePath = path.join(root, pattern);
|
|
160
|
+
if (fs.existsSync(filePath)) {
|
|
161
|
+
files.push(filePath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return files;
|
|
165
|
+
}
|
|
166
|
+
normalizeService(name, config, root) {
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
image: config.image,
|
|
170
|
+
build: this.normalizeBuild(config.build, root),
|
|
171
|
+
ports: this.normalizePorts(config.ports),
|
|
172
|
+
environment: this.normalizeEnvironment(config.environment),
|
|
173
|
+
envFile: this.normalizeEnvFile(config.env_file),
|
|
174
|
+
dependsOn: this.normalizeDependsOn(config.depends_on),
|
|
175
|
+
volumes: this.normalizeVolumes(config.volumes),
|
|
176
|
+
healthcheck: this.normalizeHealthcheck(config.healthcheck),
|
|
177
|
+
command: this.normalizeCommand(config.command),
|
|
178
|
+
labels: this.normalizeLabels(config.labels),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
normalizeBuild(build, root) {
|
|
182
|
+
if (!build)
|
|
183
|
+
return undefined;
|
|
184
|
+
if (typeof build === 'string') {
|
|
185
|
+
return {
|
|
186
|
+
context: path.resolve(root, build),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (typeof build === 'object') {
|
|
190
|
+
const b = build;
|
|
191
|
+
return {
|
|
192
|
+
context: path.resolve(root, b.context || '.'),
|
|
193
|
+
dockerfile: b.dockerfile,
|
|
194
|
+
target: b.target,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
normalizePorts(ports) {
|
|
200
|
+
if (!ports || !Array.isArray(ports))
|
|
201
|
+
return [];
|
|
202
|
+
const mappings = [];
|
|
203
|
+
for (const port of ports) {
|
|
204
|
+
if (typeof port === 'string') {
|
|
205
|
+
// Parse "3000:3000" or "3000:3000/tcp" or "3000"
|
|
206
|
+
const match = port.match(/^(\d+)(?::(\d+))?(?:\/(\w+))?$/);
|
|
207
|
+
if (match) {
|
|
208
|
+
const host = parseInt(match[1], 10);
|
|
209
|
+
const container = match[2] ? parseInt(match[2], 10) : host;
|
|
210
|
+
mappings.push({
|
|
211
|
+
host,
|
|
212
|
+
container,
|
|
213
|
+
protocol: match[3] || 'tcp',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (typeof port === 'object') {
|
|
218
|
+
const p = port;
|
|
219
|
+
if (p.published && p.target) {
|
|
220
|
+
mappings.push({
|
|
221
|
+
host: p.published,
|
|
222
|
+
container: p.target,
|
|
223
|
+
protocol: p.protocol || 'tcp',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (typeof port === 'number') {
|
|
228
|
+
mappings.push({ host: port, container: port });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return mappings;
|
|
232
|
+
}
|
|
233
|
+
normalizeEnvironment(env) {
|
|
234
|
+
if (!env)
|
|
235
|
+
return {};
|
|
236
|
+
if (Array.isArray(env)) {
|
|
237
|
+
// ["KEY=value", "KEY2=value2"]
|
|
238
|
+
const result = {};
|
|
239
|
+
for (const item of env) {
|
|
240
|
+
if (typeof item === 'string') {
|
|
241
|
+
const [key, ...valueParts] = item.split('=');
|
|
242
|
+
result[key] = valueParts.join('=');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
if (typeof env === 'object') {
|
|
248
|
+
// { KEY: "value" }
|
|
249
|
+
return env;
|
|
250
|
+
}
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
normalizeEnvFile(envFile) {
|
|
254
|
+
if (!envFile)
|
|
255
|
+
return undefined;
|
|
256
|
+
if (typeof envFile === 'string') {
|
|
257
|
+
return [envFile];
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(envFile)) {
|
|
260
|
+
return envFile.filter(f => typeof f === 'string');
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
normalizeDependsOn(dependsOn) {
|
|
265
|
+
if (!dependsOn)
|
|
266
|
+
return [];
|
|
267
|
+
if (Array.isArray(dependsOn)) {
|
|
268
|
+
return dependsOn.filter(d => typeof d === 'string');
|
|
269
|
+
}
|
|
270
|
+
if (typeof dependsOn === 'object') {
|
|
271
|
+
// { service_name: { condition: "service_healthy" } }
|
|
272
|
+
return Object.keys(dependsOn);
|
|
273
|
+
}
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
normalizeVolumes(volumes) {
|
|
277
|
+
if (!volumes || !Array.isArray(volumes))
|
|
278
|
+
return [];
|
|
279
|
+
const mappings = [];
|
|
280
|
+
for (const vol of volumes) {
|
|
281
|
+
if (typeof vol === 'string') {
|
|
282
|
+
// Parse "./data:/data:ro" or "./data:/data"
|
|
283
|
+
const parts = vol.split(':');
|
|
284
|
+
if (parts.length >= 2) {
|
|
285
|
+
mappings.push({
|
|
286
|
+
source: parts[0],
|
|
287
|
+
target: parts[1],
|
|
288
|
+
readOnly: parts[2] === 'ro',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (typeof vol === 'object') {
|
|
293
|
+
const v = vol;
|
|
294
|
+
if (v.source && v.target) {
|
|
295
|
+
mappings.push({
|
|
296
|
+
source: v.source,
|
|
297
|
+
target: v.target,
|
|
298
|
+
readOnly: v.read_only,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return mappings;
|
|
304
|
+
}
|
|
305
|
+
normalizeHealthcheck(healthcheck) {
|
|
306
|
+
if (!healthcheck || typeof healthcheck !== 'object')
|
|
307
|
+
return undefined;
|
|
308
|
+
const hc = healthcheck;
|
|
309
|
+
let test = '';
|
|
310
|
+
if (Array.isArray(hc.test)) {
|
|
311
|
+
test = hc.test.join(' ');
|
|
312
|
+
}
|
|
313
|
+
else if (typeof hc.test === 'string') {
|
|
314
|
+
test = hc.test;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
test,
|
|
318
|
+
interval: hc.interval,
|
|
319
|
+
timeout: hc.timeout,
|
|
320
|
+
retries: hc.retries,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
normalizeCommand(command) {
|
|
324
|
+
if (!command)
|
|
325
|
+
return undefined;
|
|
326
|
+
if (typeof command === 'string') {
|
|
327
|
+
return command;
|
|
328
|
+
}
|
|
329
|
+
if (Array.isArray(command)) {
|
|
330
|
+
return command.join(' ');
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
normalizeLabels(labels) {
|
|
335
|
+
if (!labels)
|
|
336
|
+
return undefined;
|
|
337
|
+
if (Array.isArray(labels)) {
|
|
338
|
+
// ["key=value"]
|
|
339
|
+
const result = {};
|
|
340
|
+
for (const item of labels) {
|
|
341
|
+
if (typeof item === 'string') {
|
|
342
|
+
const [key, ...valueParts] = item.split('=');
|
|
343
|
+
result[key] = valueParts.join('=');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
if (typeof labels === 'object') {
|
|
349
|
+
return labels;
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
categorizeServices(services) {
|
|
354
|
+
const applications = [];
|
|
355
|
+
const databases = [];
|
|
356
|
+
const caches = [];
|
|
357
|
+
const queues = [];
|
|
358
|
+
const infrastructure = [];
|
|
359
|
+
for (const service of services) {
|
|
360
|
+
const category = this.categorizeService(service);
|
|
361
|
+
switch (category) {
|
|
362
|
+
case 'application':
|
|
363
|
+
applications.push(service);
|
|
364
|
+
break;
|
|
365
|
+
case 'database':
|
|
366
|
+
databases.push(service);
|
|
367
|
+
break;
|
|
368
|
+
case 'cache':
|
|
369
|
+
caches.push(service);
|
|
370
|
+
break;
|
|
371
|
+
case 'queue':
|
|
372
|
+
queues.push(service);
|
|
373
|
+
break;
|
|
374
|
+
case 'infra':
|
|
375
|
+
infrastructure.push(service);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { applications, databases, caches, queues, infrastructure };
|
|
380
|
+
}
|
|
381
|
+
categorizeService(service) {
|
|
382
|
+
// Check labels first (explicit declaration)
|
|
383
|
+
if (service.labels?.['genbox.type']) {
|
|
384
|
+
return service.labels['genbox.type'];
|
|
385
|
+
}
|
|
386
|
+
const image = service.image?.toLowerCase() || '';
|
|
387
|
+
// Check against known patterns
|
|
388
|
+
if (DATABASE_PATTERNS.some(p => p.test(image))) {
|
|
389
|
+
return 'database';
|
|
390
|
+
}
|
|
391
|
+
if (CACHE_PATTERNS.some(p => p.test(image))) {
|
|
392
|
+
return 'cache';
|
|
393
|
+
}
|
|
394
|
+
if (QUEUE_PATTERNS.some(p => p.test(image))) {
|
|
395
|
+
return 'queue';
|
|
396
|
+
}
|
|
397
|
+
if (INFRA_PATTERNS.some(p => p.test(image))) {
|
|
398
|
+
return 'infra';
|
|
399
|
+
}
|
|
400
|
+
// If it has a build context, it's likely an application
|
|
401
|
+
if (service.build) {
|
|
402
|
+
return 'application';
|
|
403
|
+
}
|
|
404
|
+
// Default to infrastructure for other pre-built images
|
|
405
|
+
if (service.image) {
|
|
406
|
+
return 'infra';
|
|
407
|
+
}
|
|
408
|
+
return 'application';
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Get suggested genbox service name from compose service
|
|
412
|
+
*/
|
|
413
|
+
getGenboxServiceName(service) {
|
|
414
|
+
// Use label if available
|
|
415
|
+
if (service.labels?.['genbox.name']) {
|
|
416
|
+
return service.labels['genbox.name'];
|
|
417
|
+
}
|
|
418
|
+
// Clean up the name
|
|
419
|
+
return service.name
|
|
420
|
+
.replace(/[-_]service$/i, '')
|
|
421
|
+
.replace(/[-_]app$/i, '')
|
|
422
|
+
.replace(/[-_]api$/i, '-api')
|
|
423
|
+
.toLowerCase();
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get the primary port for a service
|
|
427
|
+
*/
|
|
428
|
+
getPrimaryPort(service) {
|
|
429
|
+
if (service.ports.length === 0)
|
|
430
|
+
return undefined;
|
|
431
|
+
// Prefer certain ports based on service category
|
|
432
|
+
const category = this.categorizeService(service);
|
|
433
|
+
// For applications, prefer the first exposed port
|
|
434
|
+
if (category === 'application') {
|
|
435
|
+
return service.ports[0].host;
|
|
436
|
+
}
|
|
437
|
+
// For databases/caches, use the container port
|
|
438
|
+
return service.ports[0].container;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
exports.ComposeParser = ComposeParser;
|