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.
@@ -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
+ };