@aifabrix/builder 2.47.1 → 2.47.3
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/jest.isolated-projects.js +1 -0
- package/jest.projects.js +4 -0
- package/lib/app/rotate-secret.js +19 -12
- package/lib/app/run-resolve-image.js +1 -2
- package/lib/build/docker-build-args.js +83 -0
- package/lib/build/index.js +1 -29
- package/lib/cli/setup-app.js +1 -1
- package/lib/commands/teardown.js +36 -6
- package/lib/commands/up-dataplane-credentials.js +104 -0
- package/lib/commands/up-dataplane.js +5 -3
- package/lib/utils/build-resolve-image.js +14 -4
- package/lib/utils/config-paths.js +6 -3
- package/package.json +1 -1
- package/.npmrc.token +0 -1
|
@@ -120,6 +120,7 @@ const ISOLATED_SUITE_ENTRIES = [
|
|
|
120
120
|
['paths-jest-sandbox-integration', ['**/tests/lib/utils/paths-jest-sandbox-integration.test.js']],
|
|
121
121
|
['secrets-remove-all-guard', ['**/tests/lib/commands/secrets-remove-all-guard.test.js']],
|
|
122
122
|
['app-logs', ['**/tests/lib/commands/app-logs.test.js']],
|
|
123
|
+
['app-down', ['**/tests/lib/app/app-down.test.js']],
|
|
123
124
|
[
|
|
124
125
|
'channel-real-fs',
|
|
125
126
|
[
|
package/jest.projects.js
CHANGED
|
@@ -351,6 +351,10 @@ const defaultProject = {
|
|
|
351
351
|
'\\\\tests\\\\lib\\\\commands\\\\app-logs.test.js',
|
|
352
352
|
'lib/commands/app-logs.test.js',
|
|
353
353
|
'app-logs\\.test\\.js',
|
|
354
|
+
'/tests/lib/app/app-down.test.js',
|
|
355
|
+
'\\\\tests\\\\lib\\\\app\\\\app-down.test.js',
|
|
356
|
+
'lib/app/app-down.test.js',
|
|
357
|
+
'app-down\\.test\\.js',
|
|
354
358
|
'/tests/lib/channels/channel-integration-generator.test.js',
|
|
355
359
|
'\\\\tests\\\\lib\\\\channels\\\\channel-integration-generator.test.js',
|
|
356
360
|
'lib/channels/channel-integration-generator.test.js',
|
package/lib/app/rotate-secret.js
CHANGED
|
@@ -166,7 +166,10 @@ function validateResponse(response) {
|
|
|
166
166
|
* @param {string} apiUrl - API URL
|
|
167
167
|
* @param {string} [message] - Optional message from API
|
|
168
168
|
*/
|
|
169
|
-
function displayRotationResults(appKey, environment, credentials, apiUrl, message) {
|
|
169
|
+
function displayRotationResults(appKey, environment, credentials, apiUrl, message, options = {}) {
|
|
170
|
+
if (options.quiet) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
170
173
|
logger.log(formatSuccessLine('Secret rotated successfully!\n'));
|
|
171
174
|
logger.log(chalk.bold('📋 Application Details:'));
|
|
172
175
|
logger.log(` Key: ${appKey}`);
|
|
@@ -273,7 +276,7 @@ async function getRotationAuthToken(controllerUrl, config) {
|
|
|
273
276
|
* @param {string} actualControllerUrl - Controller URL
|
|
274
277
|
* @throws {Error} If saving fails
|
|
275
278
|
*/
|
|
276
|
-
async function saveCredentialsLocally(appKey, credentials, actualControllerUrl) {
|
|
279
|
+
async function saveCredentialsLocally(appKey, credentials, actualControllerUrl, options = {}) {
|
|
277
280
|
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
278
281
|
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
279
282
|
|
|
@@ -293,10 +296,12 @@ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl)
|
|
|
293
296
|
logger.warn(chalk.yellow(`⚠ Could not validate env resolution: ${error.message}`));
|
|
294
297
|
}
|
|
295
298
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
if (!options.quiet) {
|
|
300
|
+
logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
301
|
+
logger.log(formatSuccessLine('env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL'));
|
|
302
|
+
logger.log(formatSuccessLine('Run "aifabrix resolve ' + appKey + '" to materialize an on-disk .env\n'));
|
|
303
|
+
}
|
|
304
|
+
} else if (!options.quiet) {
|
|
300
305
|
logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
|
|
301
306
|
}
|
|
302
307
|
} catch (error) {
|
|
@@ -312,7 +317,7 @@ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl)
|
|
|
312
317
|
* @param {string} environment - Environment ID or key
|
|
313
318
|
* @param {string} token - Bearer token
|
|
314
319
|
*/
|
|
315
|
-
async function executeRotation(appKey, actualControllerUrl, environment, token) {
|
|
320
|
+
async function executeRotation(appKey, actualControllerUrl, environment, token, options = {}) {
|
|
316
321
|
const authConfig = { type: 'bearer', token };
|
|
317
322
|
const response = await rotateApplicationSecret(actualControllerUrl, environment, appKey, authConfig);
|
|
318
323
|
|
|
@@ -324,8 +329,8 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
|
|
|
324
329
|
}
|
|
325
330
|
|
|
326
331
|
const { credentials, message } = validateResponse(response);
|
|
327
|
-
await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
|
|
328
|
-
displayRotationResults(appKey, environment, credentials, actualControllerUrl, message);
|
|
332
|
+
await saveCredentialsLocally(appKey, credentials, actualControllerUrl, options);
|
|
333
|
+
displayRotationResults(appKey, environment, credentials, actualControllerUrl, message, options);
|
|
329
334
|
// Clear cached client token so next deploy uses new credentials (avoids "Invalid or expired credentials" when running deploy/up-dataplane immediately after rotate)
|
|
330
335
|
await clearClientToken(environment, appKey);
|
|
331
336
|
}
|
|
@@ -338,8 +343,10 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
|
|
|
338
343
|
* @param {Object} [_options] - Command options (reserved)
|
|
339
344
|
* @throws {Error} If rotation fails
|
|
340
345
|
*/
|
|
341
|
-
async function rotateSecret(appKey,
|
|
342
|
-
|
|
346
|
+
async function rotateSecret(appKey, options = {}) {
|
|
347
|
+
if (!options.quiet) {
|
|
348
|
+
logger.log(chalk.yellow('⚠ This will invalidate the old ClientSecret!\n'));
|
|
349
|
+
}
|
|
343
350
|
|
|
344
351
|
const { ensureSecretsEncryptionKey } = require('../core/config');
|
|
345
352
|
await ensureSecretsEncryptionKey();
|
|
@@ -349,7 +356,7 @@ async function rotateSecret(appKey, _options = {}) {
|
|
|
349
356
|
const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
|
|
350
357
|
|
|
351
358
|
try {
|
|
352
|
-
await executeRotation(appKey, actualControllerUrl, environment, token);
|
|
359
|
+
await executeRotation(appKey, actualControllerUrl, environment, token, options);
|
|
353
360
|
} catch (error) {
|
|
354
361
|
logger.error(formatBlockingError(`Failed to rotate secret via controller: ${actualControllerUrl}`));
|
|
355
362
|
logger.error(chalk.gray(`Error: ${error.message}`));
|
|
@@ -10,7 +10,6 @@ const config = require('../core/config');
|
|
|
10
10
|
const { resolveDockerImageRef } = require('../utils/resolve-docker-image-ref');
|
|
11
11
|
const { checkImageExists } = require('../utils/app-run-containers');
|
|
12
12
|
const { buildDevImageRepositoryPath } = require('../utils/image-name');
|
|
13
|
-
const { wantsLocalDockerImageDiscovery } = require('../utils/build-resolve-image');
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* @param {string} appName
|
|
@@ -42,7 +41,7 @@ async function resolveRunImageWithLocalFallback(appName, appConfig, runOptions =
|
|
|
42
41
|
|
|
43
42
|
const baseRef = resolveDockerImageRef(appName, appConfig, opts);
|
|
44
43
|
|
|
45
|
-
if (opts.base === true
|
|
44
|
+
if (opts.base === true) {
|
|
46
45
|
return baseRef;
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker build-arg resolution from resolved env and shell overlays.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Build-time args for `aifabrix build` / docker build
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const secretsEnvWrite = require('../core/secrets-env-write');
|
|
13
|
+
const { assertMisoControllerNpmSdkDeps } = require('./miso-controller-npm-sdk-guard');
|
|
14
|
+
|
|
15
|
+
const MISO_CONTROLLER_UI_BUILD_ARG_KEYS = [
|
|
16
|
+
'MISO_RELATIVE_PATH',
|
|
17
|
+
'MISO_WEB_SERVER_URL',
|
|
18
|
+
'VITE_BASE_PATH',
|
|
19
|
+
'KEYCLOAK_SERVER_URL',
|
|
20
|
+
'KEYCLOAK_REALM',
|
|
21
|
+
'KEYCLOAK_CLIENT_ID'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object.<string, string>} buildArgs
|
|
26
|
+
* @param {Object.<string, string>} envMap
|
|
27
|
+
*/
|
|
28
|
+
function applyMisoControllerUiBuildArgs(buildArgs, envMap) {
|
|
29
|
+
for (const key of MISO_CONTROLLER_UI_BUILD_ARG_KEYS) {
|
|
30
|
+
const value = envMap[key];
|
|
31
|
+
if (typeof value === 'string' && value.trim() && !value.includes('://')) {
|
|
32
|
+
buildArgs[key] = value.trim();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!buildArgs.VITE_BASE_PATH && buildArgs.MISO_RELATIVE_PATH) {
|
|
36
|
+
buildArgs.VITE_BASE_PATH = buildArgs.MISO_RELATIVE_PATH;
|
|
37
|
+
}
|
|
38
|
+
if (!buildArgs.VITE_BASE_PATH && buildArgs.MISO_WEB_SERVER_URL) {
|
|
39
|
+
try {
|
|
40
|
+
const pathname = new URL(buildArgs.MISO_WEB_SERVER_URL).pathname.replace(/\/+$/, '');
|
|
41
|
+
if (pathname && pathname !== '/') {
|
|
42
|
+
buildArgs.VITE_BASE_PATH = pathname;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore invalid public URL
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve Docker build-args from resolved env map and merged BASH_* secrets.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} appName
|
|
54
|
+
* @param {string} [contextPath] Docker build context (repo root for miso-controller)
|
|
55
|
+
* @returns {Promise<Object.<string, string>>}
|
|
56
|
+
*/
|
|
57
|
+
async function resolveDockerBuildArgsForApp(appName, contextPath) {
|
|
58
|
+
const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker' });
|
|
59
|
+
const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
|
|
60
|
+
const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
|
|
61
|
+
const buildArgs = {};
|
|
62
|
+
if (envMap.NPM_TOKEN) buildArgs.NPM_TOKEN = envMap.NPM_TOKEN;
|
|
63
|
+
if (envMap.PYPI_TOKEN) buildArgs.PYPI_TOKEN = envMap.PYPI_TOKEN;
|
|
64
|
+
for (const [k, v] of Object.entries(bashOverlay)) {
|
|
65
|
+
if (v && buildArgs[k] === undefined) buildArgs[k] = v;
|
|
66
|
+
}
|
|
67
|
+
const shellNpm = typeof process.env.NPM_TOKEN === 'string' ? process.env.NPM_TOKEN.trim() : '';
|
|
68
|
+
const shellPypi = typeof process.env.PYPI_TOKEN === 'string' ? process.env.PYPI_TOKEN.trim() : '';
|
|
69
|
+
if (shellNpm) buildArgs.NPM_TOKEN = shellNpm;
|
|
70
|
+
if (shellPypi) buildArgs.PYPI_TOKEN = shellPypi;
|
|
71
|
+
if (appName === 'miso-controller') {
|
|
72
|
+
applyMisoControllerUiBuildArgs(buildArgs, envMap);
|
|
73
|
+
}
|
|
74
|
+
if (appName === 'miso-controller' && contextPath) {
|
|
75
|
+
assertMisoControllerNpmSdkDeps(path.resolve(contextPath));
|
|
76
|
+
}
|
|
77
|
+
return buildArgs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
resolveDockerBuildArgsForApp,
|
|
82
|
+
applyMisoControllerUiBuildArgs
|
|
83
|
+
};
|
package/lib/build/index.js
CHANGED
|
@@ -35,9 +35,8 @@ const dockerBuild = require('../utils/docker-build');
|
|
|
35
35
|
const buildCopy = require('../utils/build-copy');
|
|
36
36
|
const { resolveBuildDockerTagNames } = require('../utils/build-resolve-image');
|
|
37
37
|
const buildHelpers = require('../utils/build-helpers');
|
|
38
|
-
const secretsEnvWrite = require('../core/secrets-env-write');
|
|
39
38
|
const { ensureRunSecretsForApp } = require('../app/run-env-compose');
|
|
40
|
-
const {
|
|
39
|
+
const { resolveDockerBuildArgsForApp } = require('./docker-build-args');
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
42
|
* Loads application config for an application
|
|
@@ -394,33 +393,6 @@ async function handleDockerfileGeneration(appName, params, options, buildHelpers
|
|
|
394
393
|
}, generateDockerfile);
|
|
395
394
|
}
|
|
396
395
|
|
|
397
|
-
/**
|
|
398
|
-
* Resolve Docker build-args from resolved env map and merged BASH_* secrets.
|
|
399
|
-
* @param {string} appName
|
|
400
|
-
* @param {string} [contextPath] Docker build context (repo root for miso-controller)
|
|
401
|
-
* @returns {Promise<Object.<string, string>>}
|
|
402
|
-
*/
|
|
403
|
-
async function resolveDockerBuildArgsForApp(appName, contextPath) {
|
|
404
|
-
const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker' });
|
|
405
|
-
const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
|
|
406
|
-
const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
|
|
407
|
-
const buildArgs = {};
|
|
408
|
-
if (envMap.NPM_TOKEN) buildArgs.NPM_TOKEN = envMap.NPM_TOKEN;
|
|
409
|
-
if (envMap.PYPI_TOKEN) buildArgs.PYPI_TOKEN = envMap.PYPI_TOKEN;
|
|
410
|
-
for (const [k, v] of Object.entries(bashOverlay)) {
|
|
411
|
-
if (v && buildArgs[k] === undefined) buildArgs[k] = v;
|
|
412
|
-
}
|
|
413
|
-
// Exported shell tokens win (local dev: export NPM_TOKEN=... before af build / docker build).
|
|
414
|
-
const shellNpm = typeof process.env.NPM_TOKEN === 'string' ? process.env.NPM_TOKEN.trim() : '';
|
|
415
|
-
const shellPypi = typeof process.env.PYPI_TOKEN === 'string' ? process.env.PYPI_TOKEN.trim() : '';
|
|
416
|
-
if (shellNpm) buildArgs.NPM_TOKEN = shellNpm;
|
|
417
|
-
if (shellPypi) buildArgs.PYPI_TOKEN = shellPypi;
|
|
418
|
-
if (appName === 'miso-controller' && contextPath) {
|
|
419
|
-
assertMisoControllerNpmSdkDeps(path.resolve(contextPath));
|
|
420
|
-
}
|
|
421
|
-
return buildArgs;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
396
|
/**
|
|
425
397
|
* Standard Docker build path after CLI header is printed (non-external apps).
|
|
426
398
|
* @param {string} appName
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -324,7 +324,7 @@ function setupBuildRunLogsDownCommands(program) {
|
|
|
324
324
|
.option('-t, --tag <tag>', 'Image tag (default: latest). Set image.tag in application.yaml to match for deploy.')
|
|
325
325
|
.option(
|
|
326
326
|
'--base',
|
|
327
|
-
'
|
|
327
|
+
'Build/run the manifest base image (no -devN suffix). Default local build uses a developer-scoped repository when developer-id > 0.'
|
|
328
328
|
)
|
|
329
329
|
.action(async(appName, options) => {
|
|
330
330
|
try {
|
package/lib/commands/teardown.js
CHANGED
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
formatProgress,
|
|
36
36
|
successGlyph
|
|
37
37
|
} = require('../utils/cli-test-layout-chalk');
|
|
38
|
+
const { applySecretsUrlFromRemote } = require('../utils/config-paths');
|
|
38
39
|
|
|
39
40
|
/** File name kept by teardown (lowercase exact match). */
|
|
40
41
|
const PRESERVE_FILE = 'config.yaml';
|
|
@@ -149,6 +150,26 @@ function cleanMaterializedBuilderTrees() {
|
|
|
149
150
|
return { removed, failed };
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Fix preserved `config.yaml` after teardown: remote dev shared secrets API URL and cleared device tokens.
|
|
155
|
+
*
|
|
156
|
+
* @async
|
|
157
|
+
* @returns {Promise<boolean>} True when config was saved
|
|
158
|
+
*/
|
|
159
|
+
async function sanitizePreservedConfigAfterTeardown() {
|
|
160
|
+
const cfg = await config.getConfig();
|
|
161
|
+
const beforeSecrets = cfg['aifabrix-secrets'];
|
|
162
|
+
applySecretsUrlFromRemote(cfg);
|
|
163
|
+
let changed = cfg['aifabrix-secrets'] !== beforeSecrets;
|
|
164
|
+
if (cfg.device && typeof cfg.device === 'object' && Object.keys(cfg.device).length > 0) {
|
|
165
|
+
cfg.device = {};
|
|
166
|
+
changed = true;
|
|
167
|
+
}
|
|
168
|
+
if (!changed) return false;
|
|
169
|
+
await config.saveConfig(cfg);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
/**
|
|
153
174
|
* Stop infra with `down-infra -v` semantics. Tolerates already-down state.
|
|
154
175
|
* @async
|
|
@@ -291,14 +312,13 @@ async function handleTeardown(options = {}) {
|
|
|
291
312
|
await stopInfraQuietly();
|
|
292
313
|
const { removed, failed } = cleanFilesWithSpinner();
|
|
293
314
|
const builderClean = cleanMaterializedBuilderTrees();
|
|
294
|
-
|
|
295
|
-
removed.push(p);
|
|
296
|
-
}
|
|
297
|
-
for (const p of builderClean.failed) {
|
|
298
|
-
failed.push(p);
|
|
299
|
-
}
|
|
315
|
+
mergeBuilderCleanResults(removed, failed, builderClean);
|
|
300
316
|
const devStr = await buildDeveloperLabel();
|
|
317
|
+
const configFixed = await sanitizePreservedConfigAfterTeardown();
|
|
301
318
|
logTeardownFooter({ devStr, removedCount: removed.length, failedCount: failed.length });
|
|
319
|
+
if (configFixed) {
|
|
320
|
+
logger.log(formatSuccessLine('Restored remote shared secrets URL in config.yaml'));
|
|
321
|
+
}
|
|
302
322
|
|
|
303
323
|
logger.log(formatSuccessParagraph('aifabrix teardown complete.'));
|
|
304
324
|
} finally {
|
|
@@ -306,10 +326,20 @@ async function handleTeardown(options = {}) {
|
|
|
306
326
|
}
|
|
307
327
|
}
|
|
308
328
|
|
|
329
|
+
function mergeBuilderCleanResults(removed, failed, builderClean) {
|
|
330
|
+
for (const p of builderClean.removed) {
|
|
331
|
+
removed.push(p);
|
|
332
|
+
}
|
|
333
|
+
for (const p of builderClean.failed) {
|
|
334
|
+
failed.push(p);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
309
338
|
module.exports = {
|
|
310
339
|
handleTeardown,
|
|
311
340
|
cleanAifabrixSystemDir,
|
|
312
341
|
cleanMaterializedBuilderTrees,
|
|
342
|
+
sanitizePreservedConfigAfterTeardown,
|
|
313
343
|
stopInfraQuietly,
|
|
314
344
|
PRESERVE_FILE,
|
|
315
345
|
PRESERVE_CERTS_DIR
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-deploy dataplane pipeline credential alignment.
|
|
3
|
+
*
|
|
4
|
+
* Controller deploy/onboarding can reset the stored ClientSecret without updating
|
|
5
|
+
* ~/.aifabrix/secrets.local.yaml. Probe the token endpoint after deploy and rotate
|
|
6
|
+
* when local credentials no longer match the controller.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview up-dataplane credential probe and sync
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const { makeApiCall } = require('../utils/api');
|
|
15
|
+
const { loadClientCredentials } = require('../utils/token-manager');
|
|
16
|
+
const { rotateSecret } = require('../app/rotate-secret');
|
|
17
|
+
const logger = require('../utils/logger');
|
|
18
|
+
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
19
|
+
|
|
20
|
+
const DATAPLANE_APP_KEY = 'dataplane';
|
|
21
|
+
const MISALIGN_ERROR =
|
|
22
|
+
'Dataplane pipeline credentials are not aligned with the controller. ' +
|
|
23
|
+
'Run: aifabrix app rotate-secret dataplane && aifabrix run dataplane';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} controllerUrl
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function normalizeControllerUrl(controllerUrl) {
|
|
30
|
+
return String(controllerUrl || '').replace(/\/+$/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Exchange dataplane client id/secret for a controller token (probe only).
|
|
35
|
+
* @async
|
|
36
|
+
* @param {string} controllerUrl
|
|
37
|
+
* @param {{ clientId: string, clientSecret: string }} credentials
|
|
38
|
+
* @returns {Promise<boolean>}
|
|
39
|
+
*/
|
|
40
|
+
async function probeDataplaneClientToken(controllerUrl, credentials) {
|
|
41
|
+
if (!credentials?.clientId || !credentials?.clientSecret) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const response = await makeApiCall(`${normalizeControllerUrl(controllerUrl)}/api/v1/auth/token`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'x-client-id': credentials.clientId,
|
|
49
|
+
'x-client-secret': credentials.clientSecret
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return response.success === true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @async
|
|
57
|
+
* @param {string} controllerUrl
|
|
58
|
+
* @returns {Promise<boolean>}
|
|
59
|
+
*/
|
|
60
|
+
async function credentialsAcceptToken(controllerUrl) {
|
|
61
|
+
const credentials = await loadClientCredentials(DATAPLANE_APP_KEY);
|
|
62
|
+
if (!credentials) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return probeDataplaneClientToken(controllerUrl, credentials);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* After deploy, verify dataplane pipeline credentials; rotate when misaligned.
|
|
70
|
+
* @async
|
|
71
|
+
* @param {Object} options - up-dataplane / platform install options
|
|
72
|
+
* @param {string} controllerUrl
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
* @throws {Error} When credentials remain misaligned after rotate
|
|
75
|
+
*/
|
|
76
|
+
async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
|
|
77
|
+
const quiet = options.platformInstall === true || options.quiet === true;
|
|
78
|
+
const aligned = await credentialsAcceptToken(controllerUrl);
|
|
79
|
+
if (aligned) {
|
|
80
|
+
if (!quiet) {
|
|
81
|
+
logger.log(formatSuccessLine('Dataplane pipeline credentials verified.'));
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!quiet) {
|
|
87
|
+
logger.log(chalk.blue('Aligning dataplane pipeline credentials with controller...'));
|
|
88
|
+
}
|
|
89
|
+
await rotateSecret(DATAPLANE_APP_KEY, { ...options, quiet });
|
|
90
|
+
|
|
91
|
+
const stillMisaligned = !(await credentialsAcceptToken(controllerUrl));
|
|
92
|
+
if (stillMisaligned) {
|
|
93
|
+
throw new Error(MISALIGN_ERROR);
|
|
94
|
+
}
|
|
95
|
+
if (!quiet) {
|
|
96
|
+
logger.log(formatSuccessLine('Dataplane pipeline credentials aligned.'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
alignDataplaneCredentialsAfterDeploy,
|
|
102
|
+
probeDataplaneClientToken,
|
|
103
|
+
credentialsAcceptToken
|
|
104
|
+
};
|
|
@@ -3,9 +3,8 @@ const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test
|
|
|
3
3
|
* AI Fabrix Builder - Up Dataplane Command
|
|
4
4
|
*
|
|
5
5
|
* Always local deployment: requires dev infra (same gate as up-miso), then registers or
|
|
6
|
-
* rotates dataplane in dev, sends deployment manifest to Miso Controller,
|
|
7
|
-
*
|
|
8
|
-
* rotate-secret; otherwise registers.
|
|
6
|
+
* rotates dataplane in dev, sends deployment manifest to Miso Controller, aligns pipeline
|
|
7
|
+
* credentials when deploy changed controller secrets, then runs dataplane locally.
|
|
9
8
|
*
|
|
10
9
|
* @fileoverview up-dataplane command implementation
|
|
11
10
|
* @author AI Fabrix Team
|
|
@@ -35,6 +34,7 @@ const {
|
|
|
35
34
|
validateEnvOutputPathFolderOrNull,
|
|
36
35
|
refreshUrlsLocalRegistryForCurrentProject
|
|
37
36
|
} = require('./up-common');
|
|
37
|
+
const { alignDataplaneCredentialsAfterDeploy } = require('./up-dataplane-credentials');
|
|
38
38
|
|
|
39
39
|
const CONTROLLER_HEALTH_PATH = '/health';
|
|
40
40
|
|
|
@@ -262,6 +262,8 @@ async function handleUpDataplane(options = {}) {
|
|
|
262
262
|
await deployDataplaneToController({ ...options, envKey: environmentKey });
|
|
263
263
|
logger.log('');
|
|
264
264
|
|
|
265
|
+
await alignDataplaneCredentialsAfterDeploy(options, controllerUrl);
|
|
266
|
+
|
|
265
267
|
const { runEnvKey, portOverride } = await computeDataplaneRunOverrides(options);
|
|
266
268
|
|
|
267
269
|
await app.runApp('dataplane', {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
const chalk = require('chalk');
|
|
12
12
|
const logger = require('./logger');
|
|
13
13
|
const { execWithDockerEnv } = require('./docker-exec');
|
|
14
|
-
const { buildDevImageName } = require('./image-name');
|
|
14
|
+
const { buildDevImageName, buildDevImageRepositoryPath } = require('./image-name');
|
|
15
15
|
const {
|
|
16
16
|
getRepositoryPathFromConfig,
|
|
17
17
|
resolveDockerImageRef
|
|
@@ -166,8 +166,8 @@ async function discoverLocalBaseImageRepository(appName, developerId) {
|
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
168
|
* Resolve local `docker build -t` repository path(s) from the manifest or discovery.
|
|
169
|
-
* When `application.yaml` declares `image`,
|
|
170
|
-
*
|
|
169
|
+
* When `application.yaml` declares `image`, local builds still use a `-dev<id>` repository
|
|
170
|
+
* suffix for developer id > 0 unless `--base` is set (manifest ref is used exactly for base builds).
|
|
171
171
|
*
|
|
172
172
|
* @param {string} appName
|
|
173
173
|
* @param {Object} variables - application manifest
|
|
@@ -176,9 +176,19 @@ async function discoverLocalBaseImageRepository(appName, developerId) {
|
|
|
176
176
|
* @returns {Promise<{ buildImageName: string, compatBaseName: string|null }>}
|
|
177
177
|
*/
|
|
178
178
|
async function resolveBuildDockerTagNames(appName, variables, developerId, options = {}) {
|
|
179
|
+
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : Number(developerId);
|
|
180
|
+
const scopeForDeveloper =
|
|
181
|
+
Number.isFinite(idNum) && idNum > 0 && options.base !== true;
|
|
182
|
+
|
|
179
183
|
if (!wantsLocalDockerImageDiscovery(variables)) {
|
|
180
184
|
const { imageName } = resolveDockerImageRef(appName, variables, options);
|
|
181
|
-
|
|
185
|
+
if (!scopeForDeveloper) {
|
|
186
|
+
return { buildImageName: imageName, compatBaseName: null };
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
buildImageName: buildDevImageRepositoryPath(imageName, developerId),
|
|
190
|
+
compatBaseName: null
|
|
191
|
+
};
|
|
182
192
|
}
|
|
183
193
|
const baseRepo = await resolveBuildImageRepositoryName(appName, variables, developerId);
|
|
184
194
|
const buildImageName = buildDevImageName(baseRepo, developerId);
|
|
@@ -356,10 +356,11 @@ function hostnameFromUrl(url) {
|
|
|
356
356
|
|
|
357
357
|
function applySecretsUrlFromRemote(config) {
|
|
358
358
|
const remoteServer = config['remote-server'];
|
|
359
|
-
|
|
360
|
-
if (!remoteServer || !secretsPath || isHttpUrl(secretsPath)) return;
|
|
359
|
+
if (!remoteServer) return;
|
|
361
360
|
const base = typeof remoteServer === 'string' ? remoteServer.trim().replace(/\/+$/, '') : '';
|
|
362
361
|
if (!base) return;
|
|
362
|
+
const secretsPath = config['aifabrix-secrets'];
|
|
363
|
+
if (secretsPath && isHttpUrl(secretsPath)) return;
|
|
363
364
|
config['aifabrix-secrets'] = `${base}/api/dev/secrets`;
|
|
364
365
|
}
|
|
365
366
|
|
|
@@ -421,6 +422,8 @@ module.exports = {
|
|
|
421
422
|
setPathConfig,
|
|
422
423
|
createPathConfigFunctions,
|
|
423
424
|
resolveEnvConfigPathToAbsolute,
|
|
424
|
-
SETTINGS_RESPONSE_KEYS
|
|
425
|
+
SETTINGS_RESPONSE_KEYS,
|
|
426
|
+
applySecretsUrlFromRemote,
|
|
427
|
+
applySyncAndDockerFromHost
|
|
425
428
|
};
|
|
426
429
|
|
package/package.json
CHANGED
package/.npmrc.token
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
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
|