easy-containers 1.0.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/ .npmignore +13 -0
- package/Examples.md +101 -0
- package/Project structure.md +82 -0
- package/README.md +96 -0
- package/bin/cli.js +281 -0
- package/package.json +46 -0
- package/src/commands/config.js +339 -0
- package/src/commands/down.js +33 -0
- package/src/commands/download.js +23 -0
- package/src/commands/exec.js +93 -0
- package/src/commands/help.js +44 -0
- package/src/commands/index.js +17 -0
- package/src/commands/init.js +223 -0
- package/src/commands/list.js +149 -0
- package/src/commands/logs.js +76 -0
- package/src/commands/pull.js +38 -0
- package/src/commands/restart.js +45 -0
- package/src/commands/search.js +68 -0
- package/src/commands/show.js +116 -0
- package/src/commands/status.js +82 -0
- package/src/commands/up.js +29 -0
- package/src/commands/validate.js +151 -0
- package/src/constants/repository.js +12 -0
- package/src/utils/config.js +148 -0
- package/src/utils/docker.js +140 -0
- package/src/utils/downloader.js +478 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { getServicePath, ensureServicesDir } = require('./config');
|
|
8
|
+
const {
|
|
9
|
+
REPO_URL,
|
|
10
|
+
REPO_BRANCH,
|
|
11
|
+
SERVICES_BASE_PATH
|
|
12
|
+
} = require('../constants/repository');
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Download service from repository
|
|
18
|
+
* @param {string} service - Service name
|
|
19
|
+
* @returns {Promise<string>} - Path to downloaded service
|
|
20
|
+
*/
|
|
21
|
+
async function downloadService(service) {
|
|
22
|
+
const servicePath = getServicePath(service);
|
|
23
|
+
|
|
24
|
+
// Check if service already exists
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(servicePath);
|
|
27
|
+
console.log(chalk.gray(`Service ${service} already exists locally`));
|
|
28
|
+
return servicePath;
|
|
29
|
+
} catch {
|
|
30
|
+
// Service doesn't exist, need to download
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await ensureServicesDir();
|
|
34
|
+
|
|
35
|
+
const spinner = ora(`Downloading ${service} from repository...`).start();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Try to download from GitHub repo
|
|
39
|
+
await downloadFromGitRepo(service, servicePath, spinner);
|
|
40
|
+
spinner.succeed(chalk.green(`Downloaded ${service} successfully`));
|
|
41
|
+
return servicePath;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
spinner.warn(chalk.yellow(`Could not download from repository: ${error.message}`));
|
|
44
|
+
|
|
45
|
+
// Fallback to template creation
|
|
46
|
+
spinner.start(`Creating ${service} from template...`);
|
|
47
|
+
try {
|
|
48
|
+
await createServiceFromTemplate(service, servicePath);
|
|
49
|
+
spinner.succeed(chalk.green(`Created ${service} from template`));
|
|
50
|
+
return servicePath;
|
|
51
|
+
} catch (templateError) {
|
|
52
|
+
spinner.fail(chalk.red(`Failed to create service`));
|
|
53
|
+
throw new Error(`Failed to create service ${service}: ${templateError.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Download service from Git repository using sparse checkout
|
|
60
|
+
*/
|
|
61
|
+
async function downloadFromGitRepo(service, servicePath, spinner) {
|
|
62
|
+
const tempDir = path.join(servicePath, '..', `.tmp-${service}-${Date.now()}`);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Create temp directory
|
|
66
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
spinner.text = 'Initializing Git repository...';
|
|
69
|
+
|
|
70
|
+
// Initialize git repo
|
|
71
|
+
await execAsync('git init', { cwd: tempDir });
|
|
72
|
+
|
|
73
|
+
// Add remote
|
|
74
|
+
await execAsync(`git remote add origin ${REPO_URL}`, { cwd: tempDir });
|
|
75
|
+
|
|
76
|
+
// Enable sparse checkout
|
|
77
|
+
await execAsync('git config core.sparseCheckout true', { cwd: tempDir });
|
|
78
|
+
|
|
79
|
+
spinner.text = 'Configuring sparse checkout...';
|
|
80
|
+
|
|
81
|
+
// Specify which directory to checkout
|
|
82
|
+
const sparseCheckoutPath = path.join(tempDir, '.git', 'info', 'sparse-checkout');
|
|
83
|
+
await fs.writeFile(sparseCheckoutPath, `${SERVICES_BASE_PATH}/${service}/\n`);
|
|
84
|
+
|
|
85
|
+
spinner.text = `Downloading ${service}...`;
|
|
86
|
+
|
|
87
|
+
// Pull only the specified directory with depth 1 for speed
|
|
88
|
+
await execAsync(`git pull --depth=1 origin ${REPO_BRANCH}`, { cwd: tempDir });
|
|
89
|
+
|
|
90
|
+
spinner.text = 'Moving service files...';
|
|
91
|
+
|
|
92
|
+
// Check if service exists in repo
|
|
93
|
+
const sourceServicePath = path.join(tempDir, SERVICES_BASE_PATH, service);
|
|
94
|
+
try {
|
|
95
|
+
await fs.access(sourceServicePath);
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error(`Service "${service}" not found in repository`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Move service to final location
|
|
101
|
+
await fs.rename(sourceServicePath, servicePath);
|
|
102
|
+
|
|
103
|
+
// Clean up temp directory
|
|
104
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Clean up temp directory on error
|
|
108
|
+
try {
|
|
109
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
110
|
+
} catch {}
|
|
111
|
+
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create service from built-in template
|
|
118
|
+
* This is a fallback when the service is not in the repository
|
|
119
|
+
*/
|
|
120
|
+
async function createServiceFromTemplate(service, servicePath) {
|
|
121
|
+
await fs.mkdir(servicePath, { recursive: true });
|
|
122
|
+
|
|
123
|
+
// Define common service templates
|
|
124
|
+
const templates = {
|
|
125
|
+
postgres: {
|
|
126
|
+
compose: `version: '3.8'
|
|
127
|
+
|
|
128
|
+
services:
|
|
129
|
+
postgres:
|
|
130
|
+
image: postgres:15
|
|
131
|
+
container_name: ${service}_postgres
|
|
132
|
+
environment:
|
|
133
|
+
POSTGRES_PASSWORD: postgres
|
|
134
|
+
POSTGRES_USER: postgres
|
|
135
|
+
POSTGRES_DB: mydb
|
|
136
|
+
ports:
|
|
137
|
+
- "5432:5432"
|
|
138
|
+
volumes:
|
|
139
|
+
- postgres_data:/var/lib/postgresql/data
|
|
140
|
+
restart: unless-stopped
|
|
141
|
+
healthcheck:
|
|
142
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
143
|
+
interval: 10s
|
|
144
|
+
timeout: 5s
|
|
145
|
+
retries: 5
|
|
146
|
+
|
|
147
|
+
volumes:
|
|
148
|
+
postgres_data:
|
|
149
|
+
`,
|
|
150
|
+
readme: `# PostgreSQL Database
|
|
151
|
+
|
|
152
|
+
PostgreSQL 15 with persistent storage.
|
|
153
|
+
|
|
154
|
+
## Default Credentials
|
|
155
|
+
- Username: postgres
|
|
156
|
+
- Password: postgres
|
|
157
|
+
- Database: mydb
|
|
158
|
+
- Port: 5432
|
|
159
|
+
|
|
160
|
+
## Connect
|
|
161
|
+
\`\`\`bash
|
|
162
|
+
psql -h localhost -U postgres -d mydb
|
|
163
|
+
\`\`\`
|
|
164
|
+
`
|
|
165
|
+
},
|
|
166
|
+
mysql: {
|
|
167
|
+
compose: `version: '3.8'
|
|
168
|
+
|
|
169
|
+
services:
|
|
170
|
+
mysql:
|
|
171
|
+
image: mysql:8
|
|
172
|
+
container_name: ${service}_mysql
|
|
173
|
+
environment:
|
|
174
|
+
MYSQL_ROOT_PASSWORD: rootpassword
|
|
175
|
+
MYSQL_DATABASE: mydb
|
|
176
|
+
MYSQL_USER: user
|
|
177
|
+
MYSQL_PASSWORD: password
|
|
178
|
+
ports:
|
|
179
|
+
- "3306:3306"
|
|
180
|
+
volumes:
|
|
181
|
+
- mysql_data:/var/lib/mysql
|
|
182
|
+
restart: unless-stopped
|
|
183
|
+
healthcheck:
|
|
184
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
185
|
+
interval: 10s
|
|
186
|
+
timeout: 5s
|
|
187
|
+
retries: 5
|
|
188
|
+
|
|
189
|
+
volumes:
|
|
190
|
+
mysql_data:
|
|
191
|
+
`,
|
|
192
|
+
readme: `# MySQL Database
|
|
193
|
+
|
|
194
|
+
MySQL 8 with persistent storage.
|
|
195
|
+
|
|
196
|
+
## Default Credentials
|
|
197
|
+
- Root Password: rootpassword
|
|
198
|
+
- User: user
|
|
199
|
+
- Password: password
|
|
200
|
+
- Database: mydb
|
|
201
|
+
- Port: 3306
|
|
202
|
+
|
|
203
|
+
## Connect
|
|
204
|
+
\`\`\`bash
|
|
205
|
+
mysql -h localhost -u user -p mydb
|
|
206
|
+
\`\`\`
|
|
207
|
+
`
|
|
208
|
+
},
|
|
209
|
+
redis: {
|
|
210
|
+
compose: `version: '3.8'
|
|
211
|
+
|
|
212
|
+
services:
|
|
213
|
+
redis:
|
|
214
|
+
image: redis:7-alpine
|
|
215
|
+
container_name: ${service}_redis
|
|
216
|
+
ports:
|
|
217
|
+
- "6379:6379"
|
|
218
|
+
volumes:
|
|
219
|
+
- redis_data:/data
|
|
220
|
+
command: redis-server --appendonly yes
|
|
221
|
+
restart: unless-stopped
|
|
222
|
+
healthcheck:
|
|
223
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
224
|
+
interval: 10s
|
|
225
|
+
timeout: 5s
|
|
226
|
+
retries: 5
|
|
227
|
+
|
|
228
|
+
volumes:
|
|
229
|
+
redis_data:
|
|
230
|
+
`,
|
|
231
|
+
readme: `# Redis Cache
|
|
232
|
+
|
|
233
|
+
Redis 7 with AOF persistence.
|
|
234
|
+
|
|
235
|
+
## Connection
|
|
236
|
+
- Host: localhost
|
|
237
|
+
- Port: 6379
|
|
238
|
+
|
|
239
|
+
## Connect
|
|
240
|
+
\`\`\`bash
|
|
241
|
+
redis-cli
|
|
242
|
+
\`\`\`
|
|
243
|
+
`
|
|
244
|
+
},
|
|
245
|
+
mongodb: {
|
|
246
|
+
compose: `version: '3.8'
|
|
247
|
+
|
|
248
|
+
services:
|
|
249
|
+
mongodb:
|
|
250
|
+
image: mongo:7
|
|
251
|
+
container_name: ${service}_mongodb
|
|
252
|
+
environment:
|
|
253
|
+
MONGO_INITDB_ROOT_USERNAME: admin
|
|
254
|
+
MONGO_INITDB_ROOT_PASSWORD: password
|
|
255
|
+
ports:
|
|
256
|
+
- "27017:27017"
|
|
257
|
+
volumes:
|
|
258
|
+
- mongodb_data:/data/db
|
|
259
|
+
restart: unless-stopped
|
|
260
|
+
healthcheck:
|
|
261
|
+
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
|
|
262
|
+
interval: 10s
|
|
263
|
+
timeout: 5s
|
|
264
|
+
retries: 5
|
|
265
|
+
|
|
266
|
+
volumes:
|
|
267
|
+
mongodb_data:
|
|
268
|
+
`,
|
|
269
|
+
readme: `# MongoDB Database
|
|
270
|
+
|
|
271
|
+
MongoDB 7 with authentication.
|
|
272
|
+
|
|
273
|
+
## Default Credentials
|
|
274
|
+
- Username: admin
|
|
275
|
+
- Password: password
|
|
276
|
+
- Port: 27017
|
|
277
|
+
|
|
278
|
+
## Connect
|
|
279
|
+
\`\`\`bash
|
|
280
|
+
mongosh mongodb://admin:password@localhost:27017
|
|
281
|
+
\`\`\`
|
|
282
|
+
`
|
|
283
|
+
},
|
|
284
|
+
nginx: {
|
|
285
|
+
compose: `version: '3.8'
|
|
286
|
+
|
|
287
|
+
services:
|
|
288
|
+
nginx:
|
|
289
|
+
image: nginx:alpine
|
|
290
|
+
container_name: ${service}_nginx
|
|
291
|
+
ports:
|
|
292
|
+
- "80:80"
|
|
293
|
+
- "443:443"
|
|
294
|
+
volumes:
|
|
295
|
+
- ./html:/usr/share/nginx/html:ro
|
|
296
|
+
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
|
297
|
+
restart: unless-stopped
|
|
298
|
+
healthcheck:
|
|
299
|
+
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost"]
|
|
300
|
+
interval: 10s
|
|
301
|
+
timeout: 5s
|
|
302
|
+
retries: 3
|
|
303
|
+
`,
|
|
304
|
+
readme: `# NGINX Web Server
|
|
305
|
+
|
|
306
|
+
NGINX Alpine with custom configuration support.
|
|
307
|
+
|
|
308
|
+
## Ports
|
|
309
|
+
- HTTP: 80
|
|
310
|
+
- HTTPS: 443
|
|
311
|
+
|
|
312
|
+
## Files
|
|
313
|
+
- HTML: ./html/
|
|
314
|
+
- Config: ./nginx.conf
|
|
315
|
+
|
|
316
|
+
## Access
|
|
317
|
+
http://localhost
|
|
318
|
+
`
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Get template or create a generic one
|
|
323
|
+
const templateData = templates[service.toLowerCase()];
|
|
324
|
+
|
|
325
|
+
if (templateData) {
|
|
326
|
+
// Use predefined template
|
|
327
|
+
await fs.writeFile(
|
|
328
|
+
path.join(servicePath, 'docker-compose.yml'),
|
|
329
|
+
templateData.compose
|
|
330
|
+
);
|
|
331
|
+
await fs.writeFile(
|
|
332
|
+
path.join(servicePath, 'README.md'),
|
|
333
|
+
templateData.readme
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Create additional files for NGINX
|
|
337
|
+
if (service.toLowerCase() === 'nginx') {
|
|
338
|
+
await fs.mkdir(path.join(servicePath, 'html'), { recursive: true });
|
|
339
|
+
await fs.writeFile(
|
|
340
|
+
path.join(servicePath, 'html', 'index.html'),
|
|
341
|
+
'<html><body><h1>Welcome to NGINX</h1></body></html>'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Create generic template
|
|
346
|
+
const genericCompose = `version: '3.8'
|
|
347
|
+
|
|
348
|
+
services:
|
|
349
|
+
${service}:
|
|
350
|
+
image: # Specify your image here
|
|
351
|
+
container_name: ${service}
|
|
352
|
+
ports:
|
|
353
|
+
- "8080:80"
|
|
354
|
+
restart: unless-stopped
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
const genericReadme = `# ${service}
|
|
358
|
+
|
|
359
|
+
Service created with easy-containers.
|
|
360
|
+
|
|
361
|
+
## Usage
|
|
362
|
+
|
|
363
|
+
\`\`\`bash
|
|
364
|
+
# Start service
|
|
365
|
+
easy up ${service}
|
|
366
|
+
|
|
367
|
+
# Stop service
|
|
368
|
+
easy down ${service}
|
|
369
|
+
|
|
370
|
+
# View logs
|
|
371
|
+
easy logs ${service}
|
|
372
|
+
\`\`\`
|
|
373
|
+
|
|
374
|
+
## Configuration
|
|
375
|
+
|
|
376
|
+
Edit \`docker-compose.yml\` to customize the service configuration.
|
|
377
|
+
`;
|
|
378
|
+
|
|
379
|
+
await fs.writeFile(
|
|
380
|
+
path.join(servicePath, 'docker-compose.yml'),
|
|
381
|
+
genericCompose
|
|
382
|
+
);
|
|
383
|
+
await fs.writeFile(
|
|
384
|
+
path.join(servicePath, 'README.md'),
|
|
385
|
+
genericReadme
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* List available services in the repository
|
|
392
|
+
*/
|
|
393
|
+
async function listAvailableServices() {
|
|
394
|
+
const tempDir = path.join(require('os').tmpdir(), `.easy-containers-list-${Date.now()}`);
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
398
|
+
|
|
399
|
+
// Clone only the services directory listing
|
|
400
|
+
await execAsync('git init', { cwd: tempDir });
|
|
401
|
+
await execAsync(`git remote add origin ${REPO_URL}`, { cwd: tempDir });
|
|
402
|
+
await execAsync('git config core.sparseCheckout true', { cwd: tempDir });
|
|
403
|
+
|
|
404
|
+
const sparseCheckoutPath = path.join(tempDir, '.git', 'info', 'sparse-checkout');
|
|
405
|
+
await fs.writeFile(sparseCheckoutPath, `${SERVICES_BASE_PATH}/\n`);
|
|
406
|
+
|
|
407
|
+
await execAsync(`git pull --depth=1 origin ${REPO_BRANCH}`, { cwd: tempDir });
|
|
408
|
+
|
|
409
|
+
// Read services directory
|
|
410
|
+
const servicesDir = path.join(tempDir, SERVICES_BASE_PATH);
|
|
411
|
+
const entries = await fs.readdir(servicesDir, { withFileTypes: true });
|
|
412
|
+
const services = entries
|
|
413
|
+
.filter(entry => entry.isDirectory())
|
|
414
|
+
.map(entry => entry.name);
|
|
415
|
+
|
|
416
|
+
// Clean up
|
|
417
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
418
|
+
|
|
419
|
+
return services;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
// Clean up on error
|
|
422
|
+
try {
|
|
423
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
424
|
+
} catch {}
|
|
425
|
+
|
|
426
|
+
// Return built-in templates as fallback
|
|
427
|
+
return ['postgres', 'mysql', 'redis', 'mongodb', 'nginx'];
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Update service from repository
|
|
433
|
+
*/
|
|
434
|
+
async function updateService(service) {
|
|
435
|
+
const servicePath = getServicePath(service);
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
await fs.access(servicePath);
|
|
439
|
+
|
|
440
|
+
// Backup current configuration
|
|
441
|
+
const backupPath = `${servicePath}.backup-${Date.now()}`;
|
|
442
|
+
await fs.cp(servicePath, backupPath, { recursive: true });
|
|
443
|
+
|
|
444
|
+
console.log(chalk.gray(`Backup created at: ${backupPath}`));
|
|
445
|
+
|
|
446
|
+
// Remove old service
|
|
447
|
+
await fs.rm(servicePath, { recursive: true });
|
|
448
|
+
|
|
449
|
+
// Download new version
|
|
450
|
+
await downloadService(service);
|
|
451
|
+
|
|
452
|
+
console.log(chalk.green(`✓ Service ${service} updated successfully`));
|
|
453
|
+
console.log(chalk.yellow(`\nTo restore backup: mv ${backupPath} ${servicePath}`));
|
|
454
|
+
|
|
455
|
+
} catch (error) {
|
|
456
|
+
throw new Error(`Failed to update service: ${error.message}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Check if a service exists in the repository
|
|
462
|
+
*/
|
|
463
|
+
async function serviceExistsInRepo(service) {
|
|
464
|
+
try {
|
|
465
|
+
const services = await listAvailableServices();
|
|
466
|
+
return services.includes(service);
|
|
467
|
+
} catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
module.exports = {
|
|
473
|
+
downloadService,
|
|
474
|
+
updateService,
|
|
475
|
+
createServiceFromTemplate,
|
|
476
|
+
listAvailableServices,
|
|
477
|
+
serviceExistsInRepo
|
|
478
|
+
};
|