@aifabrix/builder 2.62.0 → 2.63.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/lib/api/onboarding-status.api.js +117 -0
- package/lib/app/register.js +24 -2
- package/lib/app/run-env-compose.js +39 -2
- package/lib/app/run-env-recovery.js +1 -0
- package/lib/app/run-helpers.js +1 -1
- package/lib/cli/infra-guided.js +30 -2
- package/lib/cli/setup-platform.js +18 -3
- package/lib/commands/setup-modes.js +2 -1
- package/lib/commands/setup-onboarding-readiness.js +83 -0
- package/lib/commands/setup.js +32 -10
- package/lib/commands/up-dataplane-bootstrap.js +115 -38
- package/lib/commands/up-dataplane.js +4 -31
- package/lib/core/local-env-overrides.js +23 -2
- package/lib/deployment/installation/index.js +4 -1
- package/lib/deployment/installation/infra-catalog.js +0 -2
- package/lib/deployment/installation/local-environment-authorize.js +221 -0
- package/lib/deployment/installation/local-environment-snapshot.js +124 -0
- package/lib/deployment/installation/local-environment-stage.js +43 -66
- package/lib/generator/index.js +32 -9
- package/lib/parameters/rsa-secret-values.js +14 -31
- package/lib/schema/infra.parameter.yaml +2 -45
- package/lib/schema/infrastructure-schema.json +0 -58
- package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
- package/lib/utils/log-redaction.js +4 -1
- package/package.json +2 -1
- package/templates/README.md +6 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/mirrored-templates.json +2 -2
- package/templates/applications/miso-controller/application.yaml +18 -1
- package/templates/applications/miso-controller/env.template +3 -21
- package/templates/applications/miso-controller/rbac.yaml +10 -6
- package/templates/marketplace/main.json +5 -69
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the public Miso required-baseline onboarding projection.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Public Miso onboarding readiness client
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { ApiClient } = require('./index');
|
|
12
|
+
|
|
13
|
+
const ENDPOINT = '/api/v1/health/onboarding-status';
|
|
14
|
+
const PROJECTION_KEYS = ['code', 'retryable', 'state'];
|
|
15
|
+
const VALID_PROJECTIONS = new Set([
|
|
16
|
+
'pending|null|true',
|
|
17
|
+
'complete|null|false',
|
|
18
|
+
'incomplete|baseline-dimensions-incomplete|true',
|
|
19
|
+
'incomplete|baseline-roles-incomplete|true'
|
|
20
|
+
]);
|
|
21
|
+
const VALID_PAIRS = new Set([
|
|
22
|
+
'pending|null|true::pending|null|true',
|
|
23
|
+
'incomplete|baseline-dimensions-incomplete|true::pending|null|true',
|
|
24
|
+
'complete|null|false::pending|null|true',
|
|
25
|
+
'complete|null|false::incomplete|baseline-roles-incomplete|true',
|
|
26
|
+
'complete|null|false::complete|null|false'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function onboardingStatusError(message, code = 'MISO_ONBOARDING_STATUS_INVALID') {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
error.code = code;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function projectionSignature(projection) {
|
|
36
|
+
return `${projection.state}|${projection.code === null ? 'null' : projection.code}|${projection.retryable}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasValidProjectionTypes(projection) {
|
|
40
|
+
return typeof projection.state === 'string' &&
|
|
41
|
+
(projection.code === null || typeof projection.code === 'string') &&
|
|
42
|
+
typeof projection.retryable === 'boolean';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseProjection(value, name) {
|
|
46
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
47
|
+
throw onboardingStatusError(`Miso ${name} readiness projection is missing or malformed.`);
|
|
48
|
+
}
|
|
49
|
+
const keys = Object.keys(value).sort();
|
|
50
|
+
if (keys.length !== PROJECTION_KEYS.length || keys.some((key, index) => key !== PROJECTION_KEYS[index])) {
|
|
51
|
+
throw onboardingStatusError(`Miso ${name} readiness projection has unsupported fields.`);
|
|
52
|
+
}
|
|
53
|
+
const projection = { state: value.state, code: value.code, retryable: value.retryable };
|
|
54
|
+
if (!hasValidProjectionTypes(projection)) {
|
|
55
|
+
throw onboardingStatusError(`Miso ${name} readiness projection has invalid field types.`);
|
|
56
|
+
}
|
|
57
|
+
if (!VALID_PROJECTIONS.has(projectionSignature(projection))) {
|
|
58
|
+
throw onboardingStatusError(`Miso ${name} readiness projection has an unsupported state.`);
|
|
59
|
+
}
|
|
60
|
+
return Object.freeze(projection);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseOnboardingStatusResponse(response) {
|
|
64
|
+
if (!response || response.success !== true || response.status !== 200) {
|
|
65
|
+
throw onboardingStatusError(
|
|
66
|
+
'Miso onboarding readiness is unavailable.',
|
|
67
|
+
'MISO_ONBOARDING_STATUS_UNAVAILABLE'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const wireBody = response.data;
|
|
71
|
+
if (!wireBody || typeof wireBody !== 'object' || Array.isArray(wireBody) ||
|
|
72
|
+
Object.prototype.hasOwnProperty.call(wireBody, 'success')) {
|
|
73
|
+
throw onboardingStatusError('Miso onboarding readiness response envelope is malformed.');
|
|
74
|
+
}
|
|
75
|
+
const data = wireBody.data;
|
|
76
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) ||
|
|
77
|
+
Object.prototype.hasOwnProperty.call(data, 'success')) {
|
|
78
|
+
throw onboardingStatusError('Miso onboarding readiness data envelope is malformed.');
|
|
79
|
+
}
|
|
80
|
+
const baselineDimensions = parseProjection(data.baselineDimensions, 'Dimension');
|
|
81
|
+
const baselineRoles = parseProjection(data.baselineRoles, 'Business Role');
|
|
82
|
+
const pair = `${projectionSignature(baselineDimensions)}::${projectionSignature(baselineRoles)}`;
|
|
83
|
+
if (!VALID_PAIRS.has(pair)) {
|
|
84
|
+
throw onboardingStatusError('Miso onboarding readiness contains an unsupported step ordering.');
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze({ baselineDimensions, baselineRoles });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fetch required-baseline readiness from Miso Controller.
|
|
91
|
+
* GET /api/v1/health/onboarding-status
|
|
92
|
+
* @requiresPermission {Controller} Public
|
|
93
|
+
* @param {string} controllerUrl
|
|
94
|
+
* @param {{timeoutMs?: number, client?: ApiClient}} [options]
|
|
95
|
+
* @returns {Promise<{baselineDimensions: Object, baselineRoles: Object}>}
|
|
96
|
+
*/
|
|
97
|
+
async function fetchOnboardingStatus(controllerUrl, options = {}) {
|
|
98
|
+
const client = options.client || new ApiClient(controllerUrl);
|
|
99
|
+
const requestOptions = {};
|
|
100
|
+
if (options.timeoutMs !== undefined) requestOptions.timeoutMs = options.timeoutMs;
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await client.get(ENDPOINT, requestOptions);
|
|
104
|
+
} catch {
|
|
105
|
+
throw onboardingStatusError(
|
|
106
|
+
'Miso onboarding readiness request failed.',
|
|
107
|
+
'MISO_ONBOARDING_STATUS_UNAVAILABLE'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return parseOnboardingStatusResponse(response);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
ENDPOINT,
|
|
115
|
+
fetchOnboardingStatus,
|
|
116
|
+
parseOnboardingStatusResponse
|
|
117
|
+
};
|
package/lib/app/register.js
CHANGED
|
@@ -28,6 +28,24 @@ const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-reg
|
|
|
28
28
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
29
29
|
const config = require('../core/config');
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Handle local credential persistence failure without exposing details in strict mode.
|
|
33
|
+
* @param {Error} error
|
|
34
|
+
* @param {boolean} required
|
|
35
|
+
* @returns {false}
|
|
36
|
+
*/
|
|
37
|
+
function handleCredentialPersistenceFailure(error, required) {
|
|
38
|
+
if (required) {
|
|
39
|
+
const persistenceError = new Error(
|
|
40
|
+
'Application credentials could not be saved to the supported local secret store.'
|
|
41
|
+
);
|
|
42
|
+
persistenceError.code = 'LOCAL_CREDENTIAL_PERSISTENCE_FAILED';
|
|
43
|
+
throw persistenceError;
|
|
44
|
+
}
|
|
45
|
+
logger.warn(chalk.yellow(`⚠ Could not save credentials locally: ${error.message}`));
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
/**
|
|
32
50
|
* Build registration data payload from app configuration
|
|
33
51
|
* @param {Object} appConfig - Application configuration
|
|
@@ -83,8 +101,11 @@ function buildRegistrationData(appConfig, options) {
|
|
|
83
101
|
* @async
|
|
84
102
|
* @param {Object} responseData - Registration response data
|
|
85
103
|
* @param {string} apiUrl - API URL
|
|
104
|
+
* @param {Object} [options] - Persistence behavior
|
|
105
|
+
* @param {boolean} [options.required=false] - Throw a safe typed error when persistence fails
|
|
106
|
+
* @returns {Promise<boolean>} True when credentials were persisted
|
|
86
107
|
*/
|
|
87
|
-
async function saveLocalCredentials(responseData, apiUrl) {
|
|
108
|
+
async function saveLocalCredentials(responseData, apiUrl, options = {}) {
|
|
88
109
|
const registeredAppKey = responseData.application.key;
|
|
89
110
|
const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
|
|
90
111
|
const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
|
|
@@ -113,8 +134,9 @@ async function saveLocalCredentials(responseData, apiUrl) {
|
|
|
113
134
|
logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
|
|
114
135
|
}
|
|
115
136
|
} catch (error) {
|
|
116
|
-
|
|
137
|
+
return handleCredentialPersistenceFailure(error, options.required === true);
|
|
117
138
|
}
|
|
139
|
+
return true;
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
/**
|
|
@@ -20,6 +20,11 @@ const secretsEnsure = require('../core/secrets-ensure');
|
|
|
20
20
|
const secretsEnvWrite = require('../core/secrets-env-write');
|
|
21
21
|
const { getContainerPort } = require('../utils/port-resolver');
|
|
22
22
|
const { applyAdminSecretsPlatformOverlay } = require('../core/admin-secrets-env-overlay');
|
|
23
|
+
const {
|
|
24
|
+
resolveDimensionAbacSetting,
|
|
25
|
+
applyDimensionAbacToEnvMap
|
|
26
|
+
} = require('../utils/dataplane-dimension-abac-setting');
|
|
27
|
+
const { resolveEffectiveRegistry } = require('../utils/resolve-docker-image-ref');
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Merge app env with admin-secrets; overlay ensures platform keys come from admin file.
|
|
@@ -38,6 +43,13 @@ async function applyOnboardingPlatformModeIfNeeded(appName, merged) {
|
|
|
38
43
|
merged.ONBOARDING_FULL_PLATFORM = mode === 'full' ? 'true' : 'false';
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
async function applyDimensionAbacSettingIfNeeded(appName, merged) {
|
|
47
|
+
if (appName !== 'dataplane') return;
|
|
48
|
+
const userConfig = await config.getConfig();
|
|
49
|
+
const resolved = resolveDimensionAbacSetting(userConfig);
|
|
50
|
+
applyDimensionAbacToEnvMap(appName, merged, resolved.value);
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/**
|
|
42
54
|
* Backfill only secrets needed for `aifabrix run <app>`: kv:// keys from that app's env.template
|
|
43
55
|
* (catalog defaults via placeholderContext). Does not run full `ensureInfraSecrets`.
|
|
@@ -260,6 +272,29 @@ async function applyPostgresPasswordFromPgpass(merged, developerId) {
|
|
|
260
272
|
if (pgpassPwd !== undefined) merged.POSTGRES_PASSWORD = pgpassPwd;
|
|
261
273
|
}
|
|
262
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Deliver existing local installation inputs to privileged Miso bootstrap only.
|
|
277
|
+
* @param {string} appName
|
|
278
|
+
* @param {Object} merged
|
|
279
|
+
* @param {Object} appConfig
|
|
280
|
+
* @param {Object|null} scopeOpts
|
|
281
|
+
*/
|
|
282
|
+
function applyMisoBootstrapInputsIfNeeded(appName, merged, appConfig, scopeOpts) {
|
|
283
|
+
if (appName !== 'miso-controller') return;
|
|
284
|
+
const postgresPassword = merged.POSTGRES_PASSWORD;
|
|
285
|
+
if (typeof postgresPassword !== 'string' || postgresPassword.trim().length === 0) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
'Local PostgreSQL administrator password is unavailable. Re-run "aifabrix up-infra" before starting Miso Controller.'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
merged.POSTGRES_ADMIN_PASSWORD = postgresPassword;
|
|
291
|
+
const { registry } = resolveEffectiveRegistry(appConfig, {
|
|
292
|
+
registry: scopeOpts?.registry
|
|
293
|
+
});
|
|
294
|
+
if (registry) merged.ACR_LOGIN_SERVER = registry;
|
|
295
|
+
else delete merged.ACR_LOGIN_SERVER;
|
|
296
|
+
}
|
|
297
|
+
|
|
263
298
|
/**
|
|
264
299
|
* @param {string} runEnvPath
|
|
265
300
|
* @param {string} runEnvAdminPath
|
|
@@ -300,8 +335,10 @@ async function buildMergedRunEnvAndWrite(appName, appConfig, devDir, developerId
|
|
|
300
335
|
mergeRunEnvWithAdminOverlay(merged, adminObj, appName);
|
|
301
336
|
await applyOnboardingPlatformModeIfNeeded(appName, merged);
|
|
302
337
|
await applyPostgresPasswordFromPgpass(merged, developerId);
|
|
338
|
+
applyMisoBootstrapInputsIfNeeded(appName, merged, appConfig, scopeOpts);
|
|
303
339
|
injectDatabaseNamesAndUsers(merged, appConfig, scopeOpts);
|
|
304
340
|
injectContainerPortForRun(merged, appConfig, appName);
|
|
341
|
+
await applyDimensionAbacSettingIfNeeded(appName, merged);
|
|
305
342
|
|
|
306
343
|
const runEnvPath = path.join(devDir, '.env.run');
|
|
307
344
|
const runEnvAdminPath = path.join(devDir, '.env.run.admin');
|
|
@@ -351,9 +388,9 @@ async function ensureRunEnvFilesPresent(
|
|
|
351
388
|
* @throws {Error} If password keys appear in environment-like assignment
|
|
352
389
|
*/
|
|
353
390
|
function assertNoPasswordLiteralsInCompose(composeContent) {
|
|
354
|
-
const badPattern = /\n\s+(-?\s*)(POSTGRES_PASSWORD|DB_\d+_PASSWORD)\s*[:=]/;
|
|
391
|
+
const badPattern = /\n\s+(-?\s*)(POSTGRES_PASSWORD|POSTGRES_ADMIN_PASSWORD|DB_\d+_PASSWORD)\s*[:=]/;
|
|
355
392
|
if (badPattern.test(composeContent)) {
|
|
356
|
-
throw new Error('Generated compose must not contain password literals (
|
|
393
|
+
throw new Error('Generated compose must not contain password literals (PostgreSQL or application database passwords). Use env_file only.');
|
|
357
394
|
}
|
|
358
395
|
}
|
|
359
396
|
|
|
@@ -26,6 +26,7 @@ function buildRunEnvRecovery(appName, prepared) {
|
|
|
26
26
|
prepared.appConfig.developerId,
|
|
27
27
|
{
|
|
28
28
|
runEnvKey: prepared.mergedRunOptions.env,
|
|
29
|
+
registry: prepared.mergedRunOptions.registry,
|
|
29
30
|
effectiveEnvironmentScopedResources:
|
|
30
31
|
prepared.mergedRunOptions.effectiveEnvironmentScopedResources
|
|
31
32
|
}
|
package/lib/app/run-helpers.js
CHANGED
|
@@ -366,7 +366,7 @@ async function prepareEnvironment(appName, appConfig, options) {
|
|
|
366
366
|
appConfig,
|
|
367
367
|
devDir,
|
|
368
368
|
developerId,
|
|
369
|
-
{ runEnvKey, effectiveEnvironmentScopedResources }
|
|
369
|
+
{ runEnvKey, effectiveEnvironmentScopedResources, registry: options.registry }
|
|
370
370
|
);
|
|
371
371
|
const { hydrateMissingLocalParameters } = require('./run-parameter-sync');
|
|
372
372
|
await hydrateMissingLocalParameters(appName, runEnvPath);
|
package/lib/cli/infra-guided.js
CHANGED
|
@@ -49,6 +49,34 @@ const {
|
|
|
49
49
|
emitSystemBuilderAppManifestLineIfTTY
|
|
50
50
|
} = require('../utils/manifest-source-emit');
|
|
51
51
|
const { flushDeferredPublicUrlWarnings } = require('./infra-guided-public-url-warn');
|
|
52
|
+
const { fetchOnboardingStatus } = require('../api/onboarding-status.api');
|
|
53
|
+
const {
|
|
54
|
+
waitForRequiredBaselineReadiness
|
|
55
|
+
} = require('../commands/setup-onboarding-readiness');
|
|
56
|
+
|
|
57
|
+
async function runRequiredBaselineReadinessIfNeeded(options, authOpts) {
|
|
58
|
+
if (!options || options.requireBaselineReadiness !== true) return;
|
|
59
|
+
const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
|
|
60
|
+
const spin = startSpinner('Waiting for required Miso baseline onboarding...');
|
|
61
|
+
const onProgress = createSpinnerTextUpdater(spin);
|
|
62
|
+
try {
|
|
63
|
+
await waitForRequiredBaselineReadiness({
|
|
64
|
+
request: requestOptions => fetchOnboardingStatus(controllerUrl, requestOptions),
|
|
65
|
+
onProgress
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (spin && typeof spin.fail === 'function') spin.fail('Miso baseline onboarding not ready');
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
stopSpinnerSuccess(spin, 'Miso baseline onboarding ready');
|
|
72
|
+
logger.log('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runGuidedMisoAndReadinessPhase(options, handleUpMiso, authOpts) {
|
|
76
|
+
await withMutedLogger(() => prepareUrlsLocalRegistryForUpPlatform());
|
|
77
|
+
await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
|
|
78
|
+
await runRequiredBaselineReadinessIfNeeded(options, authOpts);
|
|
79
|
+
}
|
|
52
80
|
|
|
53
81
|
/**
|
|
54
82
|
* Guided Keycloak → Miso Controller start with one spinner per app (no second health wait).
|
|
@@ -327,8 +355,7 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
|
|
|
327
355
|
skipLoginIfAuthenticated: options && options.skipLoginIfAuthenticated === true
|
|
328
356
|
};
|
|
329
357
|
|
|
330
|
-
await
|
|
331
|
-
await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
|
|
358
|
+
await runGuidedMisoAndReadinessPhase(options, handleUpMiso, authOpts);
|
|
332
359
|
await runGuidedKeycloakRealmPhase(options);
|
|
333
360
|
await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
|
|
334
361
|
const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
|
|
@@ -388,6 +415,7 @@ module.exports = {
|
|
|
388
415
|
runGuidedUpMiso,
|
|
389
416
|
runGuidedUpDataplane,
|
|
390
417
|
runGuidedUpPlatform,
|
|
418
|
+
runRequiredBaselineReadinessIfNeeded,
|
|
391
419
|
runGuidedDownInfra,
|
|
392
420
|
stripHealthPrefix
|
|
393
421
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `setup` is the one-shot platform installer. When no infrastructure is
|
|
5
5
|
* detected it runs a small wizard (admin email/password, AI tool keys) and
|
|
6
6
|
* then `up-infra` + `up-platform`. When infra is already up, it offers a
|
|
7
|
-
* mode menu (re-install, wipe data,
|
|
7
|
+
* mode menu (re-install, wipe data, update images).
|
|
8
8
|
*
|
|
9
9
|
* `teardown` is the symmetrical inverse of `setup`: `down-infra -v` plus a
|
|
10
10
|
* full clean of the AI Fabrix system directory (everything except
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
const { handleCommandError } = require('../utils/cli-utils');
|
|
21
21
|
const { handleSetup } = require('../commands/setup');
|
|
22
22
|
const { handleTeardown } = require('../commands/teardown');
|
|
23
|
+
const { parseDimensionAbacOption } = require('../utils/dataplane-dimension-abac-setting');
|
|
23
24
|
|
|
24
25
|
const SETUP_HELP_AFTER = `
|
|
25
26
|
What this command does:
|
|
@@ -27,16 +28,25 @@ What this command does:
|
|
|
27
28
|
- With infra running: shows a mode menu:
|
|
28
29
|
1) Re-install — stop infra and remove all volumes, then up-infra + up-platform --force.
|
|
29
30
|
2) Wipe data — drop every database and DB user, then up-infra + up-platform --force.
|
|
30
|
-
3)
|
|
31
|
-
4) Update images — pull the latest infra and platform images, then up-infra + up-platform.
|
|
31
|
+
3) Update images — pull the latest infra and platform images, then up-infra + up-platform.
|
|
32
32
|
- With --mode fresh: first-time install when no infra is running (non-interactive; use with --admin-email and --admin-password).
|
|
33
33
|
- With --mode update-images: refresh images when infra is already running (non-interactive).
|
|
34
34
|
|
|
35
35
|
AI tool keys are read from the user-local file (~/.aifabrix/secrets.local.yaml) merged with the shared aifabrix-secrets file. If either source already provides keys, the AI tool prompt is skipped.
|
|
36
36
|
|
|
37
|
+
Dimension ABAC defaults to off. Use --dimension-abac on|off to persist an explicit Dataplane selection. The saved selection is reused by later setup and redeploy flows; changing it takes effect after the normal Dataplane restart/redeploy boundary.
|
|
38
|
+
|
|
37
39
|
Use 'aifabrix teardown' to fully remove the local installation.
|
|
38
40
|
`;
|
|
39
41
|
|
|
42
|
+
function parseRepeatedDimensionAbacOption(raw, previous) {
|
|
43
|
+
const value = parseDimensionAbacOption(raw);
|
|
44
|
+
if (previous !== undefined && previous !== value) {
|
|
45
|
+
throw new Error('Conflicting --dimension-abac values. Supply only on or only off.');
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
const TEARDOWN_HELP_AFTER = `
|
|
41
51
|
What this command does:
|
|
42
52
|
- Runs down-infra -v (stops all infra + apps, removes every Docker volume).
|
|
@@ -64,6 +74,11 @@ function registerSetup(program) {
|
|
|
64
74
|
.option('--integration-server', 'Install the integration server (runs once in the miso environment)')
|
|
65
75
|
.option('--no-integration-server', 'Do not install the integration server (default)')
|
|
66
76
|
.option('--mode <mode>', 'Non-interactive mode: fresh (no infra) or update-images (infra running)')
|
|
77
|
+
.option(
|
|
78
|
+
'--dimension-abac <off|on>',
|
|
79
|
+
'Persist Dataplane Dimension ABAC selection (default: off)',
|
|
80
|
+
parseRepeatedDimensionAbacOption
|
|
81
|
+
)
|
|
67
82
|
.option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
|
|
68
83
|
.addHelpText('after', SETUP_HELP_AFTER)
|
|
69
84
|
.action(async(options) => {
|
|
@@ -296,7 +296,8 @@ async function runUpPlatform(opts = {}) {
|
|
|
296
296
|
base: true,
|
|
297
297
|
platformControllerUrl: authCtx.platformControllerUrl,
|
|
298
298
|
skipLoginIfAuthenticated: authCtx.skipLoginIfAuthenticated,
|
|
299
|
-
requireKeycloakRealm: opts.requireKeycloakRealm === true
|
|
299
|
+
requireKeycloakRealm: opts.requireKeycloakRealm === true,
|
|
300
|
+
requireBaselineReadiness: true
|
|
300
301
|
},
|
|
301
302
|
upMiso.handleUpMiso,
|
|
302
303
|
upDataplane.handleUpDataplane,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded observer for required Miso baseline onboarding.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Setup-only Miso baseline readiness observer
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { performance } = require('node:perf_hooks');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_INTERVAL_MS = 2000;
|
|
14
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
|
|
15
|
+
const DEFAULT_DEADLINE_MS = 90000;
|
|
16
|
+
|
|
17
|
+
function monotonicNow() {
|
|
18
|
+
return performance.now();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readinessError(message, code) {
|
|
22
|
+
const error = new Error(message);
|
|
23
|
+
error.code = code;
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function classifyReadiness(snapshot) {
|
|
28
|
+
const dimensions = snapshot.baselineDimensions;
|
|
29
|
+
const roles = snapshot.baselineRoles;
|
|
30
|
+
if (dimensions.state === 'incomplete') {
|
|
31
|
+
throw readinessError(
|
|
32
|
+
`Required baseline Dimensions are incomplete (${dimensions.code}). Retry setup after reconciliation completes.`,
|
|
33
|
+
dimensions.code
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (roles.state === 'incomplete') {
|
|
37
|
+
throw readinessError(
|
|
38
|
+
`Required baseline Business Roles are incomplete (${roles.code}). Retry setup after reconciliation completes.`,
|
|
39
|
+
roles.code
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (dimensions.state === 'complete' && roles.state === 'complete') return 'complete';
|
|
43
|
+
return dimensions.state === 'pending' ? 'dimensions' : 'roles';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function timeoutError() {
|
|
47
|
+
return readinessError(
|
|
48
|
+
'Timed out waiting for required Miso baseline onboarding. Retry setup after reconciliation completes.',
|
|
49
|
+
'MISO_BASELINE_READINESS_TIMEOUT'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function waitForRequiredBaselineReadiness(options) {
|
|
54
|
+
const request = options.request;
|
|
55
|
+
const now = options.now || monotonicNow;
|
|
56
|
+
const sleep = options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
57
|
+
const onProgress = options.onProgress || (() => {});
|
|
58
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
59
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
60
|
+
const deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
61
|
+
const startedAt = now();
|
|
62
|
+
while (now() - startedAt < deadlineMs) {
|
|
63
|
+
const remaining = deadlineMs - (now() - startedAt);
|
|
64
|
+
if (remaining <= 0) throw timeoutError();
|
|
65
|
+
const snapshot = await request({ timeoutMs: Math.min(requestTimeoutMs, remaining) });
|
|
66
|
+
const state = classifyReadiness(snapshot);
|
|
67
|
+
if (state === 'complete') return snapshot;
|
|
68
|
+
onProgress(state === 'dimensions'
|
|
69
|
+
? 'Waiting for required baseline Dimensions...'
|
|
70
|
+
: 'Waiting for required baseline Business Roles...');
|
|
71
|
+
const afterRequest = deadlineMs - (now() - startedAt);
|
|
72
|
+
if (afterRequest <= 0) throw timeoutError();
|
|
73
|
+
await sleep(Math.min(intervalMs, afterRequest));
|
|
74
|
+
}
|
|
75
|
+
throw timeoutError();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
DEFAULT_INTERVAL_MS,
|
|
80
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
81
|
+
DEFAULT_DEADLINE_MS,
|
|
82
|
+
waitForRequiredBaselineReadiness
|
|
83
|
+
};
|
package/lib/commands/setup.js
CHANGED
|
@@ -36,6 +36,11 @@ const setupModes = require('./setup-modes');
|
|
|
36
36
|
const { syncPlatformControllerUrlsInConfig } = require('./setup-platform-auth');
|
|
37
37
|
const installationLog = require('../utils/installation-log');
|
|
38
38
|
const { beginSetupCommandFlow, endSetupCommandFlow } = require('./setup-run-context');
|
|
39
|
+
const {
|
|
40
|
+
parseDimensionAbacOption,
|
|
41
|
+
resolveDimensionAbacSetting,
|
|
42
|
+
persistExplicitDimensionAbacSetting
|
|
43
|
+
} = require('../utils/dataplane-dimension-abac-setting');
|
|
39
44
|
|
|
40
45
|
const MODE = setupPrompts.MODE;
|
|
41
46
|
|
|
@@ -87,7 +92,20 @@ function normalizeCliMode(options) {
|
|
|
87
92
|
* @param {'fresh'|'update-images'} cliMode
|
|
88
93
|
* @returns {Promise<{ outcome: string, setupMode: string }>}
|
|
89
94
|
*/
|
|
90
|
-
async function
|
|
95
|
+
async function persistDimensionAbacSelection(settingContext) {
|
|
96
|
+
await persistExplicitDimensionAbacSetting(config, settingContext.explicitValue);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function prepareDimensionAbacSetting(options) {
|
|
100
|
+
const explicitValue = parseDimensionAbacOption(options.dimensionAbac);
|
|
101
|
+
const userConfig = await config.getConfig();
|
|
102
|
+
const resolved = resolveDimensionAbacSetting(userConfig, explicitValue);
|
|
103
|
+
const displayValue = resolved.value ? 'on' : 'off';
|
|
104
|
+
logger.log(infoLine(`Dimension ABAC: ${displayValue} (${resolved.source}).`));
|
|
105
|
+
return { explicitValue, ...resolved };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runSetupWithCliMode(options, cliMode, settingContext) {
|
|
91
109
|
logSetupHeader();
|
|
92
110
|
logger.log(formatProgress('Detecting current installation state...'));
|
|
93
111
|
const running = await isInfraRunning();
|
|
@@ -97,12 +115,13 @@ async function runSetupWithCliMode(options, cliMode) {
|
|
|
97
115
|
'Infrastructure is already running; use --mode update-images or run teardown first.'
|
|
98
116
|
);
|
|
99
117
|
}
|
|
100
|
-
await runFreshInstallFlow(options);
|
|
118
|
+
await runFreshInstallFlow(options, settingContext);
|
|
101
119
|
return { outcome: 'success', setupMode: CLI_SETUP_MODES.FRESH };
|
|
102
120
|
}
|
|
103
121
|
if (!running) {
|
|
104
122
|
throw new Error('No infrastructure running; use --mode fresh for first-time install.');
|
|
105
123
|
}
|
|
124
|
+
await persistDimensionAbacSelection(settingContext);
|
|
106
125
|
await dispatchMode(MODE.UPDATE_IMAGES);
|
|
107
126
|
logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${cliMode}).`));
|
|
108
127
|
return { outcome: 'success', setupMode: cliMode };
|
|
@@ -120,12 +139,13 @@ async function applySetupPlatformMode(options) {
|
|
|
120
139
|
return mode;
|
|
121
140
|
}
|
|
122
141
|
|
|
123
|
-
async function runFreshInstallFlow(options) {
|
|
142
|
+
async function runFreshInstallFlow(options, settingContext) {
|
|
124
143
|
await maybePinDeveloperIdForFreshInstall(options.developer);
|
|
125
144
|
logger.log(infoLine('No infrastructure detected; starting fresh install.'));
|
|
126
145
|
await applySetupPlatformMode(options);
|
|
127
146
|
const adminCreds = await setupPrompts.promptAdminCredentials(options);
|
|
128
147
|
await config.setAdminEmail(adminCreds.adminEmail);
|
|
148
|
+
await persistDimensionAbacSelection(settingContext);
|
|
129
149
|
await setupModes.runFreshInstall(adminCreds);
|
|
130
150
|
logger.log(formatSuccessParagraph('aifabrix setup complete (fresh install).'));
|
|
131
151
|
}
|
|
@@ -134,7 +154,7 @@ async function runFreshInstallFlow(options) {
|
|
|
134
154
|
* @param {boolean} assumeYes
|
|
135
155
|
* @returns {Promise<{ aborted: boolean, mode: string|null }>}
|
|
136
156
|
*/
|
|
137
|
-
async function runExistingInfraFlow(assumeYes) {
|
|
157
|
+
async function runExistingInfraFlow(assumeYes, settingContext) {
|
|
138
158
|
const mode = await setupPrompts.promptModeSelection();
|
|
139
159
|
const proceed = await setupPrompts.confirmDestructiveMode(mode, assumeYes);
|
|
140
160
|
if (!proceed) {
|
|
@@ -145,6 +165,7 @@ async function runExistingInfraFlow(assumeYes) {
|
|
|
145
165
|
if (mode === MODE.REINSTALL || mode === MODE.WIPE_DATA) {
|
|
146
166
|
await applySetupPlatformMode({});
|
|
147
167
|
}
|
|
168
|
+
await persistDimensionAbacSelection(settingContext);
|
|
148
169
|
await dispatchMode(mode);
|
|
149
170
|
logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${mode}).`));
|
|
150
171
|
return { aborted: false, mode };
|
|
@@ -197,20 +218,19 @@ async function dispatchMode(mode) {
|
|
|
197
218
|
* @param {boolean} assumeYes
|
|
198
219
|
* @returns {Promise<{ outcome: string, setupMode: string|null }>}
|
|
199
220
|
*/
|
|
200
|
-
async function runSetupMainPath(options, assumeYes) {
|
|
201
|
-
const cliMode = normalizeCliMode(options);
|
|
221
|
+
async function runSetupMainPath(options, assumeYes, cliMode, settingContext) {
|
|
202
222
|
if (cliMode) {
|
|
203
|
-
return runSetupWithCliMode(options, cliMode);
|
|
223
|
+
return runSetupWithCliMode(options, cliMode, settingContext);
|
|
204
224
|
}
|
|
205
225
|
|
|
206
226
|
logSetupHeader();
|
|
207
227
|
logger.log(formatProgress('Detecting current installation state...'));
|
|
208
228
|
const running = await isInfraRunning();
|
|
209
229
|
if (!running) {
|
|
210
|
-
await runFreshInstallFlow(options);
|
|
230
|
+
await runFreshInstallFlow(options, settingContext);
|
|
211
231
|
return { outcome: 'success', setupMode: 'fresh' };
|
|
212
232
|
}
|
|
213
|
-
const r = await runExistingInfraFlow(assumeYes);
|
|
233
|
+
const r = await runExistingInfraFlow(assumeYes, settingContext);
|
|
214
234
|
const outcome = r.aborted ? 'aborted' : 'success';
|
|
215
235
|
return { outcome, setupMode: r.mode };
|
|
216
236
|
}
|
|
@@ -300,7 +320,9 @@ async function handleSetup(options = {}) {
|
|
|
300
320
|
|
|
301
321
|
beginSetupCommandFlow();
|
|
302
322
|
try {
|
|
303
|
-
const
|
|
323
|
+
const cliMode = normalizeCliMode(options);
|
|
324
|
+
const settingContext = await prepareDimensionAbacSetting(options);
|
|
325
|
+
const main = await runSetupMainPath(options, assumeYes, cliMode, settingContext);
|
|
304
326
|
outcome = main.outcome;
|
|
305
327
|
setupMode = main.setupMode;
|
|
306
328
|
} catch (e) {
|