@aifabrix/builder 2.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/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/aifabrix.js +51 -0
- package/lib/app-deploy.js +209 -0
- package/lib/app-run.js +291 -0
- package/lib/app.js +472 -0
- package/lib/audit-logger.js +162 -0
- package/lib/build.js +313 -0
- package/lib/cli.js +307 -0
- package/lib/deployer.js +256 -0
- package/lib/env-reader.js +250 -0
- package/lib/generator.js +361 -0
- package/lib/github-generator.js +220 -0
- package/lib/infra.js +300 -0
- package/lib/key-generator.js +93 -0
- package/lib/push.js +141 -0
- package/lib/schema/application-schema.json +649 -0
- package/lib/schema/env-config.yaml +15 -0
- package/lib/secrets.js +282 -0
- package/lib/templates.js +301 -0
- package/lib/validator.js +377 -0
- package/package.json +59 -0
- package/templates/README.md +51 -0
- package/templates/github/ci.yaml.hbs +15 -0
- package/templates/github/pr-checks.yaml.hbs +35 -0
- package/templates/github/release.yaml.hbs +79 -0
- package/templates/github/test.hbs +11 -0
- package/templates/github/test.yaml.hbs +11 -0
- package/templates/infra/compose.yaml +93 -0
- package/templates/python/Dockerfile.hbs +49 -0
- package/templates/python/docker-compose.hbs +69 -0
- package/templates/typescript/Dockerfile.hbs +46 -0
- package/templates/typescript/docker-compose.hbs +69 -0
package/lib/infra.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Infrastructure Management
|
|
3
|
+
*
|
|
4
|
+
* This module manages local infrastructure services using Docker Compose.
|
|
5
|
+
* Handles starting/stopping Postgres, Redis, Keycloak, and Controller services.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Local infrastructure management for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { exec } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const secrets = require('./secrets');
|
|
18
|
+
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Starts local infrastructure services
|
|
23
|
+
* Launches Postgres, Redis, Keycloak, and Controller in Docker containers
|
|
24
|
+
*
|
|
25
|
+
* @async
|
|
26
|
+
* @function startInfra
|
|
27
|
+
* @returns {Promise<void>} Resolves when infrastructure is started
|
|
28
|
+
* @throws {Error} If Docker is not running or compose fails
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* await startInfra();
|
|
32
|
+
* // Infrastructure services are now running
|
|
33
|
+
*/
|
|
34
|
+
async function checkDockerAvailability() {
|
|
35
|
+
try {
|
|
36
|
+
await execAsync('docker --version');
|
|
37
|
+
await execAsync('docker-compose --version');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error('Docker or Docker Compose is not available. Please install and start Docker.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function ensureAdminSecrets() {
|
|
44
|
+
const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
|
|
45
|
+
if (!fs.existsSync(adminSecretsPath)) {
|
|
46
|
+
console.log('Generating admin-secrets.env...');
|
|
47
|
+
await secrets.generateAdminSecretsEnv();
|
|
48
|
+
}
|
|
49
|
+
return adminSecretsPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function startInfra() {
|
|
53
|
+
await checkDockerAvailability();
|
|
54
|
+
const adminSecretsPath = await ensureAdminSecrets();
|
|
55
|
+
|
|
56
|
+
// Load compose template
|
|
57
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
|
|
58
|
+
if (!fs.existsSync(templatePath)) {
|
|
59
|
+
throw new Error(`Compose template not found: ${templatePath}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
63
|
+
const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
|
|
64
|
+
fs.writeFileSync(tempComposePath, template);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
console.log('Starting infrastructure services...');
|
|
68
|
+
await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" up -d`);
|
|
69
|
+
console.log('Infrastructure services started successfully');
|
|
70
|
+
|
|
71
|
+
await waitForServices();
|
|
72
|
+
console.log('All services are healthy and ready');
|
|
73
|
+
} finally {
|
|
74
|
+
if (fs.existsSync(tempComposePath)) {
|
|
75
|
+
fs.unlinkSync(tempComposePath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Stops and removes local infrastructure services
|
|
82
|
+
* Cleanly shuts down all infrastructure containers
|
|
83
|
+
*
|
|
84
|
+
* @async
|
|
85
|
+
* @function stopInfra
|
|
86
|
+
* @returns {Promise<void>} Resolves when infrastructure is stopped
|
|
87
|
+
* @throws {Error} If Docker compose fails
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* await stopInfra();
|
|
91
|
+
* // All infrastructure containers are stopped and removed
|
|
92
|
+
*/
|
|
93
|
+
async function stopInfra() {
|
|
94
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
|
|
95
|
+
const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
|
|
96
|
+
|
|
97
|
+
if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
|
|
98
|
+
console.log('Infrastructure not running or not properly configured');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
|
|
103
|
+
fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
console.log('Stopping infrastructure services...');
|
|
107
|
+
await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" down`);
|
|
108
|
+
console.log('Infrastructure services stopped');
|
|
109
|
+
} finally {
|
|
110
|
+
if (fs.existsSync(tempComposePath)) {
|
|
111
|
+
fs.unlinkSync(tempComposePath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stops and removes local infrastructure services with volumes
|
|
118
|
+
* Cleanly shuts down all infrastructure containers and removes all data
|
|
119
|
+
*
|
|
120
|
+
* @async
|
|
121
|
+
* @function stopInfraWithVolumes
|
|
122
|
+
* @returns {Promise<void>} Resolves when infrastructure is stopped and volumes removed
|
|
123
|
+
* @throws {Error} If Docker compose fails
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* await stopInfraWithVolumes();
|
|
127
|
+
* // All infrastructure containers and data are removed
|
|
128
|
+
*/
|
|
129
|
+
async function stopInfraWithVolumes() {
|
|
130
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
|
|
131
|
+
const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
|
|
134
|
+
console.log('Infrastructure not running or not properly configured');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
|
|
139
|
+
fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
console.log('Stopping infrastructure services and removing all data...');
|
|
143
|
+
await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" down -v`);
|
|
144
|
+
console.log('Infrastructure services stopped and all data removed');
|
|
145
|
+
} finally {
|
|
146
|
+
if (fs.existsSync(tempComposePath)) {
|
|
147
|
+
fs.unlinkSync(tempComposePath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks if infrastructure services are running
|
|
154
|
+
* Validates that all required services are healthy and accessible
|
|
155
|
+
*
|
|
156
|
+
* @async
|
|
157
|
+
* @function checkInfraHealth
|
|
158
|
+
* @returns {Promise<Object>} Health status of each service
|
|
159
|
+
* @throws {Error} If health check fails
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* const health = await checkInfraHealth();
|
|
163
|
+
* // Returns: { postgres: 'healthy', redis: 'healthy', keycloak: 'healthy', controller: 'healthy' }
|
|
164
|
+
*/
|
|
165
|
+
async function checkInfraHealth() {
|
|
166
|
+
const services = ['postgres', 'redis', 'pgadmin', 'redis-commander'];
|
|
167
|
+
const health = {};
|
|
168
|
+
|
|
169
|
+
for (const service of services) {
|
|
170
|
+
try {
|
|
171
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' aifabrix-${service}`);
|
|
172
|
+
health[service] = stdout.trim();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
health[service] = 'unknown';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return health;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Gets the status of infrastructure services
|
|
183
|
+
* Returns detailed information about running containers
|
|
184
|
+
*
|
|
185
|
+
* @async
|
|
186
|
+
* @function getInfraStatus
|
|
187
|
+
* @returns {Promise<Object>} Status information for each service
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const status = await getInfraStatus();
|
|
191
|
+
* // Returns: { postgres: { status: 'running', port: 5432, url: 'localhost:5432' }, ... }
|
|
192
|
+
*/
|
|
193
|
+
async function getInfraStatus() {
|
|
194
|
+
const services = {
|
|
195
|
+
postgres: { port: 5432, url: 'localhost:5432' },
|
|
196
|
+
redis: { port: 6379, url: 'localhost:6379' },
|
|
197
|
+
pgadmin: { port: 5050, url: 'http://localhost:5050' },
|
|
198
|
+
'redis-commander': { port: 8081, url: 'http://localhost:8081' }
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const status = {};
|
|
202
|
+
|
|
203
|
+
for (const [serviceName, config] of Object.entries(services)) {
|
|
204
|
+
try {
|
|
205
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' aifabrix-${serviceName}`);
|
|
206
|
+
status[serviceName] = {
|
|
207
|
+
status: stdout.trim(),
|
|
208
|
+
port: config.port,
|
|
209
|
+
url: config.url
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
status[serviceName] = {
|
|
213
|
+
status: 'not running',
|
|
214
|
+
port: config.port,
|
|
215
|
+
url: config.url
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return status;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Restarts a specific infrastructure service
|
|
225
|
+
* Useful for applying configuration changes
|
|
226
|
+
*
|
|
227
|
+
* @async
|
|
228
|
+
* @function restartService
|
|
229
|
+
* @param {string} serviceName - Name of service to restart
|
|
230
|
+
* @returns {Promise<void>} Resolves when service is restarted
|
|
231
|
+
* @throws {Error} If service doesn't exist or restart fails
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* await restartService('keycloak');
|
|
235
|
+
* // Keycloak service is restarted
|
|
236
|
+
*/
|
|
237
|
+
async function restartService(serviceName) {
|
|
238
|
+
if (!serviceName || typeof serviceName !== 'string') {
|
|
239
|
+
throw new Error('Service name is required and must be a string');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander'];
|
|
243
|
+
if (!validServices.includes(serviceName)) {
|
|
244
|
+
throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
|
|
248
|
+
const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
|
|
249
|
+
|
|
250
|
+
if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
|
|
251
|
+
throw new Error('Infrastructure not properly configured');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
|
|
255
|
+
fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
console.log(`Restarting ${serviceName} service...`);
|
|
259
|
+
await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" restart ${serviceName}`);
|
|
260
|
+
console.log(`${serviceName} service restarted successfully`);
|
|
261
|
+
} finally {
|
|
262
|
+
if (fs.existsSync(tempComposePath)) {
|
|
263
|
+
fs.unlinkSync(tempComposePath);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Waits for services to be healthy
|
|
270
|
+
* @private
|
|
271
|
+
*/
|
|
272
|
+
async function waitForServices() {
|
|
273
|
+
const maxAttempts = 30;
|
|
274
|
+
const delay = 2000; // 2 seconds
|
|
275
|
+
|
|
276
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
277
|
+
const health = await checkInfraHealth();
|
|
278
|
+
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
279
|
+
|
|
280
|
+
if (allHealthy) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (attempt < maxAttempts) {
|
|
285
|
+
console.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
|
|
286
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
throw new Error('Services failed to become healthy within timeout period');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
startInfra,
|
|
295
|
+
stopInfra,
|
|
296
|
+
stopInfraWithVolumes,
|
|
297
|
+
checkInfraHealth,
|
|
298
|
+
getInfraStatus,
|
|
299
|
+
restartService
|
|
300
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Deployment Key Generator
|
|
3
|
+
*
|
|
4
|
+
* This module generates SHA256-based deployment keys for controller authentication.
|
|
5
|
+
* Keys are computed from variables.yaml content to ensure deployment integrity.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Deployment key generation for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generates deployment key from variables.yaml content
|
|
18
|
+
* Creates SHA256 hash for controller authentication and deployment integrity
|
|
19
|
+
*
|
|
20
|
+
* @async
|
|
21
|
+
* @function generateDeploymentKey
|
|
22
|
+
* @param {string} appName - Name of the application
|
|
23
|
+
* @returns {Promise<string>} SHA256 hash of variables.yaml content
|
|
24
|
+
* @throws {Error} If variables.yaml cannot be read
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const key = await generateDeploymentKey('myapp');
|
|
28
|
+
* // Returns: 'a1b2c3d4e5f6...' (64-character SHA256 hash)
|
|
29
|
+
*/
|
|
30
|
+
async function generateDeploymentKey(appName) {
|
|
31
|
+
if (!appName || typeof appName !== 'string') {
|
|
32
|
+
throw new Error('App name is required and must be a string');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(variablesPath)) {
|
|
38
|
+
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
42
|
+
return generateDeploymentKeyFromContent(content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generates deployment key from raw variables.yaml content
|
|
47
|
+
* Useful for testing or when content is already loaded
|
|
48
|
+
*
|
|
49
|
+
* @function generateDeploymentKeyFromContent
|
|
50
|
+
* @param {string} content - Raw variables.yaml content
|
|
51
|
+
* @returns {string} SHA256 hash of content
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* const key = generateDeploymentKeyFromContent(yamlContent);
|
|
55
|
+
* // Returns: 'a1b2c3d4e5f6...' (64-character SHA256 hash)
|
|
56
|
+
*/
|
|
57
|
+
function generateDeploymentKeyFromContent(content) {
|
|
58
|
+
if (typeof content !== 'string') {
|
|
59
|
+
throw new Error('Content is required and must be a string');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hash = crypto.createHash('sha256');
|
|
63
|
+
hash.update(content, 'utf8');
|
|
64
|
+
return hash.digest('hex');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validates deployment key format
|
|
69
|
+
* Ensures key is a valid SHA256 hash
|
|
70
|
+
*
|
|
71
|
+
* @function validateDeploymentKey
|
|
72
|
+
* @param {string} key - Deployment key to validate
|
|
73
|
+
* @returns {boolean} True if key is valid SHA256 hash
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const isValid = validateDeploymentKey('a1b2c3d4e5f6...');
|
|
77
|
+
* // Returns: true
|
|
78
|
+
*/
|
|
79
|
+
function validateDeploymentKey(key) {
|
|
80
|
+
if (!key || typeof key !== 'string') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// SHA256 produces 64-character hex string
|
|
85
|
+
const sha256Pattern = /^[a-f0-9]{64}$/i;
|
|
86
|
+
return sha256Pattern.test(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
generateDeploymentKey,
|
|
91
|
+
generateDeploymentKeyFromContent,
|
|
92
|
+
validateDeploymentKey
|
|
93
|
+
};
|
package/lib/push.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Push Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module handles pushing Docker images to Azure Container Registry.
|
|
5
|
+
* Includes authentication, tagging, and push operations.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Push utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { exec } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if Azure CLI is installed
|
|
20
|
+
* @returns {Promise<boolean>} True if Azure CLI is available
|
|
21
|
+
*/
|
|
22
|
+
async function checkAzureCLIInstalled() {
|
|
23
|
+
try {
|
|
24
|
+
await execAsync('az --version');
|
|
25
|
+
return true;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract registry name from ACR URL
|
|
33
|
+
* @param {string} registryUrl - Registry URL (e.g., myacr.azurecr.io)
|
|
34
|
+
* @returns {string} Registry name (e.g., myacr)
|
|
35
|
+
* @throws {Error} If URL format is invalid
|
|
36
|
+
*/
|
|
37
|
+
function extractRegistryName(registryUrl) {
|
|
38
|
+
const match = registryUrl.match(/^([^/]+)\.azurecr\.io$/);
|
|
39
|
+
if (!match) {
|
|
40
|
+
throw new Error(`Invalid ACR URL format: ${registryUrl}. Expected format: *.azurecr.io`);
|
|
41
|
+
}
|
|
42
|
+
return match[1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate registry URL format
|
|
47
|
+
* @param {string} registryUrl - Registry URL to validate
|
|
48
|
+
* @returns {boolean} True if valid
|
|
49
|
+
*/
|
|
50
|
+
function validateRegistryURL(registryUrl) {
|
|
51
|
+
return /^[^.]+\.azurecr\.io$/.test(registryUrl);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if already authenticated with ACR
|
|
56
|
+
* @param {string} registry - Registry URL
|
|
57
|
+
* @returns {Promise<boolean>} True if authenticated
|
|
58
|
+
*/
|
|
59
|
+
async function checkACRAuthentication(registry) {
|
|
60
|
+
try {
|
|
61
|
+
const registryName = extractRegistryName(registry);
|
|
62
|
+
await execAsync(`az acr show --name ${registryName}`);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Authenticate with Azure Container Registry
|
|
71
|
+
* @param {string} registry - Registry URL
|
|
72
|
+
* @throws {Error} If authentication fails
|
|
73
|
+
*/
|
|
74
|
+
async function authenticateACR(registry) {
|
|
75
|
+
try {
|
|
76
|
+
const registryName = extractRegistryName(registry);
|
|
77
|
+
console.log(chalk.blue(`Authenticating with ${registry}...`));
|
|
78
|
+
await execAsync(`az acr login --name ${registryName}`);
|
|
79
|
+
console.log(chalk.green(`✓ Authenticated with ${registry}`));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if Docker image exists locally
|
|
87
|
+
* @param {string} imageName - Image name
|
|
88
|
+
* @param {string} tag - Image tag (default: latest)
|
|
89
|
+
* @returns {Promise<boolean>} True if image exists
|
|
90
|
+
*/
|
|
91
|
+
async function checkLocalImageExists(imageName, tag = 'latest') {
|
|
92
|
+
try {
|
|
93
|
+
const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${imageName}:${tag}$"`);
|
|
94
|
+
return stdout.trim() === `${imageName}:${tag}`;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Tag Docker image for ACR
|
|
102
|
+
* @param {string} sourceImage - Source image tag
|
|
103
|
+
* @param {string} targetImage - Target image tag
|
|
104
|
+
* @throws {Error} If tagging fails
|
|
105
|
+
*/
|
|
106
|
+
async function tagImage(sourceImage, targetImage) {
|
|
107
|
+
try {
|
|
108
|
+
console.log(chalk.blue(`Tagging ${sourceImage} as ${targetImage}...`));
|
|
109
|
+
await execAsync(`docker tag ${sourceImage} ${targetImage}`);
|
|
110
|
+
console.log(chalk.green(`✓ Tagged: ${targetImage}`));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`Failed to tag image: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Push Docker image to registry
|
|
118
|
+
* @param {string} imageWithTag - Image with full tag
|
|
119
|
+
* @throws {Error} If push fails
|
|
120
|
+
*/
|
|
121
|
+
async function pushImage(imageWithTag) {
|
|
122
|
+
try {
|
|
123
|
+
console.log(chalk.blue(`Pushing ${imageWithTag}...`));
|
|
124
|
+
await execAsync(`docker push ${imageWithTag}`);
|
|
125
|
+
console.log(chalk.green(`✓ Pushed: ${imageWithTag}`));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(`Failed to push image: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
checkAzureCLIInstalled,
|
|
133
|
+
extractRegistryName,
|
|
134
|
+
validateRegistryURL,
|
|
135
|
+
checkACRAuthentication,
|
|
136
|
+
authenticateACR,
|
|
137
|
+
checkLocalImageExists,
|
|
138
|
+
tagImage,
|
|
139
|
+
pushImage
|
|
140
|
+
};
|
|
141
|
+
|