@aifabrix/builder 2.0.0 → 2.0.2
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 +6 -2
- 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 +334 -133
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +209 -98
- 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/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/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 +13 -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 +168 -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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Dockerfile Generation
|
|
3
|
+
*
|
|
4
|
+
* Handles Dockerfile generation for applications
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Dockerfile generation for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
const build = require('./build');
|
|
16
|
+
const { validateAppName } = require('./app-push');
|
|
17
|
+
const logger = require('./utils/logger');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if Dockerfile exists and validates overwrite permission
|
|
21
|
+
* @async
|
|
22
|
+
* @param {string} dockerfilePath - Path to Dockerfile
|
|
23
|
+
* @param {Object} options - Generation options
|
|
24
|
+
* @throws {Error} If Dockerfile exists and force is not enabled
|
|
25
|
+
*/
|
|
26
|
+
async function checkDockerfileExists(dockerfilePath, options) {
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(dockerfilePath);
|
|
29
|
+
if (!options.force) {
|
|
30
|
+
throw new Error(`Dockerfile already exists at ${dockerfilePath}. Use --force to overwrite.`);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') {
|
|
34
|
+
// File doesn't exist, that's okay
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Loads application configuration from variables.yaml
|
|
43
|
+
* @async
|
|
44
|
+
* @param {string} configPath - Path to variables.yaml
|
|
45
|
+
* @param {Object} options - Generation options
|
|
46
|
+
* @returns {Promise<Object>} Application configuration
|
|
47
|
+
* @throws {Error} If configuration cannot be loaded
|
|
48
|
+
*/
|
|
49
|
+
async function loadAppConfig(configPath, options) {
|
|
50
|
+
try {
|
|
51
|
+
const yamlContent = await fs.readFile(configPath, 'utf8');
|
|
52
|
+
const variables = yaml.load(yamlContent);
|
|
53
|
+
return {
|
|
54
|
+
language: options.language || variables.build?.language || 'typescript',
|
|
55
|
+
port: variables.build?.port || variables.port || 3000,
|
|
56
|
+
...variables
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`Failed to load configuration from ${configPath}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generates and copies Dockerfile to app directory
|
|
65
|
+
* @async
|
|
66
|
+
* @param {string} appPath - Application directory path
|
|
67
|
+
* @param {string} dockerfilePath - Target Dockerfile path
|
|
68
|
+
* @param {Object} config - Application configuration
|
|
69
|
+
* @returns {Promise<string>} Path to generated Dockerfile
|
|
70
|
+
*/
|
|
71
|
+
async function generateAndCopyDockerfile(appPath, dockerfilePath, config) {
|
|
72
|
+
// Extract buildConfig from config to pass to generateDockerfile
|
|
73
|
+
const buildConfig = config.build || {};
|
|
74
|
+
const generatedPath = await build.generateDockerfile(appPath, config.language, config, buildConfig);
|
|
75
|
+
await fs.copyFile(generatedPath, dockerfilePath);
|
|
76
|
+
logger.log(chalk.green('✓ Generated Dockerfile from template'));
|
|
77
|
+
return dockerfilePath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate Dockerfile for an application
|
|
82
|
+
* @param {string} appName - Application name
|
|
83
|
+
* @param {Object} options - Generation options
|
|
84
|
+
* @returns {Promise<string>} Path to generated Dockerfile
|
|
85
|
+
*/
|
|
86
|
+
async function generateDockerfileForApp(appName, options = {}) {
|
|
87
|
+
try {
|
|
88
|
+
// Validate app name
|
|
89
|
+
validateAppName(appName);
|
|
90
|
+
|
|
91
|
+
const appPath = path.join(process.cwd(), 'builder', appName);
|
|
92
|
+
const dockerfilePath = path.join(appPath, 'Dockerfile');
|
|
93
|
+
|
|
94
|
+
// Check if Dockerfile already exists
|
|
95
|
+
await checkDockerfileExists(dockerfilePath, options);
|
|
96
|
+
|
|
97
|
+
// Load configuration
|
|
98
|
+
const configPath = path.join(appPath, 'variables.yaml');
|
|
99
|
+
const config = await loadAppConfig(configPath, options);
|
|
100
|
+
|
|
101
|
+
// Generate and copy Dockerfile
|
|
102
|
+
return await generateAndCopyDockerfile(appPath, dockerfilePath, config);
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new Error(`Failed to generate Dockerfile: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
generateDockerfileForApp
|
|
111
|
+
};
|
|
112
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Prompting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles interactive prompts for application configuration
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Prompt utilities for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const inquirer = require('inquirer');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds basic questions (port, language)
|
|
15
|
+
* @param {Object} options - Provided options
|
|
16
|
+
* @returns {Array} Array of question objects
|
|
17
|
+
*/
|
|
18
|
+
function buildBasicQuestions(options) {
|
|
19
|
+
const questions = [];
|
|
20
|
+
|
|
21
|
+
// Port validation
|
|
22
|
+
if (!options.port) {
|
|
23
|
+
questions.push({
|
|
24
|
+
type: 'input',
|
|
25
|
+
name: 'port',
|
|
26
|
+
message: 'What port should the application run on?',
|
|
27
|
+
default: '3000',
|
|
28
|
+
validate: (input) => {
|
|
29
|
+
const port = parseInt(input, 10);
|
|
30
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
31
|
+
return 'Port must be a number between 1 and 65535';
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Language selection
|
|
39
|
+
if (!options.language) {
|
|
40
|
+
questions.push({
|
|
41
|
+
type: 'list',
|
|
42
|
+
name: 'language',
|
|
43
|
+
message: 'What language is your application written in?',
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: 'TypeScript/Node.js', value: 'typescript' },
|
|
46
|
+
{ name: 'Python', value: 'python' }
|
|
47
|
+
],
|
|
48
|
+
default: 'typescript'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return questions;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds service questions (database, redis, storage, authentication)
|
|
57
|
+
* @param {Object} options - Provided options
|
|
58
|
+
* @returns {Array} Array of question objects
|
|
59
|
+
*/
|
|
60
|
+
function buildServiceQuestions(options) {
|
|
61
|
+
const questions = [];
|
|
62
|
+
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'database')) {
|
|
64
|
+
questions.push({
|
|
65
|
+
type: 'confirm',
|
|
66
|
+
name: 'database',
|
|
67
|
+
message: 'Does your application need a database?',
|
|
68
|
+
default: false
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'redis')) {
|
|
73
|
+
questions.push({
|
|
74
|
+
type: 'confirm',
|
|
75
|
+
name: 'redis',
|
|
76
|
+
message: 'Does your application need Redis?',
|
|
77
|
+
default: false
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'storage')) {
|
|
82
|
+
questions.push({
|
|
83
|
+
type: 'confirm',
|
|
84
|
+
name: 'storage',
|
|
85
|
+
message: 'Does your application need file storage?',
|
|
86
|
+
default: false
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'authentication')) {
|
|
91
|
+
questions.push({
|
|
92
|
+
type: 'confirm',
|
|
93
|
+
name: 'authentication',
|
|
94
|
+
message: 'Does your application need authentication/RBAC?',
|
|
95
|
+
default: false
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return questions;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds workflow questions (GitHub, Controller)
|
|
104
|
+
* @param {Object} options - Provided options
|
|
105
|
+
* @returns {Array} Array of question objects
|
|
106
|
+
*/
|
|
107
|
+
function buildWorkflowQuestions(options) {
|
|
108
|
+
const questions = [];
|
|
109
|
+
|
|
110
|
+
// GitHub workflows
|
|
111
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'github')) {
|
|
112
|
+
questions.push({
|
|
113
|
+
type: 'confirm',
|
|
114
|
+
name: 'github',
|
|
115
|
+
message: 'Do you need GitHub Actions workflows?',
|
|
116
|
+
default: false
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Controller deployment
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'controller') &&
|
|
122
|
+
options.github !== false) {
|
|
123
|
+
questions.push({
|
|
124
|
+
type: 'confirm',
|
|
125
|
+
name: 'controller',
|
|
126
|
+
message: 'Do you need Controller deployment workflow?',
|
|
127
|
+
default: false,
|
|
128
|
+
when: (answers) => answers.github !== false
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Controller URL
|
|
133
|
+
if (!options.controllerUrl && options.controller &&
|
|
134
|
+
!Object.prototype.hasOwnProperty.call(options, 'controllerUrl')) {
|
|
135
|
+
const misoHost = process.env.MISO_HOST || 'localhost';
|
|
136
|
+
questions.push({
|
|
137
|
+
type: 'input',
|
|
138
|
+
name: 'controllerUrl',
|
|
139
|
+
message: 'Enter Controller URL:',
|
|
140
|
+
default: `http://${misoHost}:3000`,
|
|
141
|
+
when: (answers) => answers.controller === true
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return questions;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolves conflicts between options and answers for a specific field
|
|
150
|
+
* @function resolveField
|
|
151
|
+
* @param {*} optionValue - Value from options
|
|
152
|
+
* @param {*} answerValue - Value from answers
|
|
153
|
+
* @param {*} defaultValue - Default value
|
|
154
|
+
* @returns {*} Resolved value
|
|
155
|
+
*/
|
|
156
|
+
function resolveField(optionValue, answerValue, defaultValue) {
|
|
157
|
+
if (optionValue !== undefined && optionValue !== null) {
|
|
158
|
+
return optionValue;
|
|
159
|
+
}
|
|
160
|
+
if (answerValue !== undefined && answerValue !== null) {
|
|
161
|
+
return answerValue;
|
|
162
|
+
}
|
|
163
|
+
return defaultValue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolves conflicts between options and answers for optional boolean fields
|
|
168
|
+
* @function resolveOptionalBoolean
|
|
169
|
+
* @param {*} optionValue - Value from options
|
|
170
|
+
* @param {*} answerValue - Value from answers
|
|
171
|
+
* @param {*} defaultValue - Default value
|
|
172
|
+
* @returns {*} Resolved value
|
|
173
|
+
*/
|
|
174
|
+
function resolveOptionalBoolean(optionValue, answerValue, defaultValue) {
|
|
175
|
+
if (optionValue !== undefined) {
|
|
176
|
+
return optionValue;
|
|
177
|
+
}
|
|
178
|
+
return answerValue !== undefined ? answerValue : defaultValue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolves conflicts between options and answers
|
|
183
|
+
* @function resolveConflicts
|
|
184
|
+
* @param {Object} options - Provided options
|
|
185
|
+
* @param {Object} answers - Prompt answers
|
|
186
|
+
* @returns {Object} Resolved configuration
|
|
187
|
+
*/
|
|
188
|
+
function resolveConflicts(options, answers) {
|
|
189
|
+
return {
|
|
190
|
+
port: parseInt(resolveField(options.port, answers.port, 3000), 10),
|
|
191
|
+
language: resolveField(options.language, answers.language, 'typescript'),
|
|
192
|
+
database: resolveField(options.database, answers.database, false),
|
|
193
|
+
redis: resolveField(options.redis, answers.redis, false),
|
|
194
|
+
storage: resolveField(options.storage, answers.storage, false),
|
|
195
|
+
authentication: resolveField(options.authentication, answers.authentication, false),
|
|
196
|
+
github: resolveOptionalBoolean(options.github, answers.github, false),
|
|
197
|
+
controller: resolveOptionalBoolean(options.controller, answers.controller, false),
|
|
198
|
+
controllerUrl: resolveField(options.controllerUrl, answers.controllerUrl, undefined)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Merges provided options with prompt answers
|
|
204
|
+
* @param {string} appName - Application name
|
|
205
|
+
* @param {Object} options - Provided options
|
|
206
|
+
* @param {Object} answers - Prompt answers
|
|
207
|
+
* @returns {Object} Complete configuration
|
|
208
|
+
*/
|
|
209
|
+
function mergePromptAnswers(appName, options, answers) {
|
|
210
|
+
return {
|
|
211
|
+
appName,
|
|
212
|
+
...resolveConflicts(options, answers)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Prompt for missing configuration options
|
|
218
|
+
* @param {string} appName - Application name
|
|
219
|
+
* @param {Object} options - Provided options
|
|
220
|
+
* @returns {Promise<Object>} Complete configuration
|
|
221
|
+
*/
|
|
222
|
+
async function promptForOptions(appName, options) {
|
|
223
|
+
// Default github to false if not provided (make it truly optional)
|
|
224
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'github')) {
|
|
225
|
+
options.github = false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const questions = [
|
|
229
|
+
...buildBasicQuestions(options),
|
|
230
|
+
...buildServiceQuestions(options),
|
|
231
|
+
...buildWorkflowQuestions(options)
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// Prompt for missing options
|
|
235
|
+
const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
236
|
+
|
|
237
|
+
// Merge provided options with answers
|
|
238
|
+
return mergePromptAnswers(appName, options, answers);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
promptForOptions
|
|
243
|
+
};
|
|
244
|
+
|
package/lib/app-push.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Push Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles pushing Docker images to container registries
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Push functionality for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
const pushUtils = require('./push');
|
|
16
|
+
const logger = require('./utils/logger');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate application name format
|
|
20
|
+
* @param {string} appName - Application name to validate
|
|
21
|
+
* @throws {Error} If app name is invalid
|
|
22
|
+
*/
|
|
23
|
+
function validateAppName(appName) {
|
|
24
|
+
if (!appName || typeof appName !== 'string') {
|
|
25
|
+
throw new Error('Application name is required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// App name should be lowercase, alphanumeric with dashes, 3-40 characters
|
|
29
|
+
const nameRegex = /^[a-z0-9-]{3,40}$/;
|
|
30
|
+
if (!nameRegex.test(appName)) {
|
|
31
|
+
throw new Error('Application name must be 3-40 characters, lowercase letters, numbers, and dashes only');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Cannot start or end with dash
|
|
35
|
+
if (appName.startsWith('-') || appName.endsWith('-')) {
|
|
36
|
+
throw new Error('Application name cannot start or end with a dash');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cannot have consecutive dashes
|
|
40
|
+
if (appName.includes('--')) {
|
|
41
|
+
throw new Error('Application name cannot have consecutive dashes');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loads push configuration from variables.yaml
|
|
47
|
+
* @async
|
|
48
|
+
* @param {string} appName - Application name
|
|
49
|
+
* @param {Object} options - Push options
|
|
50
|
+
* @returns {Promise<Object>} Configuration with registry
|
|
51
|
+
* @throws {Error} If configuration cannot be loaded
|
|
52
|
+
*/
|
|
53
|
+
async function loadPushConfig(appName, options) {
|
|
54
|
+
const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
55
|
+
try {
|
|
56
|
+
const config = yaml.load(await fs.readFile(configPath, 'utf8'));
|
|
57
|
+
const registry = options.registry || config.image?.registry;
|
|
58
|
+
if (!registry) {
|
|
59
|
+
throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
|
|
60
|
+
}
|
|
61
|
+
return { registry };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.message.includes('Registry URL')) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Failed to load configuration: ${configPath}\nRun 'aifabrix create ${appName}' first`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validates push configuration
|
|
72
|
+
* @param {string} registry - Registry URL
|
|
73
|
+
* @param {string} appName - Application name
|
|
74
|
+
* @throws {Error} If validation fails
|
|
75
|
+
*/
|
|
76
|
+
async function validatePushConfig(registry, appName) {
|
|
77
|
+
// Validate ACR URL format specifically (must be *.azurecr.io)
|
|
78
|
+
if (!/^[^.]+\.azurecr\.io$/.test(registry)) {
|
|
79
|
+
throw new Error(`Invalid ACR URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!pushUtils.validateRegistryURL(registry)) {
|
|
83
|
+
throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
|
|
87
|
+
throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!await pushUtils.checkAzureCLIInstalled()) {
|
|
91
|
+
throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Authenticates with Azure Container Registry
|
|
97
|
+
* @async
|
|
98
|
+
* @param {string} registry - Registry URL
|
|
99
|
+
*/
|
|
100
|
+
async function authenticateWithRegistry(registry) {
|
|
101
|
+
if (await pushUtils.checkACRAuthentication(registry)) {
|
|
102
|
+
logger.log(chalk.green(`✓ Already authenticated with ${registry}`));
|
|
103
|
+
} else {
|
|
104
|
+
await pushUtils.authenticateACR(registry);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pushes image tags to registry
|
|
110
|
+
* @async
|
|
111
|
+
* @param {string} appName - Application name
|
|
112
|
+
* @param {string} registry - Registry URL
|
|
113
|
+
* @param {Array<string>} tags - Image tags
|
|
114
|
+
*/
|
|
115
|
+
async function pushImageTags(appName, registry, tags) {
|
|
116
|
+
await Promise.all(tags.map(async(tag) => {
|
|
117
|
+
await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
|
|
118
|
+
await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Displays push results
|
|
124
|
+
* @param {string} registry - Registry URL
|
|
125
|
+
* @param {string} appName - Application name
|
|
126
|
+
* @param {Array<string>} tags - Image tags
|
|
127
|
+
*/
|
|
128
|
+
function displayPushResults(registry, appName, tags) {
|
|
129
|
+
logger.log(chalk.green(`\n✓ Successfully pushed ${tags.length} tag(s) to ${registry}`));
|
|
130
|
+
logger.log(chalk.gray(`Image: ${registry}/${appName}:*`));
|
|
131
|
+
logger.log(chalk.gray(`Tags: ${tags.join(', ')}`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Pushes application image to Azure Container Registry
|
|
136
|
+
* @async
|
|
137
|
+
* @function pushApp
|
|
138
|
+
* @param {string} appName - Name of the application
|
|
139
|
+
* @param {Object} options - Push options (registry, tag)
|
|
140
|
+
* @returns {Promise<void>} Resolves when push is complete
|
|
141
|
+
*/
|
|
142
|
+
async function pushApp(appName, options = {}) {
|
|
143
|
+
try {
|
|
144
|
+
// Validate app name
|
|
145
|
+
validateAppName(appName);
|
|
146
|
+
|
|
147
|
+
// Load configuration
|
|
148
|
+
const { registry } = await loadPushConfig(appName, options);
|
|
149
|
+
|
|
150
|
+
// Validate push configuration
|
|
151
|
+
await validatePushConfig(registry, appName);
|
|
152
|
+
|
|
153
|
+
// Authenticate with registry
|
|
154
|
+
await authenticateWithRegistry(registry);
|
|
155
|
+
|
|
156
|
+
// Push image tags
|
|
157
|
+
const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
|
|
158
|
+
await pushImageTags(appName, registry, tags);
|
|
159
|
+
|
|
160
|
+
// Display results
|
|
161
|
+
displayPushResults(registry, appName, tags);
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new Error(`Failed to push application: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
pushApp,
|
|
170
|
+
validateAppName
|
|
171
|
+
};
|
|
172
|
+
|