@aifabrix/builder 2.61.2 → 2.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/onboarding-status.api.js +117 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/register.js +24 -2
- package/lib/app/run-env-compose.js +44 -8
- package/lib/app/run-env-recovery.js +1 -0
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/run.js +6 -4
- package/lib/build/standard-docker-build.js +4 -0
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +60 -12
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +20 -3
- package/lib/commands/auth-config.js +2 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +3 -1
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +9 -2
- package/lib/commands/setup-onboarding-readiness.js +83 -0
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup.js +35 -10
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +149 -16
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +26 -34
- package/lib/commands/up-integration-server.js +112 -0
- package/lib/commands/wizard-config-normalizer.js +5 -1
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +12 -9
- package/lib/core/secrets-env-write.js +1 -1
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/index.js +32 -9
- package/lib/generator/split-variables.js +2 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- package/lib/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- package/lib/schema/infra.parameter.yaml +61 -27
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +2 -2
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/log-redaction.js +4 -1
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -7
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/package.json +4 -2
- package/templates/README.md +6 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +43 -1
- package/templates/applications/miso-controller/env.template +45 -39
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +85 -7
package/lib/cli/infra-guided.js
CHANGED
|
@@ -14,6 +14,10 @@ const { execWithDockerEnv } = require('../utils/docker-exec');
|
|
|
14
14
|
const { getLogLevel, passesLevelFilter } = require('../commands/app-logs');
|
|
15
15
|
const { handleLogin } = require('../commands/login');
|
|
16
16
|
const { handleUpBuilderApi } = require('../commands/up-builder-api');
|
|
17
|
+
const {
|
|
18
|
+
handleUpIntegrationServer,
|
|
19
|
+
isIntegrationServerSelected
|
|
20
|
+
} = require('../commands/up-integration-server');
|
|
17
21
|
const {
|
|
18
22
|
resolveGuidedAuthControllerUrl,
|
|
19
23
|
runGuidedAuthStep,
|
|
@@ -45,6 +49,34 @@ const {
|
|
|
45
49
|
emitSystemBuilderAppManifestLineIfTTY
|
|
46
50
|
} = require('../utils/manifest-source-emit');
|
|
47
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
|
+
}
|
|
48
80
|
|
|
49
81
|
/**
|
|
50
82
|
* Guided Keycloak → Miso Controller start with one spinner per app (no second health wait).
|
|
@@ -295,8 +327,25 @@ async function runGuidedDataplaneInstallPhase(options, handleUpDataplane, handle
|
|
|
295
327
|
await refreshDataplaneVersionAfterPlatformInstall();
|
|
296
328
|
}
|
|
297
329
|
|
|
330
|
+
async function runGuidedIntegrationServerPhase(options, selected) {
|
|
331
|
+
if (!selected) return;
|
|
332
|
+
const spin = startSpinner('Starting Integration Server...');
|
|
333
|
+
await withMutedLogger(() => handleUpIntegrationServer({ ...options, platformInstall: true }));
|
|
334
|
+
stopSpinnerSuccess(spin, 'Integration Server ready');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function runGuidedKeycloakRealmPhase(options) {
|
|
338
|
+
if (!options || options.requireKeycloakRealm !== true) return;
|
|
339
|
+
const { waitForKeycloakRealmReady } = require('../utils/keycloak-realm-ready');
|
|
340
|
+
const spin = startSpinner('Waiting for Keycloak realm (aifabrix)...');
|
|
341
|
+
await waitForKeycloakRealmReady({ timeoutSeconds: 180 });
|
|
342
|
+
stopSpinnerSuccess(spin, 'Keycloak realm ready');
|
|
343
|
+
logger.log('');
|
|
344
|
+
}
|
|
345
|
+
|
|
298
346
|
async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, handleLogin, forceCleanSummary = null) {
|
|
299
|
-
|
|
347
|
+
const integrationServerSelected = await isIntegrationServerSelected();
|
|
348
|
+
logGuidedPlatformSetupHeader({ integrationServer: integrationServerSelected });
|
|
300
349
|
if (forceCleanSummary && forceCleanSummary.forceSummary) {
|
|
301
350
|
logUpPlatformForceCleanSummary(forceCleanSummary.forceSummary, forceCleanSummary.cleanedApps);
|
|
302
351
|
}
|
|
@@ -306,23 +355,21 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
|
|
|
306
355
|
skipLoginIfAuthenticated: options && options.skipLoginIfAuthenticated === true
|
|
307
356
|
};
|
|
308
357
|
|
|
309
|
-
await
|
|
310
|
-
await
|
|
311
|
-
if (options && options.requireKeycloakRealm === true) {
|
|
312
|
-
const { waitForKeycloakRealmReady } = require('../utils/keycloak-realm-ready');
|
|
313
|
-
const realmSpin = startSpinner('Waiting for Keycloak realm (aifabrix)...');
|
|
314
|
-
await waitForKeycloakRealmReady({ timeoutSeconds: 180 });
|
|
315
|
-
stopSpinnerSuccess(realmSpin, 'Keycloak realm ready');
|
|
316
|
-
logger.log('');
|
|
317
|
-
}
|
|
358
|
+
await runGuidedMisoAndReadinessPhase(options, handleUpMiso, authOpts);
|
|
359
|
+
await runGuidedKeycloakRealmPhase(options);
|
|
318
360
|
await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
|
|
319
361
|
const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
|
|
320
362
|
await runGuidedDataplaneInstallPhase(options, handleUpDataplane, handleLogin, controllerUrl);
|
|
321
363
|
const baSpin = startSpinner('Starting Builder API...');
|
|
322
364
|
await withMutedLogger(() => handleUpBuilderApi({ ...options, platformInstall: true }));
|
|
323
365
|
stopSpinnerSuccess(baSpin, 'Builder API ready');
|
|
324
|
-
await
|
|
325
|
-
|
|
366
|
+
await runGuidedIntegrationServerPhase(options, integrationServerSelected);
|
|
367
|
+
const appNames = [
|
|
368
|
+
'miso-controller', 'dataplane', 'builder-api',
|
|
369
|
+
...(integrationServerSelected ? ['integration-server'] : [])
|
|
370
|
+
];
|
|
371
|
+
await validateAppErrorLogs(appNames);
|
|
372
|
+
await logPlatformReadyFooter({ integrationServer: integrationServerSelected });
|
|
326
373
|
}
|
|
327
374
|
|
|
328
375
|
async function runGuidedDownInfra(appName, options, infraMod, appLib) {
|
|
@@ -368,6 +415,7 @@ module.exports = {
|
|
|
368
415
|
runGuidedUpMiso,
|
|
369
416
|
runGuidedUpDataplane,
|
|
370
417
|
runGuidedUpPlatform,
|
|
418
|
+
runRequiredBaselineReadinessIfNeeded,
|
|
371
419
|
runGuidedDownInfra,
|
|
372
420
|
stripHealthPrefix
|
|
373
421
|
};
|
|
@@ -17,6 +17,10 @@ const { handleLogin } = require('../commands/login');
|
|
|
17
17
|
const { handleUpMiso } = require('../commands/up-miso');
|
|
18
18
|
const { handleUpDataplane } = require('../commands/up-dataplane');
|
|
19
19
|
const { handleUpBuilderApi } = require('../commands/up-builder-api');
|
|
20
|
+
const {
|
|
21
|
+
handleUpIntegrationServer,
|
|
22
|
+
isIntegrationServerSelected
|
|
23
|
+
} = require('../commands/up-integration-server');
|
|
20
24
|
const {
|
|
21
25
|
applyUpPlatformForceConfig,
|
|
22
26
|
cleanBuilderAppDirs,
|
|
@@ -26,7 +30,13 @@ const {
|
|
|
26
30
|
const { runGuidedUpPlatform } = require('./infra-guided');
|
|
27
31
|
const { recordInfraInstallationCommand } = require('./installation-log-command');
|
|
28
32
|
|
|
29
|
-
const PLATFORM_APPS = [
|
|
33
|
+
const PLATFORM_APPS = [
|
|
34
|
+
'keycloak',
|
|
35
|
+
'miso-controller',
|
|
36
|
+
'dataplane',
|
|
37
|
+
'builder-api',
|
|
38
|
+
'integration-server'
|
|
39
|
+
];
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* @param {Object} options
|
|
@@ -59,6 +69,9 @@ async function runUpPlatformWork(options) {
|
|
|
59
69
|
await handleUpMiso(options);
|
|
60
70
|
await handleUpDataplane(options);
|
|
61
71
|
await handleUpBuilderApi({ ...options, platformInstall: true });
|
|
72
|
+
if (await isIntegrationServerSelected()) {
|
|
73
|
+
await handleUpIntegrationServer({ ...options, platformInstall: true });
|
|
74
|
+
}
|
|
62
75
|
}
|
|
63
76
|
return cleanedAppKeys;
|
|
64
77
|
}
|
|
@@ -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).
|
|
@@ -61,7 +71,14 @@ function registerSetup(program) {
|
|
|
61
71
|
.option('--pro-password-mode <mode>', 'Pro profile only: autogen or manual')
|
|
62
72
|
.option('--admin-email <email>', 'Admin email (non-interactive fresh install)')
|
|
63
73
|
.option('--admin-password <password>', 'Admin password for dev or pro manual-single (non-interactive)')
|
|
74
|
+
.option('--integration-server', 'Install the integration server (runs once in the miso environment)')
|
|
75
|
+
.option('--no-integration-server', 'Do not install the integration server (default)')
|
|
64
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
|
+
)
|
|
65
82
|
.option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
|
|
66
83
|
.addHelpText('after', SETUP_HELP_AFTER)
|
|
67
84
|
.action(async(options) => {
|
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
checkUserLoggedIn
|
|
24
24
|
} = require('../utils/auth-config-validator');
|
|
25
25
|
const { hasStoredDeviceTokenForController } = require('../utils/controller-url');
|
|
26
|
+
const { assertEnvironmentAllowedForCurrentMode } = require('../core/environment-mode-policy');
|
|
26
27
|
const logger = require('../utils/logger');
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -166,6 +167,7 @@ async function handleSetEnvironment(environment) {
|
|
|
166
167
|
try {
|
|
167
168
|
// Validate environment format
|
|
168
169
|
validateEnvironment(environment);
|
|
170
|
+
await assertEnvironmentAllowedForCurrentMode(environment);
|
|
169
171
|
|
|
170
172
|
// Get current controller from config
|
|
171
173
|
const controllerUrl = await getControllerUrl();
|
package/lib/commands/login.js
CHANGED
|
@@ -121,6 +121,10 @@ async function normalizeControllerUrl(options) {
|
|
|
121
121
|
async function handleEnvironmentConfig(options) {
|
|
122
122
|
if (options.environment) {
|
|
123
123
|
const environment = options.environment.trim();
|
|
124
|
+
// `login -e tst` sets the environment without going through `auth set-environment`,
|
|
125
|
+
// so the mode rule has to be applied here too or it is trivially bypassed.
|
|
126
|
+
const { assertEnvironmentAllowedForCurrentMode } = require('../core/environment-mode-policy');
|
|
127
|
+
await assertEnvironmentAllowedForCurrentMode(environment);
|
|
124
128
|
await setCurrentEnvironment(environment);
|
|
125
129
|
if (!options.compact) {
|
|
126
130
|
logger.log(chalk.gray(`Environment: ${environment}`));
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
+
const { repairLegacyDatasource } = require('./repair-datasource-legacy');
|
|
11
12
|
const { repairKeyedDocument } = require('./repair-datasource-keyed-document');
|
|
12
13
|
const { repairOpenapiSection } = require('./repair-datasource-openapi');
|
|
13
14
|
const { repairSyncOperationsFromCapabilities } = require('./repair-datasource-sync-operations');
|
|
@@ -122,7 +123,8 @@ function applyOpenapiDatasourceRepairs(parsed, changes) {
|
|
|
122
123
|
*/
|
|
123
124
|
function runRepairDatasourceFile(parsed, options, changes, helpers) {
|
|
124
125
|
const out = Array.isArray(changes) ? changes : [];
|
|
125
|
-
let updated =
|
|
126
|
+
let updated = repairLegacyDatasource(parsed, out);
|
|
127
|
+
updated = helpers.stripDeprecatedServiceCategory(parsed, out) || updated;
|
|
126
128
|
|
|
127
129
|
if (shouldApplyServiceContractRepair(parsed, options)) {
|
|
128
130
|
updated = repairNoneLlmToServiceContract(parsed, out) || updated;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Migrate legacy datasource storage and mapping shapes.
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const ENTITY_TYPES = {
|
|
8
|
+
'record-storage': 'recordStorage',
|
|
9
|
+
'document-storage': 'documentStorage',
|
|
10
|
+
'vector-store': 'vectorStore',
|
|
11
|
+
'message-service': 'messageService'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Preserve legacy attribute types in metadataSchema and qualify raw input paths.
|
|
16
|
+
* @param {Object} parsed
|
|
17
|
+
* @param {string[]} changes
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function repairLegacyAttributes(parsed, changes) {
|
|
21
|
+
let updated = false;
|
|
22
|
+
for (const [key, attr] of Object.entries(parsed.fieldMappings?.attributes || {})) {
|
|
23
|
+
if (!attr || typeof attr !== 'object') continue;
|
|
24
|
+
if (typeof attr.expression === 'string' && /^\{\{\s*metadata\.[\w.[\]]+\s*\}\}$/.test(attr.expression)) {
|
|
25
|
+
attr.expression = attr.expression.replace(/\{\{\s*(metadata\.[\w.[\]]+)\s*\}\}/, '{{raw.$1}}');
|
|
26
|
+
changes.push(`Qualified legacy attribute '${key}' with raw input root`);
|
|
27
|
+
updated = true;
|
|
28
|
+
}
|
|
29
|
+
if (['string', 'number', 'integer', 'boolean', 'object', 'array', 'null'].includes(attr.type)) {
|
|
30
|
+
parsed.metadataSchema ||= { type: 'object', additionalProperties: true };
|
|
31
|
+
parsed.metadataSchema.properties ||= {};
|
|
32
|
+
parsed.metadataSchema.properties[key] ||= { type: attr.type };
|
|
33
|
+
delete attr.type;
|
|
34
|
+
changes.push(`Moved legacy attribute '${key}' type to metadataSchema`);
|
|
35
|
+
updated = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return updated;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Move only unambiguous local bindings; preserve unsupported/conflicting input.
|
|
43
|
+
* @param {Object} parsed
|
|
44
|
+
* @param {string[]} changes
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function repairLegacyDimensions(parsed, changes) {
|
|
48
|
+
const legacy = parsed.fieldMappings?.dimensions;
|
|
49
|
+
if (!legacy || typeof legacy !== 'object' || Array.isArray(legacy)) return false;
|
|
50
|
+
let updated = false;
|
|
51
|
+
for (const [key, value] of Object.entries(legacy)) {
|
|
52
|
+
const match = typeof value === 'string' && /^metadata\.([a-z][a-zA-Z0-9]*)$/.exec(value);
|
|
53
|
+
if (!match || !Object.hasOwn(parsed.fieldMappings.attributes || {}, match[1])) continue;
|
|
54
|
+
const binding = { type: 'local', field: match[1] };
|
|
55
|
+
const existing = parsed.dimensions?.[key];
|
|
56
|
+
if (existing && (existing.type !== 'local' || existing.field !== match[1])) continue;
|
|
57
|
+
parsed.dimensions ||= {};
|
|
58
|
+
parsed.dimensions[key] ||= binding;
|
|
59
|
+
delete legacy[key];
|
|
60
|
+
changes.push(`Moved legacy dimension '${key}' to root dimensions`);
|
|
61
|
+
updated = true;
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(legacy).length === 0) {
|
|
64
|
+
delete parsed.fieldMappings.dimensions;
|
|
65
|
+
updated = true;
|
|
66
|
+
}
|
|
67
|
+
return updated;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Object} parsed
|
|
72
|
+
* @param {string[]} changes
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
function repairLegacyDatasource(parsed, changes = []) {
|
|
76
|
+
if (!parsed || typeof parsed !== 'object') return false;
|
|
77
|
+
let updated = false;
|
|
78
|
+
if (Object.hasOwn(ENTITY_TYPES, parsed.entityType)) {
|
|
79
|
+
parsed.entityType = ENTITY_TYPES[parsed.entityType];
|
|
80
|
+
changes.push(`Normalized legacy entityType to ${parsed.entityType}`);
|
|
81
|
+
updated = true;
|
|
82
|
+
}
|
|
83
|
+
if (['recordStorage', 'documentStorage'].includes(parsed.entityType) && !parsed.labelKey
|
|
84
|
+
&& parsed.primaryKey?.length === 1
|
|
85
|
+
&& Object.hasOwn(parsed.fieldMappings?.attributes || {}, parsed.primaryKey[0])) {
|
|
86
|
+
parsed.labelKey = [...parsed.primaryKey];
|
|
87
|
+
changes.push('Defaulted missing labelKey to the sole primary key attribute');
|
|
88
|
+
updated = true;
|
|
89
|
+
}
|
|
90
|
+
updated = repairLegacyAttributes(parsed, changes) || updated;
|
|
91
|
+
return repairLegacyDimensions(parsed, changes) || updated;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { repairLegacyDatasource };
|
|
@@ -105,6 +105,26 @@ function readPlatformDeployImageRef(appName) {
|
|
|
105
105
|
* @param {{ quiet?: boolean }} [opts]
|
|
106
106
|
* @returns {Promise<boolean>} True when a pull was attempted
|
|
107
107
|
*/
|
|
108
|
+
/**
|
|
109
|
+
* True when Docker already holds the image.
|
|
110
|
+
*
|
|
111
|
+
* An installation with no registry configured deploys the `application.yaml` image as it
|
|
112
|
+
* stands, which is built locally and never pushed. Authenticating first would prompt for
|
|
113
|
+
* credentials the install does not need, and pulling would fail against a registry that
|
|
114
|
+
* has never seen the tag, so the local image is the answer when it is already there.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} imageRef - Image reference
|
|
117
|
+
* @returns {Promise<boolean>}
|
|
118
|
+
*/
|
|
119
|
+
async function imageExistsLocally(imageRef) {
|
|
120
|
+
try {
|
|
121
|
+
await dockerExec.execWithDockerEnv(`docker image inspect ${imageRef}`);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
async function pullPlatformAppImage(appName, opts = {}) {
|
|
109
129
|
const quiet = Boolean(opts.quiet);
|
|
110
130
|
const imageRef = readPlatformDeployImageRef(appName);
|
|
@@ -114,6 +134,12 @@ async function pullPlatformAppImage(appName, opts = {}) {
|
|
|
114
134
|
}
|
|
115
135
|
return false;
|
|
116
136
|
}
|
|
137
|
+
if (await imageExistsLocally(imageRef)) {
|
|
138
|
+
if (!quiet) {
|
|
139
|
+
logger.log(formatDatasourceListRow('ok', appName, 'image already local'));
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
117
143
|
// A partial stored credential pair fails closed here instead of degrading to a warning.
|
|
118
144
|
await ensureLoginForImageRefs([imageRef]);
|
|
119
145
|
try {
|
|
@@ -58,7 +58,13 @@ const {
|
|
|
58
58
|
} = require('../utils/infra-optional-service-flags');
|
|
59
59
|
|
|
60
60
|
/** Builder app keys touched by `up-platform --force`. */
|
|
61
|
-
const PLATFORM_APPS = [
|
|
61
|
+
const PLATFORM_APPS = [
|
|
62
|
+
'keycloak',
|
|
63
|
+
'miso-controller',
|
|
64
|
+
'dataplane',
|
|
65
|
+
'builder-api',
|
|
66
|
+
'integration-server'
|
|
67
|
+
];
|
|
62
68
|
|
|
63
69
|
function formatBackupSuffix() {
|
|
64
70
|
const d = new Date();
|
|
@@ -290,7 +296,8 @@ async function runUpPlatform(opts = {}) {
|
|
|
290
296
|
base: true,
|
|
291
297
|
platformControllerUrl: authCtx.platformControllerUrl,
|
|
292
298
|
skipLoginIfAuthenticated: authCtx.skipLoginIfAuthenticated,
|
|
293
|
-
requireKeycloakRealm: opts.requireKeycloakRealm === true
|
|
299
|
+
requireKeycloakRealm: opts.requireKeycloakRealm === true,
|
|
300
|
+
requireBaselineReadiness: true
|
|
294
301
|
},
|
|
295
302
|
upMiso.handleUpMiso,
|
|
296
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
|
+
};
|
|
@@ -73,8 +73,42 @@ async function promptSetupPlatformMode(options = {}) {
|
|
|
73
73
|
return normalizeSetupPlatformMode(mode);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Whether this installation carries the integration server.
|
|
78
|
+
*
|
|
79
|
+
* It runs once per stack in the `miso` environment and most installations do not need it,
|
|
80
|
+
* so it is opt-in rather than something every install acquires by default. The answer is
|
|
81
|
+
* persisted, so it is asked once rather than on every run.
|
|
82
|
+
*
|
|
83
|
+
* `--integration-server` and `--no-integration-server` answer it non-interactively; a
|
|
84
|
+
* non-TTY run without either defaults to not installing it, because acquiring an extra
|
|
85
|
+
* application by silence is the wrong default.
|
|
86
|
+
*
|
|
87
|
+
* @async
|
|
88
|
+
* @param {Object} [options] - CLI options
|
|
89
|
+
* @returns {Promise<boolean>}
|
|
90
|
+
*/
|
|
91
|
+
async function promptIntegrationServer(options = {}) {
|
|
92
|
+
if (options.integrationServer === true || options.integrationServer === false) {
|
|
93
|
+
return options.integrationServer;
|
|
94
|
+
}
|
|
95
|
+
if (!process.stdin.isTTY || options.yes === true) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const { integrationServer } = await inquirer.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: 'confirm',
|
|
101
|
+
name: 'integrationServer',
|
|
102
|
+
message: 'Install the integration server (runs once in the miso environment)?',
|
|
103
|
+
default: false
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
return Boolean(integrationServer);
|
|
107
|
+
}
|
|
108
|
+
|
|
76
109
|
module.exports = {
|
|
77
110
|
resolvePlatformModeFromOptions,
|
|
78
111
|
promptSetupPlatformMode,
|
|
112
|
+
promptIntegrationServer,
|
|
79
113
|
getSetupPlatformModePromptChoices
|
|
80
114
|
};
|
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 };
|
|
@@ -110,19 +129,23 @@ async function runSetupWithCliMode(options, cliMode) {
|
|
|
110
129
|
|
|
111
130
|
async function applySetupPlatformMode(options) {
|
|
112
131
|
const mode = await setupPlatformModePrompts.promptSetupPlatformMode(options);
|
|
132
|
+
const integrationServer = await setupPlatformModePrompts.promptIntegrationServer(options);
|
|
113
133
|
const cfg = await config.getConfig();
|
|
114
134
|
applyPlatformTopologyFlagsToConfig(cfg, mode);
|
|
135
|
+
const { onboardingAppsForIntegrationServer } = require('../core/local-env-overrides');
|
|
136
|
+
cfg.onboardingApps = onboardingAppsForIntegrationServer(integrationServer);
|
|
115
137
|
await config.saveConfig(cfg);
|
|
116
138
|
await syncPlatformControllerUrlsInConfig();
|
|
117
139
|
return mode;
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
async function runFreshInstallFlow(options) {
|
|
142
|
+
async function runFreshInstallFlow(options, settingContext) {
|
|
121
143
|
await maybePinDeveloperIdForFreshInstall(options.developer);
|
|
122
144
|
logger.log(infoLine('No infrastructure detected; starting fresh install.'));
|
|
123
145
|
await applySetupPlatformMode(options);
|
|
124
146
|
const adminCreds = await setupPrompts.promptAdminCredentials(options);
|
|
125
147
|
await config.setAdminEmail(adminCreds.adminEmail);
|
|
148
|
+
await persistDimensionAbacSelection(settingContext);
|
|
126
149
|
await setupModes.runFreshInstall(adminCreds);
|
|
127
150
|
logger.log(formatSuccessParagraph('aifabrix setup complete (fresh install).'));
|
|
128
151
|
}
|
|
@@ -131,7 +154,7 @@ async function runFreshInstallFlow(options) {
|
|
|
131
154
|
* @param {boolean} assumeYes
|
|
132
155
|
* @returns {Promise<{ aborted: boolean, mode: string|null }>}
|
|
133
156
|
*/
|
|
134
|
-
async function runExistingInfraFlow(assumeYes) {
|
|
157
|
+
async function runExistingInfraFlow(assumeYes, settingContext) {
|
|
135
158
|
const mode = await setupPrompts.promptModeSelection();
|
|
136
159
|
const proceed = await setupPrompts.confirmDestructiveMode(mode, assumeYes);
|
|
137
160
|
if (!proceed) {
|
|
@@ -142,6 +165,7 @@ async function runExistingInfraFlow(assumeYes) {
|
|
|
142
165
|
if (mode === MODE.REINSTALL || mode === MODE.WIPE_DATA) {
|
|
143
166
|
await applySetupPlatformMode({});
|
|
144
167
|
}
|
|
168
|
+
await persistDimensionAbacSelection(settingContext);
|
|
145
169
|
await dispatchMode(mode);
|
|
146
170
|
logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${mode}).`));
|
|
147
171
|
return { aborted: false, mode };
|
|
@@ -194,20 +218,19 @@ async function dispatchMode(mode) {
|
|
|
194
218
|
* @param {boolean} assumeYes
|
|
195
219
|
* @returns {Promise<{ outcome: string, setupMode: string|null }>}
|
|
196
220
|
*/
|
|
197
|
-
async function runSetupMainPath(options, assumeYes) {
|
|
198
|
-
const cliMode = normalizeCliMode(options);
|
|
221
|
+
async function runSetupMainPath(options, assumeYes, cliMode, settingContext) {
|
|
199
222
|
if (cliMode) {
|
|
200
|
-
return runSetupWithCliMode(options, cliMode);
|
|
223
|
+
return runSetupWithCliMode(options, cliMode, settingContext);
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
logSetupHeader();
|
|
204
227
|
logger.log(formatProgress('Detecting current installation state...'));
|
|
205
228
|
const running = await isInfraRunning();
|
|
206
229
|
if (!running) {
|
|
207
|
-
await runFreshInstallFlow(options);
|
|
230
|
+
await runFreshInstallFlow(options, settingContext);
|
|
208
231
|
return { outcome: 'success', setupMode: 'fresh' };
|
|
209
232
|
}
|
|
210
|
-
const r = await runExistingInfraFlow(assumeYes);
|
|
233
|
+
const r = await runExistingInfraFlow(assumeYes, settingContext);
|
|
211
234
|
const outcome = r.aborted ? 'aborted' : 'success';
|
|
212
235
|
return { outcome, setupMode: r.mode };
|
|
213
236
|
}
|
|
@@ -297,7 +320,9 @@ async function handleSetup(options = {}) {
|
|
|
297
320
|
|
|
298
321
|
beginSetupCommandFlow();
|
|
299
322
|
try {
|
|
300
|
-
const
|
|
323
|
+
const cliMode = normalizeCliMode(options);
|
|
324
|
+
const settingContext = await prepareDimensionAbacSetting(options);
|
|
325
|
+
const main = await runSetupMainPath(options, assumeYes, cliMode, settingContext);
|
|
301
326
|
outcome = main.outcome;
|
|
302
327
|
setupMode = main.setupMode;
|
|
303
328
|
} catch (e) {
|