@aifabrix/builder 2.32.3 → 2.33.1
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/.cursor/rules/project-rules.mdc +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +12 -11
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +6 -2
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +161 -23
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +17 -10
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +48 -31
- package/lib/cli.js +219 -70
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +7 -8
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +26 -17
- package/lib/commands/login.js +12 -10
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +110 -332
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +59 -23
- package/lib/datasource/list.js +108 -19
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +53 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +33 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +4 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +23 -4
- package/lib/schema/external-datasource.schema.json +2 -2
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +102 -52
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +65 -17
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +49 -0
- package/lib/utils/error-formatters/network-errors.js +13 -3
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +9 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +36 -3
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +18 -16
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/github/ci.yaml.hbs +44 -1
- package/templates/github/release.yaml.hbs +44 -0
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- package/templates/typescript/docker-compose.hbs +26 -0
|
@@ -9,234 +9,32 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const { exec } = require('child_process');
|
|
13
|
-
const { promisify } = require('util');
|
|
14
12
|
const path = require('path');
|
|
15
13
|
const fs = require('fs');
|
|
16
|
-
const handlebars = require('handlebars');
|
|
17
|
-
const secrets = require('../core/secrets');
|
|
18
14
|
const config = require('../core/config');
|
|
19
15
|
const devConfig = require('../utils/dev-config');
|
|
20
16
|
const logger = require('../utils/logger');
|
|
21
|
-
const containerUtils = require('../utils/infra-containers');
|
|
22
17
|
const dockerUtils = require('../utils/docker');
|
|
23
18
|
const paths = require('../utils/paths');
|
|
24
19
|
const statusHelpers = require('../utils/infra-status');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
const execAsync = promisify(exec);
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Gets infrastructure directory name based on developer ID
|
|
47
|
-
* Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
|
|
48
|
-
* @param {number|string} devId - Developer ID
|
|
49
|
-
* @returns {string} Infrastructure directory name
|
|
50
|
-
*/
|
|
51
|
-
function getInfraDirName(devId) {
|
|
52
|
-
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
53
|
-
return idNum === 0 ? 'infra' : `infra-dev${devId}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Gets Docker Compose project name based on developer ID
|
|
58
|
-
* Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
|
|
59
|
-
* @param {number|string} devId - Developer ID
|
|
60
|
-
* @returns {string} Docker Compose project name
|
|
61
|
-
*/
|
|
62
|
-
function getInfraProjectName(devId) {
|
|
63
|
-
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
64
|
-
return idNum === 0 ? 'infra' : `infra-dev${devId}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Wrapper to support cwd option
|
|
68
|
-
function execAsyncWithCwd(command, options = {}) {
|
|
69
|
-
return new Promise((resolve, reject) => {
|
|
70
|
-
const { cwd, ...execOptions } = options;
|
|
71
|
-
exec(command, { ...execOptions, cwd }, (error, stdout, stderr) => {
|
|
72
|
-
if (error) {
|
|
73
|
-
reject(error);
|
|
74
|
-
} else {
|
|
75
|
-
resolve({ stdout, stderr });
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Starts local infrastructure services
|
|
83
|
-
* Launches Postgres, Redis, Keycloak, and Controller in Docker containers
|
|
84
|
-
*
|
|
85
|
-
* @async
|
|
86
|
-
* @function startInfra
|
|
87
|
-
* @returns {Promise<void>} Resolves when infrastructure is started
|
|
88
|
-
* @throws {Error} If Docker is not running or compose fails
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* await startInfra();
|
|
92
|
-
* // Infrastructure services are now running
|
|
93
|
-
*/
|
|
94
|
-
async function checkDockerAvailability() {
|
|
95
|
-
try {
|
|
96
|
-
await dockerUtils.ensureDockerAndCompose();
|
|
97
|
-
} catch (error) {
|
|
98
|
-
throw new Error('Docker or Docker Compose is not available. Please install and start Docker.');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function ensureAdminSecrets() {
|
|
103
|
-
const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
|
|
104
|
-
if (!fs.existsSync(adminSecretsPath)) {
|
|
105
|
-
logger.log('Generating admin-secrets.env...');
|
|
106
|
-
await secrets.generateAdminSecretsEnv();
|
|
107
|
-
}
|
|
108
|
-
return adminSecretsPath;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Generates pgAdmin4 configuration files (servers.json and pgpass)
|
|
113
|
-
* @param {string} infraDir - Infrastructure directory path
|
|
114
|
-
* @param {string} postgresPassword - PostgreSQL password
|
|
115
|
-
*/
|
|
116
|
-
function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
117
|
-
const serversJsonTemplatePath = path.join(__dirname, '..', 'templates', 'infra', 'servers.json.hbs');
|
|
118
|
-
if (!fs.existsSync(serversJsonTemplatePath)) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const serversJsonTemplateContent = fs.readFileSync(serversJsonTemplatePath, 'utf8');
|
|
123
|
-
const serversJsonTemplate = handlebars.compile(serversJsonTemplateContent);
|
|
124
|
-
const serversJsonContent = serversJsonTemplate({ postgresPassword });
|
|
125
|
-
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
126
|
-
fs.writeFileSync(serversJsonPath, serversJsonContent, { mode: 0o644 });
|
|
127
|
-
|
|
128
|
-
const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
129
|
-
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
130
|
-
fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Prepare infrastructure directory and extract postgres password
|
|
135
|
-
* @param {string} devId - Developer ID
|
|
136
|
-
* @param {string} adminSecretsPath - Path to admin secrets file
|
|
137
|
-
* @returns {Object} Object with infraDir and postgresPassword
|
|
138
|
-
*/
|
|
139
|
-
function prepareInfraDirectory(devId, adminSecretsPath) {
|
|
140
|
-
const aifabrixDir = paths.getAifabrixHome();
|
|
141
|
-
const infraDirName = getInfraDirName(devId);
|
|
142
|
-
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
143
|
-
if (!fs.existsSync(infraDir)) {
|
|
144
|
-
fs.mkdirSync(infraDir, { recursive: true });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
|
|
148
|
-
const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
|
|
149
|
-
const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
|
|
150
|
-
generatePgAdminConfig(infraDir, postgresPassword);
|
|
151
|
-
|
|
152
|
-
return { infraDir, postgresPassword };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Register Handlebars helper for equality comparison
|
|
157
|
-
*/
|
|
158
|
-
function registerHandlebarsHelper() {
|
|
159
|
-
handlebars.registerHelper('eq', (a, b) => {
|
|
160
|
-
if (a === null || a === undefined) a = '0';
|
|
161
|
-
if (b === null || b === undefined) b = '0';
|
|
162
|
-
const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
|
|
163
|
-
const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
|
|
164
|
-
if (typeof aNum === 'number' && typeof bNum === 'number') {
|
|
165
|
-
return aNum === bNum;
|
|
166
|
-
}
|
|
167
|
-
return a === b;
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Generate docker-compose file from template
|
|
173
|
-
* @param {string} templatePath - Path to compose template
|
|
174
|
-
* @param {string} devId - Developer ID
|
|
175
|
-
* @param {number} idNum - Developer ID number
|
|
176
|
-
* @param {Object} ports - Port configuration
|
|
177
|
-
* @param {string} infraDir - Infrastructure directory
|
|
178
|
-
* @returns {string} Path to generated compose file
|
|
179
|
-
*/
|
|
180
|
-
function generateComposeFile(templatePath, devId, idNum, ports, infraDir) {
|
|
181
|
-
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
182
|
-
const template = handlebars.compile(templateContent);
|
|
183
|
-
const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
|
|
184
|
-
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
185
|
-
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
186
|
-
const composeContent = template({
|
|
187
|
-
devId: devId,
|
|
188
|
-
postgresPort: ports.postgres,
|
|
189
|
-
redisPort: ports.redis,
|
|
190
|
-
pgadminPort: ports.pgadmin,
|
|
191
|
-
redisCommanderPort: ports.redisCommander,
|
|
192
|
-
networkName: networkName,
|
|
193
|
-
serversJsonPath: serversJsonPath,
|
|
194
|
-
pgpassPath: pgpassPath,
|
|
195
|
-
infraDir: infraDir
|
|
196
|
-
});
|
|
197
|
-
const composePath = path.join(infraDir, 'compose.yaml');
|
|
198
|
-
fs.writeFileSync(composePath, composeContent);
|
|
199
|
-
return composePath;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Start Docker services using docker-compose
|
|
204
|
-
* @async
|
|
205
|
-
* @param {string} composePath - Path to compose file
|
|
206
|
-
* @param {string} projectName - Docker project name
|
|
207
|
-
* @param {string} adminSecretsPath - Path to admin secrets file
|
|
208
|
-
* @param {string} infraDir - Infrastructure directory
|
|
209
|
-
*/
|
|
210
|
-
async function startDockerServices(composePath, projectName, adminSecretsPath, infraDir) {
|
|
211
|
-
logger.log(`Using compose file: ${composePath}`);
|
|
212
|
-
logger.log('Starting infrastructure services...');
|
|
213
|
-
const composeCmd = await dockerUtils.getComposeCommand();
|
|
214
|
-
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
|
|
215
|
-
logger.log('Infrastructure services started successfully');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Copy pgAdmin4 configuration files into container
|
|
220
|
-
* @async
|
|
221
|
-
* @param {string} pgadminContainerName - pgAdmin container name
|
|
222
|
-
* @param {string} serversJsonPath - Path to servers.json file
|
|
223
|
-
* @param {string} pgpassPath - Path to pgpass file
|
|
224
|
-
*/
|
|
225
|
-
async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath) {
|
|
226
|
-
try {
|
|
227
|
-
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for container to be ready
|
|
228
|
-
if (fs.existsSync(serversJsonPath)) {
|
|
229
|
-
await execAsync(`docker cp "${serversJsonPath}" ${pgadminContainerName}:/pgadmin4/servers.json`);
|
|
230
|
-
}
|
|
231
|
-
if (fs.existsSync(pgpassPath)) {
|
|
232
|
-
await execAsync(`docker cp "${pgpassPath}" ${pgadminContainerName}:/pgpass`);
|
|
233
|
-
await execAsync(`docker exec ${pgadminContainerName} chmod 600 /pgpass`);
|
|
234
|
-
}
|
|
235
|
-
} catch (error) {
|
|
236
|
-
// Ignore copy errors - files might already be there or container not ready
|
|
237
|
-
logger.log('Note: Could not copy pgAdmin4 config files (this is OK if container was just restarted)');
|
|
238
|
-
}
|
|
239
|
-
}
|
|
20
|
+
const {
|
|
21
|
+
getInfraDirName,
|
|
22
|
+
getInfraProjectName,
|
|
23
|
+
checkDockerAvailability,
|
|
24
|
+
ensureAdminSecrets,
|
|
25
|
+
prepareInfraDirectory,
|
|
26
|
+
registerHandlebarsHelper
|
|
27
|
+
} = require('./helpers');
|
|
28
|
+
const {
|
|
29
|
+
buildTraefikConfig,
|
|
30
|
+
validateTraefikConfig,
|
|
31
|
+
generateComposeFile
|
|
32
|
+
} = require('./compose');
|
|
33
|
+
const {
|
|
34
|
+
execAsyncWithCwd,
|
|
35
|
+
startDockerServicesAndConfigure,
|
|
36
|
+
checkInfraHealth
|
|
37
|
+
} = require('./services');
|
|
240
38
|
|
|
241
39
|
/**
|
|
242
40
|
* Prepares infrastructure environment
|
|
@@ -254,7 +52,7 @@ async function prepareInfrastructureEnvironment(developerId) {
|
|
|
254
52
|
const ports = devConfig.getDevPorts(devIdNum);
|
|
255
53
|
const idNum = devIdNum;
|
|
256
54
|
|
|
257
|
-
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
|
|
55
|
+
const templatePath = path.join(__dirname, '..', '..', 'templates', 'infra', 'compose.yaml.hbs');
|
|
258
56
|
if (!fs.existsSync(templatePath)) {
|
|
259
57
|
throw new Error(`Compose template not found: ${templatePath}`);
|
|
260
58
|
}
|
|
@@ -266,39 +64,35 @@ async function prepareInfrastructureEnvironment(developerId) {
|
|
|
266
64
|
}
|
|
267
65
|
|
|
268
66
|
/**
|
|
269
|
-
* Starts
|
|
67
|
+
* Starts local infrastructure services
|
|
68
|
+
* Launches Postgres, Redis, pgAdmin, and Redis Commander in Docker containers
|
|
69
|
+
*
|
|
270
70
|
* @async
|
|
271
|
-
* @function
|
|
272
|
-
* @param {string}
|
|
273
|
-
* @param {
|
|
274
|
-
* @param {
|
|
275
|
-
* @
|
|
276
|
-
* @
|
|
71
|
+
* @function startInfra
|
|
72
|
+
* @param {number|string|null} developerId - Developer ID (null = use current)
|
|
73
|
+
* @param {Object} [options] - Infrastructure options
|
|
74
|
+
* @param {boolean} [options.traefik=false] - Include Traefik service
|
|
75
|
+
* @returns {Promise<void>} Resolves when infrastructure is started
|
|
76
|
+
* @throws {Error} If Docker is not running or compose fails
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* await startInfra(null, { traefik: true });
|
|
80
|
+
* // Infrastructure services are now running
|
|
277
81
|
*/
|
|
278
|
-
async function
|
|
279
|
-
// Start Docker services
|
|
280
|
-
const projectName = getInfraProjectName(devId);
|
|
281
|
-
await startDockerServices(composePath, projectName, adminSecretsPath, infraDir);
|
|
282
|
-
|
|
283
|
-
// Copy pgAdmin4 config files
|
|
284
|
-
const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
|
|
285
|
-
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
286
|
-
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
287
|
-
await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath);
|
|
288
|
-
|
|
289
|
-
// Wait for services to be healthy
|
|
290
|
-
await waitForServices(devId);
|
|
291
|
-
logger.log('All services are healthy and ready');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function startInfra(developerId = null) {
|
|
82
|
+
async function startInfra(developerId = null, options = {}) {
|
|
295
83
|
const { devId, idNum, ports, templatePath, infraDir, adminSecretsPath } = await prepareInfrastructureEnvironment(developerId);
|
|
84
|
+
const { traefik = false } = options;
|
|
85
|
+
const traefikConfig = buildTraefikConfig(traefik);
|
|
86
|
+
const validation = validateTraefikConfig(traefikConfig);
|
|
87
|
+
if (!validation.valid) {
|
|
88
|
+
throw new Error(validation.errors.join('\n'));
|
|
89
|
+
}
|
|
296
90
|
|
|
297
91
|
// Register Handlebars helper
|
|
298
92
|
registerHandlebarsHelper();
|
|
299
93
|
|
|
300
94
|
// Generate compose file
|
|
301
|
-
const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir);
|
|
95
|
+
const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, { traefik: traefikConfig });
|
|
302
96
|
|
|
303
97
|
try {
|
|
304
98
|
await startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir);
|
|
@@ -381,42 +175,6 @@ async function stopInfraWithVolumes() {
|
|
|
381
175
|
}
|
|
382
176
|
}
|
|
383
177
|
|
|
384
|
-
/**
|
|
385
|
-
* Checks if infrastructure services are running
|
|
386
|
-
* Validates that all required services are healthy and accessible
|
|
387
|
-
*
|
|
388
|
-
* @async
|
|
389
|
-
* @function checkInfraHealth
|
|
390
|
-
* @returns {Promise<Object>} Health status of each service
|
|
391
|
-
* @throws {Error} If health check fails
|
|
392
|
-
*
|
|
393
|
-
* @example
|
|
394
|
-
* const health = await checkInfraHealth();
|
|
395
|
-
* // Returns: { postgres: 'healthy', redis: 'healthy', keycloak: 'healthy', controller: 'healthy' }
|
|
396
|
-
*/
|
|
397
|
-
async function checkInfraHealth(devId = null) {
|
|
398
|
-
const developerId = devId || await config.getDeveloperId();
|
|
399
|
-
const servicesWithHealthCheck = ['postgres', 'redis'];
|
|
400
|
-
const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
|
|
401
|
-
const health = {};
|
|
402
|
-
|
|
403
|
-
// Check health status for services with health checks
|
|
404
|
-
for (const service of servicesWithHealthCheck) {
|
|
405
|
-
health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Check if services without health checks are running
|
|
409
|
-
for (const service of servicesWithoutHealthCheck) {
|
|
410
|
-
health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return health;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Re-export status helper functions
|
|
417
|
-
const getInfraStatus = statusHelpers.getInfraStatus;
|
|
418
|
-
const getAppStatus = statusHelpers.getAppStatus;
|
|
419
|
-
|
|
420
178
|
/**
|
|
421
179
|
* Restarts a specific infrastructure service
|
|
422
180
|
* Useful for applying configuration changes
|
|
@@ -436,7 +194,7 @@ async function restartService(serviceName) {
|
|
|
436
194
|
throw new Error('Service name is required and must be a string');
|
|
437
195
|
}
|
|
438
196
|
|
|
439
|
-
const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander'];
|
|
197
|
+
const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander', 'traefik'];
|
|
440
198
|
if (!validServices.includes(serviceName)) {
|
|
441
199
|
throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
|
|
442
200
|
}
|
|
@@ -463,34 +221,17 @@ async function restartService(serviceName) {
|
|
|
463
221
|
}
|
|
464
222
|
}
|
|
465
223
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
470
|
-
*/
|
|
471
|
-
async function waitForServices(devId = null) {
|
|
472
|
-
const maxAttempts = 30;
|
|
473
|
-
const delay = 2000; // 2 seconds
|
|
474
|
-
|
|
475
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
476
|
-
const health = await checkInfraHealth(devId);
|
|
477
|
-
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
478
|
-
|
|
479
|
-
if (allHealthy) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Debug logging
|
|
484
|
-
|
|
485
|
-
if (attempt < maxAttempts) {
|
|
486
|
-
logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
|
|
487
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
488
|
-
}
|
|
489
|
-
}
|
|
224
|
+
// Re-export status helper functions
|
|
225
|
+
const getInfraStatus = statusHelpers.getInfraStatus;
|
|
226
|
+
const getAppStatus = statusHelpers.getAppStatus;
|
|
490
227
|
|
|
491
|
-
throw new Error('Services failed to become healthy within timeout period');
|
|
492
|
-
}
|
|
493
228
|
module.exports = {
|
|
494
|
-
startInfra,
|
|
495
|
-
|
|
229
|
+
startInfra,
|
|
230
|
+
stopInfra,
|
|
231
|
+
stopInfraWithVolumes,
|
|
232
|
+
checkInfraHealth,
|
|
233
|
+
getInfraStatus,
|
|
234
|
+
getAppStatus,
|
|
235
|
+
restartService,
|
|
236
|
+
ensureAdminSecrets
|
|
496
237
|
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Infrastructure Docker Services
|
|
3
|
+
*
|
|
4
|
+
* Handles Docker service operations including starting, stopping, and configuring services.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Docker service management for infrastructure
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { exec } = require('child_process');
|
|
12
|
+
const { promisify } = require('util');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const logger = require('../utils/logger');
|
|
16
|
+
const containerUtils = require('../utils/infra-containers');
|
|
17
|
+
const dockerUtils = require('../utils/docker');
|
|
18
|
+
const config = require('../core/config');
|
|
19
|
+
const { getInfraProjectName } = require('./helpers');
|
|
20
|
+
|
|
21
|
+
const execAsync = promisify(exec);
|
|
22
|
+
|
|
23
|
+
// Wrapper to support cwd option
|
|
24
|
+
function execAsyncWithCwd(command, options = {}) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const { cwd, ...execOptions } = options;
|
|
27
|
+
exec(command, { ...execOptions, cwd }, (error, stdout, stderr) => {
|
|
28
|
+
if (error) {
|
|
29
|
+
reject(error);
|
|
30
|
+
} else {
|
|
31
|
+
resolve({ stdout, stderr });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start Docker services using docker-compose
|
|
39
|
+
* @async
|
|
40
|
+
* @param {string} composePath - Path to compose file
|
|
41
|
+
* @param {string} projectName - Docker project name
|
|
42
|
+
* @param {string} adminSecretsPath - Path to admin secrets file
|
|
43
|
+
* @param {string} infraDir - Infrastructure directory
|
|
44
|
+
*/
|
|
45
|
+
async function startDockerServices(composePath, projectName, adminSecretsPath, infraDir) {
|
|
46
|
+
logger.log(`Using compose file: ${composePath}`);
|
|
47
|
+
logger.log('Starting infrastructure services...');
|
|
48
|
+
const composeCmd = await dockerUtils.getComposeCommand();
|
|
49
|
+
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
|
|
50
|
+
logger.log('Infrastructure services started successfully');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Copy pgAdmin4 configuration files into container
|
|
55
|
+
* @async
|
|
56
|
+
* @param {string} pgadminContainerName - pgAdmin container name
|
|
57
|
+
* @param {string} serversJsonPath - Path to servers.json file
|
|
58
|
+
* @param {string} pgpassPath - Path to pgpass file
|
|
59
|
+
*/
|
|
60
|
+
async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath) {
|
|
61
|
+
try {
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for container to be ready
|
|
63
|
+
if (fs.existsSync(serversJsonPath)) {
|
|
64
|
+
await execAsync(`docker cp "${serversJsonPath}" ${pgadminContainerName}:/pgadmin4/servers.json`);
|
|
65
|
+
}
|
|
66
|
+
if (fs.existsSync(pgpassPath)) {
|
|
67
|
+
await execAsync(`docker cp "${pgpassPath}" ${pgadminContainerName}:/pgpass`);
|
|
68
|
+
await execAsync(`docker exec ${pgadminContainerName} chmod 600 /pgpass`);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Ignore copy errors - files might already be there or container not ready
|
|
72
|
+
logger.log('Note: Could not copy pgAdmin4 config files (this is OK if container was just restarted)');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Starts Docker services and configures pgAdmin
|
|
78
|
+
* @async
|
|
79
|
+
* @function startDockerServicesAndConfigure
|
|
80
|
+
* @param {string} composePath - Compose file path
|
|
81
|
+
* @param {string} devId - Developer ID
|
|
82
|
+
* @param {number} idNum - Developer ID number
|
|
83
|
+
* @param {string} adminSecretsPath - Admin secrets path
|
|
84
|
+
* @param {string} infraDir - Infrastructure directory
|
|
85
|
+
*/
|
|
86
|
+
async function startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir) {
|
|
87
|
+
// Start Docker services
|
|
88
|
+
const projectName = getInfraProjectName(devId);
|
|
89
|
+
await startDockerServices(composePath, projectName, adminSecretsPath, infraDir);
|
|
90
|
+
|
|
91
|
+
// Copy pgAdmin4 config files
|
|
92
|
+
const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
|
|
93
|
+
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
94
|
+
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
95
|
+
await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath);
|
|
96
|
+
|
|
97
|
+
// Wait for services to be healthy
|
|
98
|
+
await waitForServices(devId);
|
|
99
|
+
logger.log('All services are healthy and ready');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Waits for services to be healthy
|
|
104
|
+
* @private
|
|
105
|
+
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
106
|
+
*/
|
|
107
|
+
async function waitForServices(devId = null) {
|
|
108
|
+
const maxAttempts = 30;
|
|
109
|
+
const delay = 2000; // 2 seconds
|
|
110
|
+
|
|
111
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
112
|
+
const health = await checkInfraHealth(devId);
|
|
113
|
+
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
114
|
+
|
|
115
|
+
if (allHealthy) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (attempt < maxAttempts) {
|
|
120
|
+
logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
|
|
121
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new Error('Services failed to become healthy within timeout period');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Checks if infrastructure services are running
|
|
130
|
+
* Validates that all required services are healthy and accessible
|
|
131
|
+
*
|
|
132
|
+
* @async
|
|
133
|
+
* @function checkInfraHealth
|
|
134
|
+
* @param {number|string|null} [devId] - Developer ID (null = use current)
|
|
135
|
+
* @returns {Promise<Object>} Health status of each service
|
|
136
|
+
* @throws {Error} If health check fails
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* const health = await checkInfraHealth();
|
|
140
|
+
* // Returns: { postgres: 'healthy', redis: 'healthy', pgadmin: 'healthy', redis-commander: 'healthy' }
|
|
141
|
+
*/
|
|
142
|
+
async function checkInfraHealth(devId = null) {
|
|
143
|
+
const developerId = devId || await config.getDeveloperId();
|
|
144
|
+
const servicesWithHealthCheck = ['postgres', 'redis'];
|
|
145
|
+
const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
|
|
146
|
+
const health = {};
|
|
147
|
+
|
|
148
|
+
// Check health status for services with health checks
|
|
149
|
+
for (const service of servicesWithHealthCheck) {
|
|
150
|
+
health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if services without health checks are running
|
|
154
|
+
for (const service of servicesWithoutHealthCheck) {
|
|
155
|
+
health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return health;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
execAsyncWithCwd,
|
|
163
|
+
startDockerServices,
|
|
164
|
+
copyPgAdminConfig,
|
|
165
|
+
startDockerServicesAndConfigure,
|
|
166
|
+
waitForServices,
|
|
167
|
+
checkInfraHealth
|
|
168
|
+
};
|
|
@@ -457,20 +457,20 @@
|
|
|
457
457
|
},
|
|
458
458
|
"frontDoorRouting": {
|
|
459
459
|
"type": "object",
|
|
460
|
-
"description": "Front Door routing configuration",
|
|
460
|
+
"description": "Front Door routing configuration for Azure deployments and Traefik ingress for local development",
|
|
461
461
|
"properties": {
|
|
462
462
|
"pattern": {
|
|
463
463
|
"type": "string",
|
|
464
|
-
"description": "URL pattern for routing (e.g., '/app/*')",
|
|
464
|
+
"description": "URL pattern for routing (e.g., '/app/*', '/api/v1/*'). Used for both Front Door and Traefik path routing",
|
|
465
465
|
"pattern": "^/.+"
|
|
466
466
|
},
|
|
467
467
|
"requiresRuleSet": {
|
|
468
468
|
"type": "boolean",
|
|
469
|
-
"description": "Whether URL rewriting rule set is required"
|
|
469
|
+
"description": "Whether URL rewriting rule set is required (Azure Front Door only)"
|
|
470
470
|
},
|
|
471
471
|
"ruleSetConditions": {
|
|
472
472
|
"type": "array",
|
|
473
|
-
"description": "Rule set conditions for URL rewriting",
|
|
473
|
+
"description": "Rule set conditions for URL rewriting (Azure Front Door only)",
|
|
474
474
|
"items": {
|
|
475
475
|
"type": "object",
|
|
476
476
|
"properties": {
|
|
@@ -490,6 +490,25 @@
|
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
492
|
}
|
|
493
|
+
},
|
|
494
|
+
"enabled": {
|
|
495
|
+
"type": "boolean",
|
|
496
|
+
"description": "Enable Traefik ingress labels for local development (docker-compose)"
|
|
497
|
+
},
|
|
498
|
+
"host": {
|
|
499
|
+
"type": "string",
|
|
500
|
+
"description": "Hostname for Traefik routing. Supports ${DEV_USERNAME} variable interpolation (e.g., '${DEV_USERNAME}.aifabrix.dev'). Required if enabled is true.",
|
|
501
|
+
"pattern": "^[a-z0-9.$\\{\\}-]+$"
|
|
502
|
+
},
|
|
503
|
+
"tls": {
|
|
504
|
+
"type": "boolean",
|
|
505
|
+
"description": "Enable TLS/HTTPS for Traefik (local development)",
|
|
506
|
+
"default": true
|
|
507
|
+
},
|
|
508
|
+
"certStore": {
|
|
509
|
+
"type": "string",
|
|
510
|
+
"description": "Certificate store name for wildcard certificates. Optional - only needed when using a pre-configured certificate store in Traefik.",
|
|
511
|
+
"pattern": "^[a-z0-9-]+$"
|
|
493
512
|
}
|
|
494
513
|
},
|
|
495
514
|
"additionalProperties": false
|
|
@@ -176,8 +176,8 @@
|
|
|
176
176
|
"properties":{
|
|
177
177
|
"expression":{
|
|
178
178
|
"type":"string",
|
|
179
|
-
"description":"Pipe-based DSL expression: '{{raw.path}} | toLower | trim'.",
|
|
180
|
-
"pattern":"^\\s
|
|
179
|
+
"description":"Pipe-based DSL expression: '{{raw.path}} | toLower | trim' or record reference: 'record_ref:customer'.",
|
|
180
|
+
"pattern":"^\\s*((\\{\\{[^}]+\\}\\}(\\s*\\|\\s*[a-zA-Z0-9_]+(\\([^)]*\\))?)*)|(record_ref:[a-z0-9-]+))\\s*$"
|
|
181
181
|
},
|
|
182
182
|
"type":{
|
|
183
183
|
"type":"string",
|