@aifabrix/builder 2.33.1 → 2.33.4
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 +13 -0
- package/lib/app/deploy-config.js +161 -0
- package/lib/app/deploy.js +28 -153
- package/lib/app/register.js +6 -5
- package/lib/app/run-helpers.js +23 -17
- package/lib/cli.js +31 -1
- package/lib/commands/logout.js +3 -4
- package/lib/commands/up-common.js +72 -0
- package/lib/commands/up-dataplane.js +109 -0
- package/lib/commands/up-miso.js +134 -0
- package/lib/core/config.js +32 -9
- package/lib/core/secrets-docker-env.js +88 -0
- package/lib/core/secrets.js +142 -115
- package/lib/infrastructure/helpers.js +82 -1
- package/lib/infrastructure/index.js +2 -0
- package/lib/schema/env-config.yaml +7 -0
- package/lib/utils/compose-generator.js +13 -13
- package/lib/utils/config-paths.js +13 -0
- package/lib/utils/device-code.js +2 -2
- package/lib/utils/env-endpoints.js +2 -5
- package/lib/utils/env-map.js +18 -14
- package/lib/utils/parse-image-ref.js +27 -0
- package/lib/utils/paths.js +28 -4
- package/lib/utils/secrets-generator.js +34 -12
- package/lib/utils/secrets-helpers.js +1 -2
- package/lib/utils/token-manager-refresh.js +5 -0
- package/package.json +1 -1
- package/templates/applications/dataplane/Dockerfile +16 -0
- package/templates/applications/dataplane/README.md +205 -0
- package/templates/applications/dataplane/env.template +143 -0
- package/templates/applications/dataplane/rbac.yaml +283 -0
- package/templates/applications/dataplane/variables.yaml +143 -0
- package/templates/applications/keycloak/Dockerfile +1 -1
- package/templates/applications/keycloak/README.md +193 -0
- package/templates/applications/keycloak/variables.yaml +5 -6
- package/templates/applications/miso-controller/Dockerfile +8 -8
- package/templates/applications/miso-controller/README.md +369 -0
- package/templates/applications/miso-controller/env.template +114 -6
- package/templates/applications/miso-controller/rbac.yaml +74 -0
- package/templates/applications/miso-controller/variables.yaml +93 -5
- package/templates/infra/compose.yaml.hbs +2 -1
- package/templates/applications/miso-controller/test.yaml +0 -1
package/lib/core/secrets.js
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const yaml = require('js-yaml');
|
|
15
14
|
const logger = require('../utils/logger');
|
|
16
15
|
const config = require('./config');
|
|
17
16
|
const {
|
|
@@ -30,6 +29,7 @@ const {
|
|
|
30
29
|
const { processEnvVariables } = require('../utils/env-copy');
|
|
31
30
|
const { buildEnvVarMap } = require('../utils/env-map');
|
|
32
31
|
const { resolveServicePortsInEnvContent } = require('../utils/secrets-url');
|
|
32
|
+
const { updatePortForDocker } = require('./secrets-docker-env');
|
|
33
33
|
const {
|
|
34
34
|
generateMissingSecrets,
|
|
35
35
|
createDefaultSecrets
|
|
@@ -44,7 +44,6 @@ const {
|
|
|
44
44
|
} = require('../utils/secrets-utils');
|
|
45
45
|
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
46
46
|
const pathsUtil = require('../utils/paths');
|
|
47
|
-
const { getContainerPortFromPath } = require('../utils/port-resolver');
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* Generates a canonical secret name from an environment variable key.
|
|
@@ -113,7 +112,7 @@ async function decryptSecretsObject(secrets) {
|
|
|
113
112
|
/**
|
|
114
113
|
* Loads secrets with cascading lookup
|
|
115
114
|
* Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
|
|
116
|
-
*
|
|
115
|
+
* When aifabrix-secrets (or secrets-path) is set in config.yaml and that file exists, it is used as base; user's file (local) is strongest and overrides project for same key. Otherwise user's file first, then aifabrix-secrets as fallback.
|
|
117
116
|
* Automatically decrypts values with secure:// prefix
|
|
118
117
|
*
|
|
119
118
|
* @async
|
|
@@ -126,9 +125,40 @@ async function decryptSecretsObject(secrets) {
|
|
|
126
125
|
* @example
|
|
127
126
|
* const secrets = await loadSecrets('../../secrets.local.yaml');
|
|
128
127
|
* // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* // When config.yaml has aifabrix-secrets: ./secrets.local.yaml, project file is base;
|
|
131
|
+
* // ~/.aifabrix/secrets.local.yaml overrides project for same key (local strongest).
|
|
132
|
+
* const secrets = await loadSecrets(undefined, 'myapp');
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Loads config secrets path, merges with user secrets (user overrides). Used by loadSecrets cascade.
|
|
137
|
+
* @async
|
|
138
|
+
* @returns {Promise<Object|null>} Merged secrets object or null
|
|
129
139
|
*/
|
|
140
|
+
async function loadMergedConfigAndUserSecrets() {
|
|
141
|
+
try {
|
|
142
|
+
const configSecretsPath = await config.getSecretsPath();
|
|
143
|
+
if (!configSecretsPath) return null;
|
|
144
|
+
const resolvedConfigPath = path.isAbsolute(configSecretsPath)
|
|
145
|
+
? configSecretsPath
|
|
146
|
+
: path.resolve(process.cwd(), configSecretsPath);
|
|
147
|
+
if (!fs.existsSync(resolvedConfigPath)) return null;
|
|
148
|
+
const configSecrets = readYamlAtPath(resolvedConfigPath);
|
|
149
|
+
if (!configSecrets || typeof configSecrets !== 'object') return null;
|
|
150
|
+
const merged = { ...configSecrets };
|
|
151
|
+
const userSecrets = loadUserSecrets();
|
|
152
|
+
for (const key of Object.keys(userSecrets)) {
|
|
153
|
+
merged[key] = userSecrets[key];
|
|
154
|
+
}
|
|
155
|
+
return merged;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
130
161
|
async function loadSecrets(secretsPath, _appName) {
|
|
131
|
-
// Explicit path branch
|
|
132
162
|
if (secretsPath) {
|
|
133
163
|
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
134
164
|
if (!fs.existsSync(resolvedPath)) {
|
|
@@ -141,9 +171,11 @@ async function loadSecrets(secretsPath, _appName) {
|
|
|
141
171
|
return await decryptSecretsObject(explicitSecrets);
|
|
142
172
|
}
|
|
143
173
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
174
|
+
let mergedSecrets = await loadMergedConfigAndUserSecrets();
|
|
175
|
+
if (!mergedSecrets || Object.keys(mergedSecrets).length === 0) {
|
|
176
|
+
mergedSecrets = loadUserSecrets();
|
|
177
|
+
mergedSecrets = await applyCanonicalSecretsOverride(mergedSecrets);
|
|
178
|
+
}
|
|
147
179
|
if (Object.keys(mergedSecrets).length === 0) {
|
|
148
180
|
mergedSecrets = loadDefaultSecrets();
|
|
149
181
|
}
|
|
@@ -174,14 +206,12 @@ async function loadSecrets(secretsPath, _appName) {
|
|
|
174
206
|
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null) {
|
|
175
207
|
const os = require('os');
|
|
176
208
|
|
|
177
|
-
// Get developer-id for local
|
|
209
|
+
// Get developer-id for port variables (local and docker: *_PUBLIC_PORT = base + devId*100)
|
|
178
210
|
let developerId = null;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// ignore, will use null (buildEnvVarMap will fetch it)
|
|
184
|
-
}
|
|
211
|
+
try {
|
|
212
|
+
developerId = await config.getDeveloperId();
|
|
213
|
+
} catch {
|
|
214
|
+
// ignore, buildEnvVarMap will use default
|
|
185
215
|
}
|
|
186
216
|
|
|
187
217
|
let envVars = await buildEnvVarMap(environment, os, developerId);
|
|
@@ -199,103 +229,6 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
199
229
|
return replaceKvInContent(resolved, secrets, envVars);
|
|
200
230
|
}
|
|
201
231
|
|
|
202
|
-
// resolveServicePortsInEnvContent, loadEnvTemplate, and processEnvVariables
|
|
203
|
-
// are imported from ./utils/secrets-helpers above.
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Generates .env file from template and secrets
|
|
207
|
-
* Creates environment file for local development
|
|
208
|
-
*
|
|
209
|
-
* @async
|
|
210
|
-
* @function generateEnvFile
|
|
211
|
-
* @param {string} appName - Name of the application
|
|
212
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
213
|
-
* @param {string} [environment='local'] - Environment context
|
|
214
|
-
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
215
|
-
* @param {boolean} [skipOutputPath=false] - Skip processing envOutputPath (to avoid recursion)
|
|
216
|
-
* @returns {Promise<string>} Path to generated .env file
|
|
217
|
-
* @throws {Error} If generation fails
|
|
218
|
-
*
|
|
219
|
-
* @example
|
|
220
|
-
* const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml', 'local', true);
|
|
221
|
-
* // Returns: './builder/myapp/.env'
|
|
222
|
-
*/
|
|
223
|
-
/**
|
|
224
|
-
* Gets base docker environment config
|
|
225
|
-
* @async
|
|
226
|
-
* @function getBaseDockerEnv
|
|
227
|
-
* @returns {Promise<Object>} Docker environment config
|
|
228
|
-
*/
|
|
229
|
-
async function getBaseDockerEnv() {
|
|
230
|
-
const { getEnvHosts } = require('../utils/env-endpoints');
|
|
231
|
-
return await getEnvHosts('docker');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Applies config.yaml override to docker environment
|
|
236
|
-
* @function applyDockerEnvOverride
|
|
237
|
-
* @param {Object} dockerEnv - Base docker environment config
|
|
238
|
-
* @returns {Object} Updated docker environment config
|
|
239
|
-
*/
|
|
240
|
-
function applyDockerEnvOverride(dockerEnv) {
|
|
241
|
-
try {
|
|
242
|
-
const os = require('os');
|
|
243
|
-
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
244
|
-
if (fs.existsSync(cfgPath)) {
|
|
245
|
-
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
246
|
-
const cfg = yaml.load(cfgContent) || {};
|
|
247
|
-
if (cfg && cfg.environments && cfg.environments.docker) {
|
|
248
|
-
return { ...dockerEnv, ...cfg.environments.docker };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
} catch {
|
|
252
|
-
// Ignore config.yaml read errors, continue with env-config values
|
|
253
|
-
}
|
|
254
|
-
return dockerEnv;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Gets container port from docker environment config
|
|
259
|
-
* @function getContainerPortFromDockerEnv
|
|
260
|
-
* @param {Object} dockerEnv - Docker environment config
|
|
261
|
-
* @returns {number} Container port (defaults to 3000)
|
|
262
|
-
*/
|
|
263
|
-
function getContainerPortFromDockerEnv(dockerEnv) {
|
|
264
|
-
if (dockerEnv.PORT === undefined || dockerEnv.PORT === null) {
|
|
265
|
-
return 3000;
|
|
266
|
-
}
|
|
267
|
-
const portVal = typeof dockerEnv.PORT === 'number' ? dockerEnv.PORT : parseInt(dockerEnv.PORT, 10);
|
|
268
|
-
return Number.isNaN(portVal) ? 3000 : portVal;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Updates PORT in resolved content for docker environment
|
|
273
|
-
* Sets PORT to container port (build.containerPort or port from variables.yaml)
|
|
274
|
-
* NOT the host port (which includes developer-id offset)
|
|
275
|
-
* @async
|
|
276
|
-
* @function updatePortForDocker
|
|
277
|
-
* @param {string} resolved - Resolved environment content
|
|
278
|
-
* @param {string} variablesPath - Path to variables.yaml file
|
|
279
|
-
* @returns {Promise<string>} Updated content with PORT set
|
|
280
|
-
*/
|
|
281
|
-
async function updatePortForDocker(resolved, variablesPath) {
|
|
282
|
-
// Step 1: Get base config from env-config.yaml
|
|
283
|
-
let dockerEnv = await getBaseDockerEnv();
|
|
284
|
-
|
|
285
|
-
// Step 2: Apply config.yaml → environments.docker override (if exists)
|
|
286
|
-
dockerEnv = applyDockerEnvOverride(dockerEnv);
|
|
287
|
-
|
|
288
|
-
// Step 3: Get PORT value for container (should be container port, NOT host port)
|
|
289
|
-
let containerPort = getContainerPortFromPath(variablesPath);
|
|
290
|
-
if (containerPort === null) {
|
|
291
|
-
containerPort = getContainerPortFromDockerEnv(dockerEnv);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// PORT in container should be the container port (no developer-id adjustment)
|
|
295
|
-
// Docker will map container port to host port via port mapping
|
|
296
|
-
return resolved.replace(/^PORT\s*=\s*.*$/m, `PORT=${containerPort}`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
232
|
/**
|
|
300
233
|
* Applies environment-specific transformations to resolved content
|
|
301
234
|
* @async
|
|
@@ -348,7 +281,7 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
348
281
|
* @throws {Error} If generation fails
|
|
349
282
|
*/
|
|
350
283
|
async function generateEnvContent(appName, secretsPath, environment = 'local', force = false) {
|
|
351
|
-
const builderPath =
|
|
284
|
+
const builderPath = pathsUtil.getBuilderPath(appName);
|
|
352
285
|
const templatePath = path.join(builderPath, 'env.template');
|
|
353
286
|
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
354
287
|
|
|
@@ -375,14 +308,108 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
|
|
|
375
308
|
return resolved;
|
|
376
309
|
}
|
|
377
310
|
|
|
378
|
-
|
|
379
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Parses .env file content into a key-to-value map.
|
|
313
|
+
* Only includes lines that look like KEY=value (first = separates key and value).
|
|
314
|
+
*
|
|
315
|
+
* @function parseEnvContentToMap
|
|
316
|
+
* @param {string} content - Raw .env file content
|
|
317
|
+
* @returns {Object.<string, string>} Map of variable name to value
|
|
318
|
+
*/
|
|
319
|
+
function parseEnvContentToMap(content) {
|
|
320
|
+
if (!content || typeof content !== 'string') {
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
const map = {};
|
|
324
|
+
const lines = content.split(/\r?\n/);
|
|
325
|
+
for (const line of lines) {
|
|
326
|
+
const trimmed = line.trim();
|
|
327
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const eq = trimmed.indexOf('=');
|
|
331
|
+
if (eq > 0) {
|
|
332
|
+
const key = trimmed.substring(0, eq).trim();
|
|
333
|
+
const value = trimmed.substring(eq + 1);
|
|
334
|
+
map[key] = value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return map;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Merges new .env content with existing .env: newly resolved content wins for keys it
|
|
342
|
+
* defines (so project secrets take effect when re-running). Keys only in existing .env
|
|
343
|
+
* are appended so manual additions are kept.
|
|
344
|
+
*
|
|
345
|
+
* @function mergeEnvContentPreservingExisting
|
|
346
|
+
* @param {string} newContent - Newly generated .env content (from template + loadSecrets)
|
|
347
|
+
* @param {Object.<string, string>} existingMap - Existing key-to-value map from current .env
|
|
348
|
+
* @returns {string} Merged content: new values for keys in newContent, plus extra existing keys
|
|
349
|
+
*/
|
|
350
|
+
function mergeEnvContentPreservingExisting(newContent, existingMap) {
|
|
351
|
+
const lines = newContent.split(/\r?\n/);
|
|
352
|
+
const newKeys = new Set();
|
|
353
|
+
const out = [];
|
|
354
|
+
for (const line of lines) {
|
|
355
|
+
const trimmed = line.trim();
|
|
356
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
357
|
+
out.push(line);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const eq = trimmed.indexOf('=');
|
|
361
|
+
if (eq > 0) {
|
|
362
|
+
const key = trimmed.substring(0, eq).trim();
|
|
363
|
+
newKeys.add(key);
|
|
364
|
+
}
|
|
365
|
+
out.push(line);
|
|
366
|
+
}
|
|
367
|
+
if (existingMap && Object.keys(existingMap).length > 0) {
|
|
368
|
+
for (const key of Object.keys(existingMap)) {
|
|
369
|
+
if (!newKeys.has(key)) {
|
|
370
|
+
out.push(`${key}=${existingMap[key]}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out.join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Generates and writes .env file. Newly resolved values (from template + secrets) win over
|
|
379
|
+
* existing .env so that project secrets (e.g. from config aifabrix-secrets) take effect on
|
|
380
|
+
* re-run. Extra variables only in existing .env are kept.
|
|
381
|
+
*
|
|
382
|
+
* @async
|
|
383
|
+
* @function generateEnvFile
|
|
384
|
+
* @param {string} appName - Name of the application
|
|
385
|
+
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
386
|
+
* @param {string} [environment='local'] - Environment context ('local' or 'docker')
|
|
387
|
+
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
388
|
+
* @param {boolean} [skipOutputPath=false] - Skip copying to envOutputPath
|
|
389
|
+
* @param {string} [preserveFromPath=null] - Path to existing .env to preserve values from (defaults to builder .env)
|
|
390
|
+
* @returns {Promise<string>} Path to generated .env file
|
|
391
|
+
* @throws {Error} If generation fails
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* const envPath = await generateEnvFile('myapp', undefined, 'docker');
|
|
395
|
+
* // When builder/myapp/.env already exists, existing values are preserved
|
|
396
|
+
*/
|
|
397
|
+
async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, skipOutputPath = false, preserveFromPath = null) {
|
|
398
|
+
const builderPath = pathsUtil.getBuilderPath(appName);
|
|
380
399
|
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
381
400
|
const envPath = path.join(builderPath, '.env');
|
|
382
401
|
|
|
383
402
|
const resolved = await generateEnvContent(appName, secretsPath, environment, force);
|
|
384
403
|
|
|
385
|
-
|
|
404
|
+
let toWrite = resolved;
|
|
405
|
+
const pathToPreserve = preserveFromPath || envPath;
|
|
406
|
+
if (pathToPreserve && fs.existsSync(pathToPreserve)) {
|
|
407
|
+
const existingContent = fs.readFileSync(pathToPreserve, 'utf8');
|
|
408
|
+
const existingMap = parseEnvContentToMap(existingContent);
|
|
409
|
+
toWrite = mergeEnvContentPreservingExisting(resolved, existingMap);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
fs.writeFileSync(envPath, toWrite, { mode: 0o600 });
|
|
386
413
|
|
|
387
414
|
// Process and copy to envOutputPath if configured (uses localPort for copied file)
|
|
388
415
|
if (!skipOutputPath) {
|
|
@@ -73,7 +73,7 @@ async function ensureAdminSecrets() {
|
|
|
73
73
|
* @param {string} postgresPassword - PostgreSQL password
|
|
74
74
|
*/
|
|
75
75
|
function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
76
|
-
const serversJsonTemplatePath = path.join(__dirname, '..', 'templates', 'infra', 'servers.json.hbs');
|
|
76
|
+
const serversJsonTemplatePath = path.join(__dirname, '..', '..', 'templates', 'infra', 'servers.json.hbs');
|
|
77
77
|
if (!fs.existsSync(serversJsonTemplatePath)) {
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
@@ -89,6 +89,86 @@ function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
|
89
89
|
fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Escape single quotes for use in PostgreSQL string literal (double each single quote)
|
|
94
|
+
* @param {string} s - Raw string
|
|
95
|
+
* @returns {string} Escaped string
|
|
96
|
+
*/
|
|
97
|
+
function escapePgString(s) {
|
|
98
|
+
if (s === null || s === undefined || typeof s !== 'string') return '';
|
|
99
|
+
return s.replace(/'/g, '\'\'');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract password from a URL string or plain password value
|
|
104
|
+
* @param {string} urlOrPassword - URL (with ://) or plain password
|
|
105
|
+
* @returns {string|null} Extracted password or null to keep default
|
|
106
|
+
*/
|
|
107
|
+
function extractPasswordFromUrlOrValue(urlOrPassword) {
|
|
108
|
+
if (typeof urlOrPassword !== 'string' || urlOrPassword.length === 0) return null;
|
|
109
|
+
if (!urlOrPassword.includes('://')) return urlOrPassword;
|
|
110
|
+
try {
|
|
111
|
+
const u = new URL(urlOrPassword.replace(/\$\{DB_HOST\}/g, 'postgres').replace(/\$\{DB_PORT\}/g, '5432'));
|
|
112
|
+
return u.password || null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Ensures Postgres init script exists for miso-controller app (database miso, user miso_user).
|
|
120
|
+
* Uses password from secrets (databases-miso-controller-0-passwordKeyVault) or default miso_pass123.
|
|
121
|
+
* Init scripts run only when the Postgres data volume is first created. If infra was already
|
|
122
|
+
* started before this script existed, run `aifabrix down -v` then `aifabrix up` to re-init, or
|
|
123
|
+
* create the database and user manually (e.g. via pgAdmin or psql).
|
|
124
|
+
*
|
|
125
|
+
* @async
|
|
126
|
+
* @param {string} infraDir - Infrastructure directory path
|
|
127
|
+
* @returns {Promise<void>}
|
|
128
|
+
*/
|
|
129
|
+
async function ensureMisoInitScript(infraDir) {
|
|
130
|
+
const initScriptsDir = path.join(infraDir, 'init-scripts');
|
|
131
|
+
if (!fs.existsSync(initScriptsDir)) {
|
|
132
|
+
fs.mkdirSync(initScriptsDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let password = 'miso_pass123';
|
|
136
|
+
try {
|
|
137
|
+
const loaded = await secrets.loadSecrets(undefined);
|
|
138
|
+
const urlOrPassword = loaded['databases-miso-controller-0-passwordKeyVault'] ||
|
|
139
|
+
loaded['databases-miso-controller-0-urlKeyVault'];
|
|
140
|
+
const extracted = extractPasswordFromUrlOrValue(urlOrPassword);
|
|
141
|
+
if (extracted !== null) password = extracted;
|
|
142
|
+
} catch {
|
|
143
|
+
// No secrets or load failed; use default
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const passwordEscaped = escapePgString(password);
|
|
147
|
+
|
|
148
|
+
const sh = `#!/bin/bash
|
|
149
|
+
set -e
|
|
150
|
+
# Miso-controller app database and user (matches DATABASE_URL in env)
|
|
151
|
+
# Runs on first Postgres volume init only
|
|
152
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<EOSQL
|
|
153
|
+
DO \\$\\$
|
|
154
|
+
BEGIN
|
|
155
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'miso_user') THEN
|
|
156
|
+
CREATE USER miso_user WITH PASSWORD '${passwordEscaped}';
|
|
157
|
+
ELSE
|
|
158
|
+
ALTER USER miso_user WITH PASSWORD '${passwordEscaped}';
|
|
159
|
+
END IF;
|
|
160
|
+
END
|
|
161
|
+
\\$\\$;
|
|
162
|
+
EOSQL
|
|
163
|
+
if ! psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -tAc "SELECT 1 FROM pg_database WHERE datname = 'miso'" | grep -q 1; then
|
|
164
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -c "CREATE DATABASE miso OWNER miso_user;"
|
|
165
|
+
fi
|
|
166
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "miso" -c "GRANT ALL ON SCHEMA public TO miso_user; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO miso_user;"
|
|
167
|
+
`;
|
|
168
|
+
const initPath = path.join(initScriptsDir, '02-miso-app.sh');
|
|
169
|
+
fs.writeFileSync(initPath, sh, { mode: 0o755 });
|
|
170
|
+
}
|
|
171
|
+
|
|
92
172
|
/**
|
|
93
173
|
* Prepare infrastructure directory and extract postgres password
|
|
94
174
|
* @param {string} devId - Developer ID
|
|
@@ -135,5 +215,6 @@ module.exports = {
|
|
|
135
215
|
ensureAdminSecrets,
|
|
136
216
|
generatePgAdminConfig,
|
|
137
217
|
prepareInfraDirectory,
|
|
218
|
+
ensureMisoInitScript,
|
|
138
219
|
registerHandlebarsHelper
|
|
139
220
|
};
|
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
checkDockerAvailability,
|
|
24
24
|
ensureAdminSecrets,
|
|
25
25
|
prepareInfraDirectory,
|
|
26
|
+
ensureMisoInitScript,
|
|
26
27
|
registerHandlebarsHelper
|
|
27
28
|
} = require('./helpers');
|
|
28
29
|
const {
|
|
@@ -59,6 +60,7 @@ async function prepareInfrastructureEnvironment(developerId) {
|
|
|
59
60
|
|
|
60
61
|
// Prepare infrastructure directory
|
|
61
62
|
const { infraDir } = prepareInfraDirectory(devId, adminSecretsPath);
|
|
63
|
+
await ensureMisoInitScript(infraDir);
|
|
62
64
|
|
|
63
65
|
return { devId, idNum, ports, templatePath, infraDir, adminSecretsPath };
|
|
64
66
|
}
|
|
@@ -18,6 +18,9 @@ environments:
|
|
|
18
18
|
MISO_PORT: 3000 # Internal port (container-to-container). MISO_PUBLIC_PORT calculated automatically.
|
|
19
19
|
KEYCLOAK_HOST: keycloak
|
|
20
20
|
KEYCLOAK_PORT: 8082 # Internal port (container-to-container). KEYCLOAK_PUBLIC_PORT calculated automatically.
|
|
21
|
+
KEYCLOAK_PUBLIC_PORT: 8082
|
|
22
|
+
DATAPLANE_HOST: dataplane
|
|
23
|
+
DATAPLANE_PORT: 3001
|
|
21
24
|
NODE_ENV: production
|
|
22
25
|
PYTHONUNBUFFERED: 1
|
|
23
26
|
PYTHONDONTWRITEBYTECODE: 1
|
|
@@ -30,8 +33,12 @@ environments:
|
|
|
30
33
|
REDIS_PORT: 6379
|
|
31
34
|
MISO_HOST: localhost
|
|
32
35
|
MISO_PORT: 3010
|
|
36
|
+
MISO_PUBLIC_PORT: 3010
|
|
33
37
|
KEYCLOAK_HOST: localhost
|
|
34
38
|
KEYCLOAK_PORT: 8082
|
|
39
|
+
KEYCLOAK_PUBLIC_PORT: 8082
|
|
40
|
+
DATAPLANE_HOST: localhost
|
|
41
|
+
DATAPLANE_PORT: 3011
|
|
35
42
|
NODE_ENV: development
|
|
36
43
|
PYTHONUNBUFFERED: 1
|
|
37
44
|
PYTHONDONTWRITEBYTECODE: 1
|
|
@@ -17,6 +17,7 @@ const config = require('../core/config');
|
|
|
17
17
|
const buildCopy = require('./build-copy');
|
|
18
18
|
const { formatMissingDbPasswordError } = require('./error-formatter');
|
|
19
19
|
const { getContainerPort } = require('./port-resolver');
|
|
20
|
+
const { parseImageOverride } = require('./parse-image-ref');
|
|
20
21
|
|
|
21
22
|
// Register commonly used helpers
|
|
22
23
|
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
@@ -129,9 +130,14 @@ function buildAppConfig(appName, config) {
|
|
|
129
130
|
* Builds image configuration section
|
|
130
131
|
* @param {Object} config - Application configuration
|
|
131
132
|
* @param {string} appName - Application name
|
|
133
|
+
* @param {string} [imageOverride] - Optional full image reference (registry/name:tag) to use instead of config
|
|
132
134
|
* @returns {Object} Image configuration
|
|
133
135
|
*/
|
|
134
|
-
function buildImageConfig(config, appName) {
|
|
136
|
+
function buildImageConfig(config, appName, imageOverride) {
|
|
137
|
+
const parsed = imageOverride ? parseImageOverride(imageOverride) : null;
|
|
138
|
+
if (parsed) {
|
|
139
|
+
return { name: parsed.name, tag: parsed.tag };
|
|
140
|
+
}
|
|
135
141
|
const imageName = getImageName(config, appName);
|
|
136
142
|
const imageTag = config.image?.tag || 'latest';
|
|
137
143
|
return {
|
|
@@ -237,14 +243,15 @@ function buildRequiresConfig(config) {
|
|
|
237
243
|
* @param {Object} config - Application configuration
|
|
238
244
|
* @param {number} port - Application port
|
|
239
245
|
* @param {string|number} devId - Developer ID
|
|
246
|
+
* @param {string} [imageOverride] - Optional full image reference for run (e.g. from --image)
|
|
240
247
|
* @returns {Object} Service configuration
|
|
241
248
|
*/
|
|
242
|
-
function buildServiceConfig(appName, config, port, devId) {
|
|
249
|
+
function buildServiceConfig(appName, config, port, devId, imageOverride) {
|
|
243
250
|
const containerPortValue = getContainerPort(config, 3000);
|
|
244
251
|
const hostPort = port;
|
|
245
252
|
return {
|
|
246
253
|
app: buildAppConfig(appName, config),
|
|
247
|
-
image: buildImageConfig(config, appName),
|
|
254
|
+
image: buildImageConfig(config, appName, imageOverride),
|
|
248
255
|
port: containerPortValue, // Container port (for health check and template)
|
|
249
256
|
containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
|
|
250
257
|
hostPort: hostPort, // Host port (options.port if provided, else config.port)
|
|
@@ -275,14 +282,7 @@ function buildNetworksConfig(config) {
|
|
|
275
282
|
return { databases: config.requires?.databases || config.databases || [] };
|
|
276
283
|
}
|
|
277
284
|
|
|
278
|
-
/**
|
|
279
|
-
* Reads and parses .env file
|
|
280
|
-
* @async
|
|
281
|
-
* @function readEnvFile
|
|
282
|
-
* @param {string} envPath - Path to .env file
|
|
283
|
-
* @returns {Promise<Object>} Object with environment variables
|
|
284
|
-
* @throws {Error} If file not found or read fails
|
|
285
|
-
*/
|
|
285
|
+
/** Reads and parses .env file. @param {string} envPath - Path to .env file. @returns {Promise<Object>} env vars. @throws {Error} If file not found. */
|
|
286
286
|
async function readEnvFile(envPath) {
|
|
287
287
|
if (!fsSync.existsSync(envPath)) {
|
|
288
288
|
throw new Error(`.env file not found: ${envPath}`);
|
|
@@ -458,9 +458,10 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
458
458
|
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
459
459
|
const template = loadDockerComposeTemplate(language);
|
|
460
460
|
const port = options.port || appConfig.port || 3000;
|
|
461
|
+
const imageOverride = options.image || options.imageOverride;
|
|
461
462
|
const { devId, idNum } = await getDeveloperIdAndNumeric();
|
|
462
463
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
|
|
463
|
-
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId);
|
|
464
|
+
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
|
|
464
465
|
const volumesConfig = buildVolumesConfig(appName);
|
|
465
466
|
const networksConfig = buildNetworksConfig(appConfig);
|
|
466
467
|
|
|
@@ -495,4 +496,3 @@ module.exports = {
|
|
|
495
496
|
buildTraefikConfig,
|
|
496
497
|
buildDevUsername
|
|
497
498
|
};
|
|
498
|
-
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @version 2.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Get path configuration value
|
|
13
15
|
* @async
|
|
@@ -102,6 +104,17 @@ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
|
|
|
102
104
|
*/
|
|
103
105
|
async setAifabrixEnvConfigPath(envConfigPath) {
|
|
104
106
|
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get builder root directory (dirname of aifabrix-env-config when set).
|
|
111
|
+
* When set, app dirs and generated .env use this instead of cwd/builder.
|
|
112
|
+
* @async
|
|
113
|
+
* @returns {Promise<string|null>} Builder root path or null to use cwd/builder
|
|
114
|
+
*/
|
|
115
|
+
async getAifabrixBuilderDir() {
|
|
116
|
+
const envConfigPath = await getPathConfig(getConfigFn, 'aifabrix-env-config');
|
|
117
|
+
return envConfigPath && typeof envConfigPath === 'string' ? path.dirname(envConfigPath) : null;
|
|
105
118
|
}
|
|
106
119
|
};
|
|
107
120
|
}
|
package/lib/utils/device-code.js
CHANGED
|
@@ -469,12 +469,13 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
|
|
472
|
+
// Send both refresh_token (OAuth2 RFC 6749 / Keycloak) and refreshToken (camelCase) so controller accepts either
|
|
472
473
|
const response = await getMakeApiCall()(url, {
|
|
473
474
|
method: 'POST',
|
|
474
475
|
headers: {
|
|
475
476
|
'Content-Type': 'application/json'
|
|
476
477
|
},
|
|
477
|
-
body: JSON.stringify({ refreshToken })
|
|
478
|
+
body: JSON.stringify({ refresh_token: refreshToken, refreshToken })
|
|
478
479
|
});
|
|
479
480
|
|
|
480
481
|
if (!response.success) {
|
|
@@ -490,7 +491,6 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
490
491
|
|
|
491
492
|
return tokenResponse;
|
|
492
493
|
}
|
|
493
|
-
|
|
494
494
|
module.exports = {
|
|
495
495
|
initiateDeviceCodeFlow,
|
|
496
496
|
pollDeviceCodeToken,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
12
|
const yaml = require('js-yaml');
|
|
14
13
|
const config = require('../core/config');
|
|
15
14
|
const devConfig = require('../utils/dev-config');
|
|
@@ -49,8 +48,7 @@ function splitHost(value) {
|
|
|
49
48
|
function getLocalhostOverride(context) {
|
|
50
49
|
if (context !== 'local') return null;
|
|
51
50
|
try {
|
|
52
|
-
const
|
|
53
|
-
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
51
|
+
const cfgPath = config.CONFIG_FILE;
|
|
54
52
|
if (fs.existsSync(cfgPath)) {
|
|
55
53
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
56
54
|
const cfg = yaml.load(cfgContent) || {};
|
|
@@ -310,8 +308,7 @@ async function rewriteInfraEndpoints(envContent, context, devPorts, adjustedHost
|
|
|
310
308
|
|
|
311
309
|
// Apply config.yaml → environments.{context} override (if exists)
|
|
312
310
|
try {
|
|
313
|
-
const
|
|
314
|
-
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
311
|
+
const cfgPath = config.CONFIG_FILE;
|
|
315
312
|
if (fs.existsSync(cfgPath)) {
|
|
316
313
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
317
314
|
const cfg = yaml.load(cfgContent) || {};
|