@aifabrix/builder 2.40.2 → 2.41.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/README.md +6 -4
- package/integration/hubspot/test.js +1 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/app/config.js +21 -0
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +9 -0
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +1 -3
- package/lib/app/run-env-compose.js +201 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +140 -14
- package/lib/cli/setup-dev.js +180 -17
- package/lib/cli/setup-environment.js +4 -2
- package/lib/cli/setup-external-system.js +71 -21
- package/lib/cli/setup-infra.js +29 -2
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +12 -3
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +309 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +26 -1
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +147 -81
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +7 -0
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test.js +5 -1
- package/lib/generator/index.js +174 -25
- package/lib/generator/wizard.js +8 -0
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +88 -10
- package/lib/infrastructure/services.js +70 -15
- package/lib/schema/application-schema.json +24 -3
- package/lib/schema/external-system.schema.json +435 -413
- package/lib/utils/api.js +3 -3
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +76 -75
- package/lib/utils/compose-handlebars-helpers.js +43 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/credential-secrets-env.js +267 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +83 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -1
- package/lib/utils/help-builder.js +15 -2
- package/lib/utils/infra-status.js +30 -1
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +43 -33
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-generator.js +94 -6
- package/lib/utils/secrets-helpers.js +33 -25
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +5 -4
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/validator.js +65 -0
- package/package.json +2 -2
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +5 -4
- package/templates/applications/dataplane/env.template +12 -7
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +11 -9
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const fs = require('fs');
|
|
14
|
+
const chalk = require('chalk');
|
|
14
15
|
const handlebars = require('handlebars');
|
|
15
16
|
const secrets = require('../core/secrets');
|
|
16
17
|
const logger = require('../utils/logger');
|
|
17
18
|
const dockerUtils = require('../utils/docker');
|
|
18
19
|
const paths = require('../utils/paths');
|
|
20
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Gets infrastructure directory name based on developer ID
|
|
@@ -53,24 +55,92 @@ async function checkDockerAvailability() {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/** Default admin password for new local installations when admin-secrets.env is empty */
|
|
59
|
+
const DEFAULT_ADMIN_PASSWORD = 'admin123';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Log hint to reset Postgres volume when admin password was changed after first init.
|
|
63
|
+
* @param {string} infraDir - Path to infra directory
|
|
64
|
+
*/
|
|
65
|
+
function logVolumeResetHint(infraDir) {
|
|
66
|
+
logger.log(chalk.yellow(
|
|
67
|
+
'If Postgres was already started with a different password, login will fail until you reset the volume. ' +
|
|
68
|
+
`Run: cd ${infraDir} && docker compose -f compose.yaml -p aifabrix down -v , then run 'aifabrix up-infra --adminPwd <password>' again.`
|
|
69
|
+
));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Apply password to admin-secrets file content (all three password keys).
|
|
74
|
+
* @param {string} content - Current file content
|
|
75
|
+
* @param {string} password - Password to set
|
|
76
|
+
* @returns {string} Updated content
|
|
77
|
+
*/
|
|
78
|
+
function applyPasswordToAdminSecretsContent(content, password) {
|
|
79
|
+
return content
|
|
80
|
+
.replace(/^POSTGRES_PASSWORD=.*$/m, `POSTGRES_PASSWORD=${password}`)
|
|
81
|
+
.replace(/^PGADMIN_DEFAULT_PASSWORD=.*$/m, `PGADMIN_DEFAULT_PASSWORD=${password}`)
|
|
82
|
+
.replace(/^REDIS_COMMANDER_PASSWORD=.*$/m, `REDIS_COMMANDER_PASSWORD=${password}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
/**
|
|
57
|
-
*
|
|
86
|
+
* Sync postgres-passwordKeyVault to the main secrets store (file or remote).
|
|
87
|
+
* @param {string} password - Password to store
|
|
88
|
+
*/
|
|
89
|
+
async function syncPostgresPasswordToStore(password) {
|
|
90
|
+
try {
|
|
91
|
+
await secretsEnsure.setSecretInStore('postgres-passwordKeyVault', password);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
logger.warn(`Could not sync postgres-passwordKeyVault to secrets store: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Ensure admin secrets file exists and set admin password.
|
|
99
|
+
* When adminPwd is provided, update POSTGRES_PASSWORD, PGADMIN_DEFAULT_PASSWORD, REDIS_COMMANDER_PASSWORD
|
|
100
|
+
* in admin-secrets.env (overwrites existing values). Otherwise only backfill empty fields.
|
|
101
|
+
*
|
|
58
102
|
* @async
|
|
103
|
+
* @param {Object} [options] - Options
|
|
104
|
+
* @param {string} [options.adminPwd] - Override admin password for Postgres, pgAdmin, Redis Commander (updates file when provided)
|
|
59
105
|
* @returns {Promise<string>} Path to admin secrets file
|
|
60
106
|
*/
|
|
61
|
-
async function ensureAdminSecrets() {
|
|
107
|
+
async function ensureAdminSecrets(options = {}) {
|
|
108
|
+
const adminPwdOverride = options.adminPwd && typeof options.adminPwd === 'string' && options.adminPwd.trim() !== ''
|
|
109
|
+
? options.adminPwd.trim()
|
|
110
|
+
: null;
|
|
111
|
+
const passwordToUse = adminPwdOverride || DEFAULT_ADMIN_PASSWORD;
|
|
112
|
+
|
|
62
113
|
const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
|
|
63
114
|
if (!fs.existsSync(adminSecretsPath)) {
|
|
64
115
|
logger.log('Generating admin-secrets.env...');
|
|
65
|
-
await secrets.generateAdminSecretsEnv();
|
|
116
|
+
await secrets.generateAdminSecretsEnv(undefined);
|
|
117
|
+
}
|
|
118
|
+
let content = fs.readFileSync(adminSecretsPath, 'utf8');
|
|
119
|
+
const needsBackfill = /^POSTGRES_PASSWORD=\s*$/m.test(content) ||
|
|
120
|
+
/^PGADMIN_DEFAULT_PASSWORD=\s*$/m.test(content) ||
|
|
121
|
+
/^REDIS_COMMANDER_PASSWORD=\s*$/m.test(content);
|
|
122
|
+
const shouldOverwriteWithAdminPwd = adminPwdOverride !== null;
|
|
123
|
+
|
|
124
|
+
if (shouldOverwriteWithAdminPwd) {
|
|
125
|
+
content = applyPasswordToAdminSecretsContent(content, passwordToUse);
|
|
126
|
+
fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
|
|
127
|
+
logger.log('Updated admin password in admin-secrets.env.');
|
|
128
|
+
await syncPostgresPasswordToStore(passwordToUse);
|
|
129
|
+
logVolumeResetHint(path.join(paths.getAifabrixHome(), getInfraDirName(0)));
|
|
130
|
+
} else if (needsBackfill) {
|
|
131
|
+
content = applyPasswordToAdminSecretsContent(content, passwordToUse);
|
|
132
|
+
fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
|
|
133
|
+
logger.log('Set default admin password in admin-secrets.env for local use.');
|
|
66
134
|
}
|
|
67
135
|
return adminSecretsPath;
|
|
68
136
|
}
|
|
69
137
|
|
|
70
138
|
/**
|
|
71
|
-
* Generates pgAdmin4
|
|
139
|
+
* Generates pgAdmin4 servers.json only. pgpass is not written to disk (ISO 27K);
|
|
140
|
+
* it is created temporarily in startDockerServicesAndConfigure and deleted after copy to container.
|
|
141
|
+
*
|
|
72
142
|
* @param {string} infraDir - Infrastructure directory path
|
|
73
|
-
* @param {string} postgresPassword - PostgreSQL password
|
|
143
|
+
* @param {string} postgresPassword - PostgreSQL password (for servers.json PassFile reference only; password not stored in file)
|
|
74
144
|
*/
|
|
75
145
|
function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
76
146
|
const serversJsonTemplatePath = path.join(__dirname, '..', '..', 'templates', 'infra', 'servers.json.hbs');
|
|
@@ -83,10 +153,6 @@ function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
|
83
153
|
const serversJsonContent = serversJsonTemplate({ postgresPassword });
|
|
84
154
|
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
85
155
|
fs.writeFileSync(serversJsonPath, serversJsonContent, { mode: 0o644 });
|
|
86
|
-
|
|
87
|
-
const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
88
|
-
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
89
|
-
fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
|
|
90
156
|
}
|
|
91
157
|
|
|
92
158
|
/**
|
|
@@ -117,10 +183,8 @@ function extractPasswordFromUrlOrValue(urlOrPassword) {
|
|
|
117
183
|
|
|
118
184
|
/**
|
|
119
185
|
* Ensures Postgres init script exists for miso-controller app (database miso, user miso_user).
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* started before this script existed, run `aifabrix down-infra -v` then `aifabrix up-infra` to re-init, or
|
|
123
|
-
* create the database and user manually (e.g. via pgAdmin or psql).
|
|
186
|
+
* Reads password from configured store (file or remote). Fails with clear message if secret is missing.
|
|
187
|
+
* Run ensureInfraSecrets before startInfra so databases-miso-controller-0-passwordKeyVault exists.
|
|
124
188
|
*
|
|
125
189
|
* @async
|
|
126
190
|
* @param {string} infraDir - Infrastructure directory path
|
|
@@ -132,15 +196,24 @@ async function ensureMisoInitScript(infraDir) {
|
|
|
132
196
|
fs.mkdirSync(initScriptsDir, { recursive: true });
|
|
133
197
|
}
|
|
134
198
|
|
|
135
|
-
|
|
199
|
+
const secretKey = 'databases-miso-controller-0-passwordKeyVault';
|
|
200
|
+
let password;
|
|
136
201
|
try {
|
|
137
202
|
const loaded = await secrets.loadSecrets(undefined);
|
|
138
|
-
const urlOrPassword = loaded['databases-miso-controller-0-
|
|
139
|
-
loaded['databases-miso-controller-0-urlKeyVault'];
|
|
203
|
+
const urlOrPassword = loaded[secretKey] || loaded['databases-miso-controller-0-urlKeyVault'];
|
|
140
204
|
const extracted = extractPasswordFromUrlOrValue(urlOrPassword);
|
|
141
|
-
if (extracted !== null)
|
|
142
|
-
|
|
143
|
-
|
|
205
|
+
if (extracted !== null && extracted.trim() !== '') {
|
|
206
|
+
password = extracted;
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Secret ${secretKey} not found or could not load secrets. Run "aifabrix up-infra" to ensure infra secrets, or add it to your secrets file. ${err.message}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (!password || password.trim() === '') {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Secret ${secretKey} is missing or empty. Run "aifabrix up-infra" to ensure infra secrets, or add it to your secrets file.`
|
|
216
|
+
);
|
|
144
217
|
}
|
|
145
218
|
|
|
146
219
|
const passwordEscaped = escapePgString(password);
|
|
@@ -183,9 +256,19 @@ function prepareInfraDirectory(devId, adminSecretsPath) {
|
|
|
183
256
|
fs.mkdirSync(infraDir, { recursive: true });
|
|
184
257
|
}
|
|
185
258
|
|
|
259
|
+
const oldPgpassPath = path.join(infraDir, 'pgpass');
|
|
260
|
+
if (fs.existsSync(oldPgpassPath)) {
|
|
261
|
+
try {
|
|
262
|
+
fs.unlinkSync(oldPgpassPath);
|
|
263
|
+
} catch {
|
|
264
|
+
// Ignore
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
186
268
|
const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
|
|
187
269
|
const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
|
|
188
|
-
const
|
|
270
|
+
const raw = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
|
|
271
|
+
const postgresPassword = (raw && raw.trim()) || DEFAULT_ADMIN_PASSWORD;
|
|
189
272
|
generatePgAdminConfig(infraDir, postgresPassword);
|
|
190
273
|
|
|
191
274
|
return { infraDir, postgresPassword };
|
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
ensureMisoInitScript,
|
|
27
27
|
registerHandlebarsHelper
|
|
28
28
|
} = require('./helpers');
|
|
29
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
29
30
|
const {
|
|
30
31
|
buildTraefikConfig,
|
|
31
32
|
validateTraefikConfig,
|
|
@@ -36,17 +37,22 @@ const {
|
|
|
36
37
|
startDockerServicesAndConfigure,
|
|
37
38
|
checkInfraHealth
|
|
38
39
|
} = require('./services');
|
|
40
|
+
// Lazy require to avoid circular dependency: infra -> app/down -> run-helpers -> infra
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Prepares infrastructure environment
|
|
44
|
+
* Ensures infra secrets exist, then admin-secrets.env, then miso init script.
|
|
45
|
+
*
|
|
42
46
|
* @async
|
|
43
47
|
* @function prepareInfrastructureEnvironment
|
|
44
48
|
* @param {string|number|null} developerId - Developer ID
|
|
49
|
+
* @param {Object} [options] - Options (traefik, adminPwd)
|
|
45
50
|
* @returns {Promise<Object>} Prepared environment configuration
|
|
46
51
|
*/
|
|
47
|
-
async function prepareInfrastructureEnvironment(developerId) {
|
|
52
|
+
async function prepareInfrastructureEnvironment(developerId, options = {}) {
|
|
48
53
|
await checkDockerAvailability();
|
|
49
|
-
|
|
54
|
+
await secretsEnsure.ensureInfraSecrets({ adminPwd: options.adminPwd });
|
|
55
|
+
const adminSecretsPath = await ensureAdminSecrets({ adminPwd: options.adminPwd });
|
|
50
56
|
|
|
51
57
|
const devId = developerId || await config.getDeveloperId();
|
|
52
58
|
const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
@@ -82,7 +88,7 @@ async function prepareInfrastructureEnvironment(developerId) {
|
|
|
82
88
|
* // Infrastructure services are now running
|
|
83
89
|
*/
|
|
84
90
|
async function startInfra(developerId = null, options = {}) {
|
|
85
|
-
const { devId, idNum, ports, templatePath, infraDir
|
|
91
|
+
const { devId, idNum, ports, templatePath, infraDir } = await prepareInfrastructureEnvironment(developerId, options);
|
|
86
92
|
const { traefik = false } = options;
|
|
87
93
|
const traefikConfig = buildTraefikConfig(traefik);
|
|
88
94
|
const validation = validateTraefikConfig(traefikConfig);
|
|
@@ -97,15 +103,56 @@ async function startInfra(developerId = null, options = {}) {
|
|
|
97
103
|
const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, { traefik: traefikConfig });
|
|
98
104
|
|
|
99
105
|
try {
|
|
100
|
-
await startDockerServicesAndConfigure(composePath, devId, idNum,
|
|
106
|
+
await startDockerServicesAndConfigure(composePath, devId, idNum, infraDir);
|
|
101
107
|
} finally {
|
|
102
108
|
// Keep the compose file for stop commands
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
/**
|
|
107
|
-
* Stops and removes
|
|
108
|
-
*
|
|
113
|
+
* Stops and removes all app containers for the current developer (same network).
|
|
114
|
+
* @param {string} devId - Developer ID
|
|
115
|
+
* @returns {Promise<void>}
|
|
116
|
+
*/
|
|
117
|
+
async function stopAllAppContainers(devId) {
|
|
118
|
+
const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
|
|
119
|
+
for (const name of containerNames) {
|
|
120
|
+
try {
|
|
121
|
+
await execAsyncWithCwd(`docker rm -f ${name}`);
|
|
122
|
+
logger.log(`Stopped and removed container: ${name}`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.log(`Container ${name} not running or already removed`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Removes Docker volumes for the given app names (current developer).
|
|
131
|
+
* @param {string[]} appNames - Application names
|
|
132
|
+
* @param {string} devId - Developer ID
|
|
133
|
+
* @returns {Promise<void>}
|
|
134
|
+
*/
|
|
135
|
+
async function removeAppVolumes(appNames, devId) {
|
|
136
|
+
const { getAppVolumeName } = require('../app/down');
|
|
137
|
+
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
138
|
+
for (const appName of appNames) {
|
|
139
|
+
const primaryName = getAppVolumeName(appName, devId);
|
|
140
|
+
const legacyDev0Name = idNum === 0 ? `aifabrix_dev0_${appName}_data` : null;
|
|
141
|
+
const candidates = Array.from(new Set([primaryName, legacyDev0Name].filter(Boolean)));
|
|
142
|
+
for (const volumeName of candidates) {
|
|
143
|
+
try {
|
|
144
|
+
await execAsyncWithCwd(`docker volume rm -f ${volumeName}`);
|
|
145
|
+
logger.log(`Removed volume: ${volumeName}`);
|
|
146
|
+
} catch {
|
|
147
|
+
logger.log(`Volume ${volumeName} not found or already removed`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Stops and removes local infrastructure services and all application containers
|
|
155
|
+
* on the same network. Cleanly shuts down infra and app containers.
|
|
109
156
|
*
|
|
110
157
|
* @async
|
|
111
158
|
* @function stopInfra
|
|
@@ -114,7 +161,7 @@ async function startInfra(developerId = null, options = {}) {
|
|
|
114
161
|
*
|
|
115
162
|
* @example
|
|
116
163
|
* await stopInfra();
|
|
117
|
-
* // All infrastructure containers are stopped and removed
|
|
164
|
+
* // All infrastructure and app containers on the same network are stopped and removed
|
|
118
165
|
*/
|
|
119
166
|
async function stopInfra() {
|
|
120
167
|
const devId = await config.getDeveloperId();
|
|
@@ -130,6 +177,8 @@ async function stopInfra() {
|
|
|
130
177
|
}
|
|
131
178
|
|
|
132
179
|
try {
|
|
180
|
+
logger.log('Stopping application containers on the same network...');
|
|
181
|
+
await stopAllAppContainers(devId);
|
|
133
182
|
logger.log('Stopping infrastructure services...');
|
|
134
183
|
const projectName = getInfraProjectName(devId);
|
|
135
184
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
@@ -141,8 +190,35 @@ async function stopInfra() {
|
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
/**
|
|
144
|
-
* Stops
|
|
145
|
-
*
|
|
193
|
+
* Stops all app containers on the network and removes their volumes.
|
|
194
|
+
* @param {string} devId - Developer ID
|
|
195
|
+
* @returns {Promise<void>}
|
|
196
|
+
*/
|
|
197
|
+
async function stopAllAppContainersAndVolumes(devId) {
|
|
198
|
+
const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
199
|
+
const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
|
|
200
|
+
for (const name of containerNames) {
|
|
201
|
+
try {
|
|
202
|
+
await execAsyncWithCwd(`docker rm -f ${name}`);
|
|
203
|
+
logger.log(`Stopped and removed container: ${name}`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
logger.log(`Container ${name} not running or already removed`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const appNames = [...new Set(
|
|
209
|
+
containerNames
|
|
210
|
+
.map(n => statusHelpers.extractAppName(n, devIdNum, devId))
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
)];
|
|
213
|
+
if (appNames.length > 0) {
|
|
214
|
+
logger.log('Removing application volumes...');
|
|
215
|
+
await removeAppVolumes(appNames, devId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Stops and removes local infrastructure services and all application containers
|
|
221
|
+
* on the same network, and removes all volumes (infra and app data).
|
|
146
222
|
*
|
|
147
223
|
* @async
|
|
148
224
|
* @function stopInfraWithVolumes
|
|
@@ -151,7 +227,7 @@ async function stopInfra() {
|
|
|
151
227
|
*
|
|
152
228
|
* @example
|
|
153
229
|
* await stopInfraWithVolumes();
|
|
154
|
-
* // All infrastructure containers and
|
|
230
|
+
* // All infrastructure and app containers and volumes are removed
|
|
155
231
|
*/
|
|
156
232
|
async function stopInfraWithVolumes() {
|
|
157
233
|
const devId = await config.getDeveloperId();
|
|
@@ -167,6 +243,8 @@ async function stopInfraWithVolumes() {
|
|
|
167
243
|
}
|
|
168
244
|
|
|
169
245
|
try {
|
|
246
|
+
logger.log('Stopping application containers on the same network...');
|
|
247
|
+
await stopAllAppContainersAndVolumes(devId);
|
|
170
248
|
logger.log('Stopping infrastructure services and removing all data...');
|
|
171
249
|
const projectName = getInfraProjectName(devId);
|
|
172
250
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
@@ -17,6 +17,7 @@ const containerUtils = require('../utils/infra-containers');
|
|
|
17
17
|
const dockerUtils = require('../utils/docker');
|
|
18
18
|
const config = require('../core/config');
|
|
19
19
|
const { getInfraProjectName } = require('./helpers');
|
|
20
|
+
const adminSecrets = require('../core/admin-secrets');
|
|
20
21
|
|
|
21
22
|
const execAsync = promisify(exec);
|
|
22
23
|
|
|
@@ -74,29 +75,83 @@ async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPa
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
*
|
|
78
|
+
* Prepare run env file from decrypted admin secrets.
|
|
79
|
+
* @async
|
|
80
|
+
* @param {string} infraDir - Infrastructure directory
|
|
81
|
+
* @returns {Promise<{ adminObj: Object, runEnvPath: string }>}
|
|
82
|
+
*/
|
|
83
|
+
async function prepareRunEnv(infraDir) {
|
|
84
|
+
const runEnvPath = path.join(infraDir, '.env.run');
|
|
85
|
+
const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
|
|
86
|
+
const content = adminSecrets.envObjectToContent(adminObj);
|
|
87
|
+
fs.writeFileSync(runEnvPath, content, { mode: 0o600 });
|
|
88
|
+
return { adminObj, runEnvPath };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Write pgpass file and copy pgAdmin config into container.
|
|
93
|
+
* @async
|
|
94
|
+
* @param {string} infraDir - Infrastructure directory
|
|
95
|
+
* @param {Object} adminObj - Decrypted admin secrets object
|
|
96
|
+
* @param {string} devId - Developer ID
|
|
97
|
+
* @param {number} idNum - Developer ID number
|
|
98
|
+
* @returns {Promise<string>} Path to pgpass run file
|
|
99
|
+
*/
|
|
100
|
+
async function writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum) {
|
|
101
|
+
const pgpassRunPath = path.join(infraDir, '.pgpass.run');
|
|
102
|
+
const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
|
|
103
|
+
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
104
|
+
const postgresPassword = adminObj.POSTGRES_PASSWORD || '';
|
|
105
|
+
const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
106
|
+
fs.writeFileSync(pgpassRunPath, pgpassContent, { mode: 0o600 });
|
|
107
|
+
await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassRunPath);
|
|
108
|
+
return pgpassRunPath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove temporary run files (env and pgpass) if they exist.
|
|
113
|
+
* @param {string} runEnvPath - Path to .env.run
|
|
114
|
+
* @param {string} [pgpassRunPath] - Path to .pgpass.run
|
|
115
|
+
*/
|
|
116
|
+
function cleanupRunFiles(runEnvPath, pgpassRunPath) {
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(runEnvPath)) fs.unlinkSync(runEnvPath);
|
|
119
|
+
if (pgpassRunPath && fs.existsSync(pgpassRunPath)) fs.unlinkSync(pgpassRunPath);
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore unlink errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Starts Docker services and configures pgAdmin.
|
|
127
|
+
* Writes decrypted admin secrets to a temporary .env in infra dir, runs compose, then deletes the file (ISO 27K).
|
|
128
|
+
*
|
|
78
129
|
* @async
|
|
79
130
|
* @function startDockerServicesAndConfigure
|
|
80
131
|
* @param {string} composePath - Compose file path
|
|
81
132
|
* @param {string} devId - Developer ID
|
|
82
133
|
* @param {number} idNum - Developer ID number
|
|
83
|
-
* @param {string} adminSecretsPath - Admin secrets path
|
|
84
134
|
* @param {string} infraDir - Infrastructure directory
|
|
85
135
|
*/
|
|
86
|
-
async function startDockerServicesAndConfigure(composePath, devId, idNum,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath);
|
|
136
|
+
async function startDockerServicesAndConfigure(composePath, devId, idNum, infraDir) {
|
|
137
|
+
let runEnvPath;
|
|
138
|
+
let pgpassRunPath;
|
|
139
|
+
let adminObj;
|
|
140
|
+
try {
|
|
141
|
+
({ adminObj, runEnvPath } = await prepareRunEnv(infraDir));
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new Error(`Failed to prepare infra env: ${err.message}`);
|
|
144
|
+
}
|
|
96
145
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
146
|
+
try {
|
|
147
|
+
const projectName = getInfraProjectName(devId);
|
|
148
|
+
await startDockerServices(composePath, projectName, runEnvPath, infraDir);
|
|
149
|
+
pgpassRunPath = await writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum);
|
|
150
|
+
await waitForServices(devId);
|
|
151
|
+
logger.log('All services are healthy and ready');
|
|
152
|
+
} finally {
|
|
153
|
+
cleanupRunFiles(runEnvPath, pgpassRunPath);
|
|
154
|
+
}
|
|
100
155
|
}
|
|
101
156
|
|
|
102
157
|
/**
|
|
@@ -736,13 +736,13 @@
|
|
|
736
736
|
"properties": {
|
|
737
737
|
"envOutputPath": {
|
|
738
738
|
"type": "string",
|
|
739
|
-
"description": "Path where .env file is
|
|
739
|
+
"description": "Path where .env file is written for local development (relative to config dir); single .env for run",
|
|
740
740
|
"pattern": "^[^/].*"
|
|
741
741
|
},
|
|
742
742
|
"localPort": {
|
|
743
743
|
"type": "integer",
|
|
744
|
-
"description": "Port for local development (
|
|
745
|
-
"minimum":
|
|
744
|
+
"description": "Port for local development (host and .env PORT); when set and > 0, overrides port for run and local .env. Container/deployment still use port/containerPort.",
|
|
745
|
+
"minimum": 1,
|
|
746
746
|
"maximum": 65535
|
|
747
747
|
},
|
|
748
748
|
"containerPort": {
|
|
@@ -768,6 +768,27 @@
|
|
|
768
768
|
"type": "string",
|
|
769
769
|
"description": "Dockerfile name (empty or missing = use auto-generated template)",
|
|
770
770
|
"pattern": "^[^/].*"
|
|
771
|
+
},
|
|
772
|
+
"remoteSyncPath": {
|
|
773
|
+
"type": "string",
|
|
774
|
+
"description": "Relative path under user-mutagen-folder for remote sync and Docker -v; when unset, defaults to dev/<appKey>. No leading slash.",
|
|
775
|
+
"pattern": "^[^/].*"
|
|
776
|
+
},
|
|
777
|
+
"reloadStart": {
|
|
778
|
+
"type": "string",
|
|
779
|
+
"description": "When running with --reload, override the container command with this command (run from the mounted app root /app). Examples: TypeScript pnpm run reloadStart, Python make reloadStart."
|
|
780
|
+
},
|
|
781
|
+
"scripts": {
|
|
782
|
+
"type": "object",
|
|
783
|
+
"description": "Override CLI script commands (aifabrix test, install, lint, test-e2e, test-integration). When a key is missing, language default is used (TypeScript: pnpm test/install/lint/test:e2e/test:integration, Python: make test/install/lint/test:e2e/test-integration).",
|
|
784
|
+
"properties": {
|
|
785
|
+
"test": { "type": "string", "description": "Command for aifabrix test <app>" },
|
|
786
|
+
"install": { "type": "string", "description": "Command for aifabrix install <app>" },
|
|
787
|
+
"lint": { "type": "string", "description": "Command for aifabrix lint <app>" },
|
|
788
|
+
"testE2e": { "type": "string", "description": "Command for aifabrix test-e2e <app> (YAML may use test:e2e)" },
|
|
789
|
+
"testIntegration": { "type": "string", "description": "Command for aifabrix test-integration <app> (YAML may use test:integration). Defaults to testE2e when unset." }
|
|
790
|
+
},
|
|
791
|
+
"additionalProperties": true
|
|
771
792
|
}
|
|
772
793
|
},
|
|
773
794
|
"additionalProperties": false
|