@aifabrix/builder 2.0.0 → 2.0.3
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 -3
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +235 -144
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +177 -125
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/env-config.yaml +9 -1
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +14 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +214 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- package/templates/typescript/tsconfig.json +24 -0
package/lib/infra.js
CHANGED
|
@@ -15,9 +15,24 @@ const path = require('path');
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const os = require('os');
|
|
17
17
|
const secrets = require('./secrets');
|
|
18
|
+
const logger = require('./utils/logger');
|
|
18
19
|
|
|
19
20
|
const execAsync = promisify(exec);
|
|
20
21
|
|
|
22
|
+
// Wrapper to support cwd option
|
|
23
|
+
function execAsyncWithCwd(command, options = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const { cwd, ...execOptions } = options;
|
|
26
|
+
exec(command, { ...execOptions, cwd }, (error, stdout, stderr) => {
|
|
27
|
+
if (error) {
|
|
28
|
+
reject(error);
|
|
29
|
+
} else {
|
|
30
|
+
resolve({ stdout, stderr });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
/**
|
|
22
37
|
* Starts local infrastructure services
|
|
23
38
|
* Launches Postgres, Redis, Keycloak, and Controller in Docker containers
|
|
@@ -43,7 +58,7 @@ async function checkDockerAvailability() {
|
|
|
43
58
|
async function ensureAdminSecrets() {
|
|
44
59
|
const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
|
|
45
60
|
if (!fs.existsSync(adminSecretsPath)) {
|
|
46
|
-
|
|
61
|
+
logger.log('Generating admin-secrets.env...');
|
|
47
62
|
await secrets.generateAdminSecretsEnv();
|
|
48
63
|
}
|
|
49
64
|
return adminSecretsPath;
|
|
@@ -59,21 +74,26 @@ async function startInfra() {
|
|
|
59
74
|
throw new Error(`Compose template not found: ${templatePath}`);
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
77
|
+
// Create infra directory in ~/.aifabrix
|
|
78
|
+
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
79
|
+
const infraDir = path.join(aifabrixDir, 'infra');
|
|
80
|
+
if (!fs.existsSync(infraDir)) {
|
|
81
|
+
fs.mkdirSync(infraDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const composePath = path.join(infraDir, 'compose.yaml');
|
|
85
|
+
fs.writeFileSync(composePath, fs.readFileSync(templatePath, 'utf8'));
|
|
65
86
|
|
|
66
87
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
logger.log(`Using compose file: ${composePath}`);
|
|
89
|
+
logger.log('Starting infrastructure services...');
|
|
90
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
|
|
91
|
+
logger.log('Infrastructure services started successfully');
|
|
70
92
|
|
|
71
93
|
await waitForServices();
|
|
72
|
-
|
|
94
|
+
logger.log('All services are healthy and ready');
|
|
73
95
|
} finally {
|
|
74
|
-
|
|
75
|
-
fs.unlinkSync(tempComposePath);
|
|
76
|
-
}
|
|
96
|
+
// Keep the compose file for stop commands
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
|
|
@@ -91,25 +111,23 @@ async function startInfra() {
|
|
|
91
111
|
* // All infrastructure containers are stopped and removed
|
|
92
112
|
*/
|
|
93
113
|
async function stopInfra() {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
114
|
+
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
115
|
+
const composePath = path.join(aifabrixDir, 'infra', 'compose.yaml');
|
|
116
|
+
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
96
117
|
|
|
97
|
-
if (!fs.existsSync(
|
|
98
|
-
|
|
118
|
+
if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
|
|
119
|
+
logger.log('Infrastructure not running or not properly configured');
|
|
99
120
|
return;
|
|
100
121
|
}
|
|
101
122
|
|
|
102
|
-
const
|
|
103
|
-
fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
|
|
123
|
+
const infraDir = path.join(aifabrixDir, 'infra');
|
|
104
124
|
|
|
105
125
|
try {
|
|
106
|
-
|
|
107
|
-
await
|
|
108
|
-
|
|
126
|
+
logger.log('Stopping infrastructure services...');
|
|
127
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" down`, { cwd: infraDir });
|
|
128
|
+
logger.log('Infrastructure services stopped');
|
|
109
129
|
} finally {
|
|
110
|
-
|
|
111
|
-
fs.unlinkSync(tempComposePath);
|
|
112
|
-
}
|
|
130
|
+
// Keep the compose file for future use
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
|
|
@@ -127,25 +145,90 @@ async function stopInfra() {
|
|
|
127
145
|
* // All infrastructure containers and data are removed
|
|
128
146
|
*/
|
|
129
147
|
async function stopInfraWithVolumes() {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
148
|
+
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
149
|
+
const composePath = path.join(aifabrixDir, 'infra', 'compose.yaml');
|
|
150
|
+
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
132
151
|
|
|
133
|
-
if (!fs.existsSync(
|
|
134
|
-
|
|
152
|
+
if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
|
|
153
|
+
logger.log('Infrastructure not running or not properly configured');
|
|
135
154
|
return;
|
|
136
155
|
}
|
|
137
156
|
|
|
138
|
-
const
|
|
139
|
-
fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
|
|
157
|
+
const infraDir = path.join(aifabrixDir, 'infra');
|
|
140
158
|
|
|
141
159
|
try {
|
|
142
|
-
|
|
143
|
-
await
|
|
144
|
-
|
|
160
|
+
logger.log('Stopping infrastructure services and removing all data...');
|
|
161
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" down -v`, { cwd: infraDir });
|
|
162
|
+
logger.log('Infrastructure services stopped and all data removed');
|
|
145
163
|
} finally {
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
// Keep the compose file for future use
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Finds container by name pattern
|
|
170
|
+
* @private
|
|
171
|
+
* @async
|
|
172
|
+
* @param {string} serviceName - Service name
|
|
173
|
+
* @returns {Promise<string|null>} Container name or null if not found
|
|
174
|
+
*/
|
|
175
|
+
async function findContainer(serviceName) {
|
|
176
|
+
try {
|
|
177
|
+
// Try both naming patterns: infra-* (dynamic names) and aifabrix-* (hardcoded names)
|
|
178
|
+
let { stdout } = await execAsync(`docker ps --filter "name=infra-${serviceName}" --format "{{.Names}}"`);
|
|
179
|
+
let containerName = stdout.trim();
|
|
180
|
+
if (!containerName) {
|
|
181
|
+
// Fallback to hardcoded names
|
|
182
|
+
({ stdout } = await execAsync(`docker ps --filter "name=aifabrix-${serviceName}" --format "{{.Names}}"`));
|
|
183
|
+
containerName = stdout.trim();
|
|
184
|
+
}
|
|
185
|
+
return containerName;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Checks health status for a service with health checks
|
|
193
|
+
* @private
|
|
194
|
+
* @async
|
|
195
|
+
* @param {string} serviceName - Service name
|
|
196
|
+
* @returns {Promise<string>} Health status
|
|
197
|
+
*/
|
|
198
|
+
async function checkServiceWithHealthCheck(serviceName) {
|
|
199
|
+
try {
|
|
200
|
+
const containerName = await findContainer(serviceName);
|
|
201
|
+
if (!containerName) {
|
|
202
|
+
return 'unknown';
|
|
148
203
|
}
|
|
204
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' ${containerName}`);
|
|
205
|
+
const status = stdout.trim().replace(/['"]/g, '');
|
|
206
|
+
// Accept both 'healthy' and 'starting' as healthy (starting means it's initializing)
|
|
207
|
+
return (status === 'healthy' || status === 'starting') ? 'healthy' : status;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return 'unknown';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Checks health status for a service without health checks
|
|
215
|
+
* @private
|
|
216
|
+
* @async
|
|
217
|
+
* @param {string} serviceName - Service name
|
|
218
|
+
* @returns {Promise<string>} Health status
|
|
219
|
+
*/
|
|
220
|
+
async function checkServiceWithoutHealthCheck(serviceName) {
|
|
221
|
+
try {
|
|
222
|
+
const containerName = await findContainer(serviceName);
|
|
223
|
+
if (!containerName) {
|
|
224
|
+
return 'unknown';
|
|
225
|
+
}
|
|
226
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
|
|
227
|
+
const status = stdout.trim().replace(/['"]/g, '');
|
|
228
|
+
// Treat 'running' or 'healthy' as 'healthy' for services without health checks
|
|
229
|
+
return (status === 'running' || status === 'healthy') ? 'healthy' : 'unhealthy';
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return 'unknown';
|
|
149
232
|
}
|
|
150
233
|
}
|
|
151
234
|
|
|
@@ -163,16 +246,18 @@ async function stopInfraWithVolumes() {
|
|
|
163
246
|
* // Returns: { postgres: 'healthy', redis: 'healthy', keycloak: 'healthy', controller: 'healthy' }
|
|
164
247
|
*/
|
|
165
248
|
async function checkInfraHealth() {
|
|
166
|
-
const
|
|
249
|
+
const servicesWithHealthCheck = ['postgres', 'redis'];
|
|
250
|
+
const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
|
|
167
251
|
const health = {};
|
|
168
252
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
253
|
+
// Check health status for services with health checks
|
|
254
|
+
for (const service of servicesWithHealthCheck) {
|
|
255
|
+
health[service] = await checkServiceWithHealthCheck(service);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check if services without health checks are running
|
|
259
|
+
for (const service of servicesWithoutHealthCheck) {
|
|
260
|
+
health[service] = await checkServiceWithoutHealthCheck(service);
|
|
176
261
|
}
|
|
177
262
|
|
|
178
263
|
return health;
|
|
@@ -202,12 +287,21 @@ async function getInfraStatus() {
|
|
|
202
287
|
|
|
203
288
|
for (const [serviceName, config] of Object.entries(services)) {
|
|
204
289
|
try {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
290
|
+
const containerName = await findContainer(serviceName);
|
|
291
|
+
if (containerName) {
|
|
292
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
|
|
293
|
+
status[serviceName] = {
|
|
294
|
+
status: stdout.trim(),
|
|
295
|
+
port: config.port,
|
|
296
|
+
url: config.url
|
|
297
|
+
};
|
|
298
|
+
} else {
|
|
299
|
+
status[serviceName] = {
|
|
300
|
+
status: 'not running',
|
|
301
|
+
port: config.port,
|
|
302
|
+
url: config.url
|
|
303
|
+
};
|
|
304
|
+
}
|
|
211
305
|
} catch (error) {
|
|
212
306
|
status[serviceName] = {
|
|
213
307
|
status: 'not running',
|
|
@@ -244,24 +338,22 @@ async function restartService(serviceName) {
|
|
|
244
338
|
throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
|
|
245
339
|
}
|
|
246
340
|
|
|
247
|
-
const
|
|
248
|
-
const
|
|
341
|
+
const aifabrixDir = path.join(os.homedir(), '.aifabrix');
|
|
342
|
+
const composePath = path.join(aifabrixDir, 'infra', 'compose.yaml');
|
|
343
|
+
const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
249
344
|
|
|
250
|
-
if (!fs.existsSync(
|
|
345
|
+
if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
|
|
251
346
|
throw new Error('Infrastructure not properly configured');
|
|
252
347
|
}
|
|
253
348
|
|
|
254
|
-
const
|
|
255
|
-
fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
|
|
349
|
+
const infraDir = path.join(aifabrixDir, 'infra');
|
|
256
350
|
|
|
257
351
|
try {
|
|
258
|
-
|
|
259
|
-
await
|
|
260
|
-
|
|
352
|
+
logger.log(`Restarting ${serviceName} service...`);
|
|
353
|
+
await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" restart ${serviceName}`, { cwd: infraDir });
|
|
354
|
+
logger.log(`${serviceName} service restarted successfully`);
|
|
261
355
|
} finally {
|
|
262
|
-
|
|
263
|
-
fs.unlinkSync(tempComposePath);
|
|
264
|
-
}
|
|
356
|
+
// Keep the compose file for future use
|
|
265
357
|
}
|
|
266
358
|
}
|
|
267
359
|
|
|
@@ -281,8 +373,10 @@ async function waitForServices() {
|
|
|
281
373
|
return;
|
|
282
374
|
}
|
|
283
375
|
|
|
376
|
+
// Debug logging
|
|
377
|
+
|
|
284
378
|
if (attempt < maxAttempts) {
|
|
285
|
-
|
|
379
|
+
logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
|
|
286
380
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
287
381
|
}
|
|
288
382
|
}
|
|
@@ -296,5 +390,6 @@ module.exports = {
|
|
|
296
390
|
stopInfraWithVolumes,
|
|
297
391
|
checkInfraHealth,
|
|
298
392
|
getInfraStatus,
|
|
299
|
-
restartService
|
|
393
|
+
restartService,
|
|
394
|
+
ensureAdminSecrets
|
|
300
395
|
};
|
package/lib/push.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const { exec } = require('child_process');
|
|
13
13
|
const { promisify } = require('util');
|
|
14
14
|
const chalk = require('chalk');
|
|
15
|
+
const logger = require('./utils/logger');
|
|
15
16
|
|
|
16
17
|
const execAsync = promisify(exec);
|
|
17
18
|
|
|
@@ -43,12 +44,55 @@ function extractRegistryName(registryUrl) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
+
* Parses registry URL format
|
|
48
|
+
* @function parseRegistryUrl
|
|
49
|
+
* @param {string} registryUrl - Registry URL to parse
|
|
50
|
+
* @returns {Object|null} Parsed registry info or null if invalid
|
|
51
|
+
*/
|
|
52
|
+
function parseRegistryUrl(registryUrl) {
|
|
53
|
+
if (!registryUrl || typeof registryUrl !== 'string') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (registryUrl.includes('://')) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (/^[^.]+\.azurecr\.io$/.test(registryUrl)) {
|
|
62
|
+
return { type: 'acr', valid: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (registryUrl === 'docker.io' || registryUrl === 'index.docker.io') {
|
|
66
|
+
return { type: 'dockerhub', valid: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (registryUrl === 'ghcr.io') {
|
|
70
|
+
return { type: 'ghcr', valid: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (registryUrl === 'azurecr.io') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (/^[^.]+\.azurecr\.com$/.test(registryUrl)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (/^[a-z0-9][a-z0-9.-]+\.[a-z]{2,}(?::[0-9]+)?$/.test(registryUrl)) {
|
|
82
|
+
return { type: 'custom', valid: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validates registry URL format
|
|
47
90
|
* @param {string} registryUrl - Registry URL to validate
|
|
48
91
|
* @returns {boolean} True if valid
|
|
49
92
|
*/
|
|
50
93
|
function validateRegistryURL(registryUrl) {
|
|
51
|
-
|
|
94
|
+
const parsed = parseRegistryUrl(registryUrl);
|
|
95
|
+
return parsed !== null && parsed.valid;
|
|
52
96
|
}
|
|
53
97
|
|
|
54
98
|
/**
|
|
@@ -74,14 +118,62 @@ async function checkACRAuthentication(registry) {
|
|
|
74
118
|
async function authenticateACR(registry) {
|
|
75
119
|
try {
|
|
76
120
|
const registryName = extractRegistryName(registry);
|
|
77
|
-
|
|
121
|
+
logger.log(chalk.blue(`Authenticating with ${registry}...`));
|
|
78
122
|
await execAsync(`az acr login --name ${registryName}`);
|
|
79
|
-
|
|
123
|
+
logger.log(chalk.green(`✓ Authenticated with ${registry}`));
|
|
80
124
|
} catch (error) {
|
|
81
125
|
throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
|
|
82
126
|
}
|
|
83
127
|
}
|
|
84
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Authenticate with external registry
|
|
131
|
+
* @param {string} registry - Registry URL
|
|
132
|
+
* @param {string} username - Username for authentication
|
|
133
|
+
* @param {string} password - Password or token for authentication
|
|
134
|
+
* @throws {Error} If authentication fails
|
|
135
|
+
*/
|
|
136
|
+
async function authenticateExternalRegistry(registry, username, password) {
|
|
137
|
+
try {
|
|
138
|
+
logger.log(chalk.blue(`Authenticating with ${registry}...`));
|
|
139
|
+
|
|
140
|
+
// Use cross-platform approach: write password to stdin directly
|
|
141
|
+
// This works on Windows, Linux, and macOS
|
|
142
|
+
const { spawn } = require('child_process');
|
|
143
|
+
const dockerLogin = spawn('docker', ['login', registry, '-u', username, '--password-stdin']);
|
|
144
|
+
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
let errorOutput = '';
|
|
147
|
+
|
|
148
|
+
dockerLogin.stdin.write(password);
|
|
149
|
+
dockerLogin.stdin.end();
|
|
150
|
+
|
|
151
|
+
dockerLogin.stdout.on('data', (_data) => {
|
|
152
|
+
// Authentication output (usually minimal)
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
dockerLogin.stderr.on('data', (data) => {
|
|
156
|
+
errorOutput += data.toString();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
dockerLogin.on('close', (code) => {
|
|
160
|
+
if (code === 0) {
|
|
161
|
+
logger.log(chalk.green(`✓ Authenticated with ${registry}`));
|
|
162
|
+
resolve();
|
|
163
|
+
} else {
|
|
164
|
+
reject(new Error(`Docker login failed: ${errorOutput || `Exit code ${code}`}`));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
dockerLogin.on('error', (error) => {
|
|
169
|
+
reject(new Error(`Failed to execute docker login: ${error.message}`));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new Error(`Failed to authenticate with external registry: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
85
177
|
/**
|
|
86
178
|
* Check if Docker image exists locally
|
|
87
179
|
* @param {string} imageName - Image name
|
|
@@ -90,8 +182,10 @@ async function authenticateACR(registry) {
|
|
|
90
182
|
*/
|
|
91
183
|
async function checkLocalImageExists(imageName, tag = 'latest') {
|
|
92
184
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
185
|
+
// Use Docker's native filtering for cross-platform compatibility (Windows-safe)
|
|
186
|
+
const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${imageName}:${tag}"`);
|
|
187
|
+
const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
|
|
188
|
+
return lines.some(line => line.trim() === `${imageName}:${tag}`);
|
|
95
189
|
} catch (error) {
|
|
96
190
|
return false;
|
|
97
191
|
}
|
|
@@ -105,9 +199,9 @@ async function checkLocalImageExists(imageName, tag = 'latest') {
|
|
|
105
199
|
*/
|
|
106
200
|
async function tagImage(sourceImage, targetImage) {
|
|
107
201
|
try {
|
|
108
|
-
|
|
202
|
+
logger.log(chalk.blue(`Tagging ${sourceImage} as ${targetImage}...`));
|
|
109
203
|
await execAsync(`docker tag ${sourceImage} ${targetImage}`);
|
|
110
|
-
|
|
204
|
+
logger.log(chalk.green(`✓ Tagged: ${targetImage}`));
|
|
111
205
|
} catch (error) {
|
|
112
206
|
throw new Error(`Failed to tag image: ${error.message}`);
|
|
113
207
|
}
|
|
@@ -120,9 +214,9 @@ async function tagImage(sourceImage, targetImage) {
|
|
|
120
214
|
*/
|
|
121
215
|
async function pushImage(imageWithTag) {
|
|
122
216
|
try {
|
|
123
|
-
|
|
217
|
+
logger.log(chalk.blue(`Pushing ${imageWithTag}...`));
|
|
124
218
|
await execAsync(`docker push ${imageWithTag}`);
|
|
125
|
-
|
|
219
|
+
logger.log(chalk.green(`✓ Pushed: ${imageWithTag}`));
|
|
126
220
|
} catch (error) {
|
|
127
221
|
throw new Error(`Failed to push image: ${error.message}`);
|
|
128
222
|
}
|
|
@@ -134,6 +228,7 @@ module.exports = {
|
|
|
134
228
|
validateRegistryURL,
|
|
135
229
|
checkACRAuthentication,
|
|
136
230
|
authenticateACR,
|
|
231
|
+
authenticateExternalRegistry,
|
|
137
232
|
checkLocalImageExists,
|
|
138
233
|
tagImage,
|
|
139
234
|
pushImage
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"name": {
|
|
91
91
|
"type": "string",
|
|
92
92
|
"description": "Database name",
|
|
93
|
-
"pattern": "^[a-z0-
|
|
93
|
+
"pattern": "^[a-z0-9-_]+$"
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
"additionalProperties": false
|
|
@@ -506,7 +506,7 @@
|
|
|
506
506
|
},
|
|
507
507
|
"repositoryUrl": {
|
|
508
508
|
"type": "string",
|
|
509
|
-
"description": "Full repository URL for
|
|
509
|
+
"description": "Full repository URL for pipeline validation (same as OAuth callback)",
|
|
510
510
|
"pattern": "^(https://github.com/[^/]+/[^/]+|https://gitlab.com/[^/]+/[^/]+|https://dev.azure.com/[^/]+/[^/]+/[^/]+)$"
|
|
511
511
|
}
|
|
512
512
|
},
|
|
@@ -586,6 +586,12 @@
|
|
|
586
586
|
"minimum": 1000,
|
|
587
587
|
"maximum": 65535
|
|
588
588
|
},
|
|
589
|
+
"containerPort": {
|
|
590
|
+
"type": "integer",
|
|
591
|
+
"description": "Container internal port (defaults to port if not specified)",
|
|
592
|
+
"minimum": 1,
|
|
593
|
+
"maximum": 65535
|
|
594
|
+
},
|
|
589
595
|
"language": {
|
|
590
596
|
"type": "string",
|
|
591
597
|
"description": "Runtime language for template selection",
|
|
@@ -603,6 +609,28 @@
|
|
|
603
609
|
}
|
|
604
610
|
},
|
|
605
611
|
"additionalProperties": false
|
|
612
|
+
},
|
|
613
|
+
"deployment": {
|
|
614
|
+
"type": "object",
|
|
615
|
+
"description": "Deployment configuration for pipeline API",
|
|
616
|
+
"properties": {
|
|
617
|
+
"controllerUrl": {
|
|
618
|
+
"type": "string",
|
|
619
|
+
"description": "Controller API URL for deployment",
|
|
620
|
+
"pattern": "^https://.*$"
|
|
621
|
+
},
|
|
622
|
+
"clientId": {
|
|
623
|
+
"type": "string",
|
|
624
|
+
"description": "Pipeline ClientId for automated deployment",
|
|
625
|
+
"pattern": "^[a-z0-9-]+$"
|
|
626
|
+
},
|
|
627
|
+
"clientSecret": {
|
|
628
|
+
"type": "string",
|
|
629
|
+
"description": "Pipeline ClientSecret (use kv:// reference)",
|
|
630
|
+
"pattern": "^(kv://.*|.+)$"
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
"additionalProperties": false
|
|
606
634
|
}
|
|
607
635
|
},
|
|
608
636
|
"additionalProperties": false,
|
|
@@ -7,9 +7,17 @@ environments:
|
|
|
7
7
|
REDIS_HOST: redis
|
|
8
8
|
MISO_HOST: miso-controller
|
|
9
9
|
KEYCLOAK_HOST: keycloak
|
|
10
|
-
|
|
10
|
+
MORI_HOST: mori-controller
|
|
11
|
+
OPENWEBUI_HOST: openwebui
|
|
12
|
+
FLOWISE_HOST: flowise
|
|
13
|
+
DATAPLANE_HOST: dataplane
|
|
14
|
+
|
|
11
15
|
local:
|
|
12
16
|
DB_HOST: localhost
|
|
13
17
|
REDIS_HOST: localhost
|
|
14
18
|
MISO_HOST: localhost
|
|
15
19
|
KEYCLOAK_HOST: localhost
|
|
20
|
+
MORI_HOST: localhost
|
|
21
|
+
OPENWEBUI_HOST: localhost
|
|
22
|
+
FLOWISE_HOST: localhost
|
|
23
|
+
DATAPLANE_HOST: localhost
|