@aifabrix/builder 2.61.2 → 2.62.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/app/deploy.js +14 -5
- package/lib/app/run-env-compose.js +5 -6
- 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 +30 -10
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +2 -0
- 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 +7 -1
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup.js +3 -0
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +70 -14
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +37 -18
- 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/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/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/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 +3 -2
- 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
|
@@ -202,6 +202,26 @@ async function getApplicationStatus(controllerUrl, envKey, appKey, authConfig) {
|
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Update the authenticated application's own runtime status and URLs.
|
|
207
|
+
* POST /api/v1/environments/{envKey}/applications/self/status
|
|
208
|
+
* @requiresPermission {Controller} Application client token
|
|
209
|
+
* @async
|
|
210
|
+
* @function updateApplicationSelfStatus
|
|
211
|
+
* @param {string} controllerUrl - Controller base URL
|
|
212
|
+
* @param {string} envKey - Environment key
|
|
213
|
+
* @param {Object} authConfig - Client-token authentication configuration
|
|
214
|
+
* @param {Object} statusData - Runtime status payload
|
|
215
|
+
* @returns {Promise<Object>} Updated application response
|
|
216
|
+
* @throws {Error} If request fails
|
|
217
|
+
*/
|
|
218
|
+
async function updateApplicationSelfStatus(controllerUrl, envKey, authConfig, statusData) {
|
|
219
|
+
const client = new ApiClient(controllerUrl, authConfig);
|
|
220
|
+
return await client.post(`/api/v1/environments/${envKey}/applications/self/status`, {
|
|
221
|
+
body: statusData
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
205
225
|
module.exports = {
|
|
206
226
|
listApplications,
|
|
207
227
|
createApplication,
|
|
@@ -211,6 +231,6 @@ module.exports = {
|
|
|
211
231
|
registerApplication,
|
|
212
232
|
bootstrapApplication,
|
|
213
233
|
rotateApplicationSecret,
|
|
214
|
-
getApplicationStatus
|
|
234
|
+
getApplicationStatus,
|
|
235
|
+
updateApplicationSelfStatus
|
|
215
236
|
};
|
|
216
|
-
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /api/v1/auth/bootstrap` — an application's resolved configuration (Miso 257.0).
|
|
3
|
+
*
|
|
4
|
+
* This is what removes the drift. Miso generates an environment's variables through the same
|
|
5
|
+
* catalog and reconciliation it uses for Azure, holds them in whichever store the deployment
|
|
6
|
+
* calls for — Key Vault, or encrypted in `Environment.configuration` for a local install —
|
|
7
|
+
* and hands back the resolved set. Builder stops generating values and stops deriving secret
|
|
8
|
+
* names, so there is one generator, one naming rule and one record.
|
|
9
|
+
*
|
|
10
|
+
* **The caller is the application, not the operator.** A client id and secret are provisioned
|
|
11
|
+
* with the application itself, so there is no chicken-and-egg: they exchange for a client
|
|
12
|
+
* token at `/api/v1/auth/token`, which needs no prior authentication. The token carries the
|
|
13
|
+
* application and environment, which means the snapshot is server-assigned — an application
|
|
14
|
+
* cannot ask for another application's configuration, because it cannot mint a token naming
|
|
15
|
+
* one. Builder passes credentials it already holds and is told what that application gets.
|
|
16
|
+
*
|
|
17
|
+
* **`success === true` is load-bearing.** Miso writes this envelope out explicitly rather than
|
|
18
|
+
* through the helper every neighbouring route uses, because that helper omits `success` and a
|
|
19
|
+
* consumer treats its absence as a protocol error. Checking it here keeps Builder honest with
|
|
20
|
+
* the same contract the SDK enforces.
|
|
21
|
+
*
|
|
22
|
+
* @fileoverview Fetch an application's bootstrap configuration snapshot from Miso
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
/** The only protocol version the endpoint accepts. */
|
|
28
|
+
const BOOTSTRAP_PROTOCOL_VERSION = 1;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} controllerUrl - Controller base URL
|
|
32
|
+
* @returns {string} Without a trailing slash
|
|
33
|
+
*/
|
|
34
|
+
function normalizeControllerUrl(controllerUrl) {
|
|
35
|
+
return String(controllerUrl || '').trim().replace(/\/+$/, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Exchange an application's client credentials for a client token.
|
|
40
|
+
*
|
|
41
|
+
* @async
|
|
42
|
+
* @param {string} controllerUrl - Controller base URL
|
|
43
|
+
* @param {{ clientId: string, clientSecret: string }} credentials - Application credentials
|
|
44
|
+
* @param {Function} [makeApiCall] - Injected transport
|
|
45
|
+
* @returns {Promise<string>} The client token
|
|
46
|
+
* @throws {Error} When the exchange fails or returns no token
|
|
47
|
+
*/
|
|
48
|
+
async function exchangeClientToken(controllerUrl, credentials, makeApiCall) {
|
|
49
|
+
const call = makeApiCall || require('../utils/api').makeApiCall;
|
|
50
|
+
const response = await call(`${normalizeControllerUrl(controllerUrl)}/api/v1/auth/token`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'x-client-id': credentials.clientId,
|
|
55
|
+
'x-client-secret': credentials.clientSecret
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (!response || response.success !== true) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Could not authenticate as ${credentials.clientId}: ${(response && response.error) || 'request failed'}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const data = response.data || {};
|
|
64
|
+
const token = data.token || data.data?.token || data.accessToken;
|
|
65
|
+
if (!token) {
|
|
66
|
+
// Names the client, never the secret, because this message reaches the terminal.
|
|
67
|
+
throw new Error(`Authentication for ${credentials.clientId} returned no token`);
|
|
68
|
+
}
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fetch an application's resolved configuration for its environment.
|
|
74
|
+
*
|
|
75
|
+
* @async
|
|
76
|
+
* @param {string} controllerUrl - Controller base URL
|
|
77
|
+
* @param {{ clientId: string, clientSecret: string }} credentials - Application credentials
|
|
78
|
+
* @param {Object} [deps] - Injected collaborators
|
|
79
|
+
* @returns {Promise<{ configuration: Object, context: Object, issuedAt: string }>} The snapshot
|
|
80
|
+
* @throws {Error} When authentication fails, or the snapshot is refused or malformed
|
|
81
|
+
*/
|
|
82
|
+
async function fetchBootstrapSnapshot(controllerUrl, credentials, deps = {}) {
|
|
83
|
+
if (!credentials || !credentials.clientId || !credentials.clientSecret) {
|
|
84
|
+
throw new Error('Client id and secret are required to fetch a bootstrap snapshot');
|
|
85
|
+
}
|
|
86
|
+
const call = deps.makeApiCall || require('../utils/api').makeApiCall;
|
|
87
|
+
const token = await exchangeClientToken(controllerUrl, credentials, call);
|
|
88
|
+
const response = await call(`${normalizeControllerUrl(controllerUrl)}/api/v1/auth/bootstrap`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
Authorization: `Bearer ${token}`
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({ protocolVersion: BOOTSTRAP_PROTOCOL_VERSION })
|
|
95
|
+
});
|
|
96
|
+
if (!response || response.success !== true) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Miso refused a configuration snapshot for ${credentials.clientId}: ` +
|
|
99
|
+
`${(response && response.error) || 'request failed'}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const snapshot = response.data && response.data.data ? response.data.data : response.data;
|
|
103
|
+
if (!snapshot || typeof snapshot.configuration !== 'object' || snapshot.configuration === null) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Miso returned a snapshot with no configuration for ${credentials.clientId}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return snapshot;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
BOOTSTRAP_PROTOCOL_VERSION,
|
|
113
|
+
exchangeClientToken,
|
|
114
|
+
fetchBootstrapSnapshot
|
|
115
|
+
};
|
package/lib/app/deploy.js
CHANGED
|
@@ -191,12 +191,21 @@ async function handleDeploymentError(error, appName, controllerUrl, usedExternal
|
|
|
191
191
|
* @throws {Error} If image is missing or has no registry
|
|
192
192
|
*/
|
|
193
193
|
function validateImageIsPullable(imageRef, appName) {
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
if (imageRef && imageRef.includes('/')) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// No registry configured means the installation deploys the application.yaml image as it
|
|
198
|
+
// stands, from the local Docker daemon. Demanding a registry component here would make a
|
|
199
|
+
// local install impossible for the one case it is meant to serve. A configured registry
|
|
200
|
+
// still has to appear in the reference, because then the image is expected to be pullable.
|
|
201
|
+
const { readInstallationRegistrySync } = require('../utils/config-registry-preference');
|
|
202
|
+
if (!readInstallationRegistrySync()) {
|
|
203
|
+
return;
|
|
199
204
|
}
|
|
205
|
+
const hint = `Set image.registry and image.tag in builder/${appName}/application.yaml, or pass a full image ref (e.g. --image <registry>/${appName}:<tag>) when deploying`;
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Deployed image must be pullable (include a registry). Current image: "${imageRef || 'none'}". ${hint}`
|
|
208
|
+
);
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
/**
|
|
@@ -13,6 +13,7 @@ const path = require('path');
|
|
|
13
13
|
const fs = require('fs').promises;
|
|
14
14
|
const fsSync = require('fs');
|
|
15
15
|
const pathsUtil = require('../utils/paths');
|
|
16
|
+
const { isScopeActiveForOptions } = require('../utils/environment-scoped-resources');
|
|
16
17
|
const adminSecrets = require('../core/admin-secrets');
|
|
17
18
|
const config = require('../core/config');
|
|
18
19
|
const secretsEnsure = require('../core/secrets-ensure');
|
|
@@ -25,8 +26,8 @@ const { applyAdminSecretsPlatformOverlay } = require('../core/admin-secrets-env-
|
|
|
25
26
|
* @param {Object.<string, string>} merged
|
|
26
27
|
* @param {Object.<string, string>} adminObj
|
|
27
28
|
*/
|
|
28
|
-
function mergeRunEnvWithAdminOverlay(merged, adminObj) {
|
|
29
|
-
applyAdminSecretsPlatformOverlay(merged, adminObj);
|
|
29
|
+
function mergeRunEnvWithAdminOverlay(merged, adminObj, appName) {
|
|
30
|
+
applyAdminSecretsPlatformOverlay(merged, adminObj, appName);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async function applyOnboardingPlatformModeIfNeeded(appName, merged) {
|
|
@@ -108,9 +109,7 @@ function injectDatabaseNamesAndUsers(env, appConfig, scopeOpts = null) {
|
|
|
108
109
|
if (!Array.isArray(databases) || databases.length === 0) return;
|
|
109
110
|
const prefix =
|
|
110
111
|
scopeOpts &&
|
|
111
|
-
scopeOpts
|
|
112
|
-
scopeOpts.runEnvKey &&
|
|
113
|
-
(scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
|
|
112
|
+
isScopeActiveForOptions(scopeOpts)
|
|
114
113
|
? `${String(scopeOpts.runEnvKey).toLowerCase()}-`
|
|
115
114
|
: '';
|
|
116
115
|
for (let i = 0; i < databases.length; i++) {
|
|
@@ -298,7 +297,7 @@ async function buildMergedRunEnvAndWrite(appName, appConfig, devDir, developerId
|
|
|
298
297
|
runEnvKey
|
|
299
298
|
});
|
|
300
299
|
const merged = { ...adminObj, ...appObj };
|
|
301
|
-
mergeRunEnvWithAdminOverlay(merged, adminObj);
|
|
300
|
+
mergeRunEnvWithAdminOverlay(merged, adminObj, appName);
|
|
302
301
|
await applyOnboardingPlatformModeIfNeeded(appName, merged);
|
|
303
302
|
await applyPostgresPasswordFromPgpass(merged, developerId);
|
|
304
303
|
injectDatabaseNamesAndUsers(merged, appConfig, scopeOpts);
|
package/lib/app/run.js
CHANGED
|
@@ -258,10 +258,12 @@ function applyTraefikFlagToRunOptions(runOptions, userCfg) {
|
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
260
|
* @param {string} envKey
|
|
261
|
-
* @
|
|
261
|
+
* @param {string} appName
|
|
262
|
+
* @throws {Error} If env is not a supported workload environment or a miso-only platform app
|
|
262
263
|
*/
|
|
263
|
-
function assertValidRunEnvKey(envKey) {
|
|
264
|
-
|
|
264
|
+
function assertValidRunEnvKey(envKey, appName) {
|
|
265
|
+
const misoOnlyPlatformApp = appName === 'builder-api' || appName === 'integration-server';
|
|
266
|
+
if (envKey !== 'dev' && envKey !== 'tst' && envKey !== 'pro' && !(envKey === 'miso' && misoOnlyPlatformApp)) {
|
|
265
267
|
throw new Error('--env must be dev, tst, or pro');
|
|
266
268
|
}
|
|
267
269
|
}
|
|
@@ -345,7 +347,7 @@ function emitRunManifestMetadataIfRequested(appName, options) {
|
|
|
345
347
|
*/
|
|
346
348
|
async function prepareAppRun(appName, options, debug) {
|
|
347
349
|
const envKey = (options.env || 'dev').toLowerCase();
|
|
348
|
-
assertValidRunEnvKey(envKey);
|
|
350
|
+
assertValidRunEnvKey(envKey, appName);
|
|
349
351
|
const shouldContinue = await validateAppForRun(appName, debug);
|
|
350
352
|
if (!shouldContinue) {
|
|
351
353
|
return null;
|
|
@@ -104,6 +104,10 @@ async function executeDockerImageBuild(appName, options, buildConfig) {
|
|
|
104
104
|
* @returns {Promise<string>} Built image ref e.g. name:tag
|
|
105
105
|
*/
|
|
106
106
|
async function runStandardDockerBuild(appName, options) {
|
|
107
|
+
// Before the tag plan, the promote shortcut and any Docker call: every build path reaches
|
|
108
|
+
// here, and a build into tst or pro must not get as far as producing an artifact.
|
|
109
|
+
const { assertBuildEnvironmentForOptions } = require('../core/environment-mode-policy');
|
|
110
|
+
await assertBuildEnvironmentForOptions(options);
|
|
107
111
|
const { resolveBuildTagPlan, readAppVersion } = require('../utils/image-tags');
|
|
108
112
|
const { buildConfig, config: appConfig } = await buildHelpers.loadAndValidateConfig(appName);
|
|
109
113
|
// Resolve and validate every tag before any Docker build, tag, login or promote.
|
|
@@ -92,7 +92,7 @@ function renderTemplateFile(templatePath, context) {
|
|
|
92
92
|
* @param {boolean} dryRun
|
|
93
93
|
* @returns {{ installPath: string, content: string }|null}
|
|
94
94
|
*/
|
|
95
|
-
function renderInstallGuideIfPresent(templatesDir, runtimeValues, outputDir, dryRun) {
|
|
95
|
+
function renderInstallGuideIfPresent(templatesDir, runtimeValues, outputDir, dryRun, durableDir) {
|
|
96
96
|
const installTemplate = path.join(templatesDir, 'install.template.md.hbs');
|
|
97
97
|
if (!fs.existsSync(installTemplate)) {
|
|
98
98
|
return null;
|
|
@@ -100,11 +100,19 @@ function renderInstallGuideIfPresent(templatesDir, runtimeValues, outputDir, dry
|
|
|
100
100
|
const content = renderTemplateFile(installTemplate, runtimeValues);
|
|
101
101
|
assertNoSecretsInArtifact(content);
|
|
102
102
|
const installPath = path.join(outputDir, 'INSTALL.md');
|
|
103
|
+
// `outputDir` is under gitignored dist/, so a copy there alone is lost on any clean and the
|
|
104
|
+
// API that serves this document 404s afterwards. Write the durable copy too when the caller
|
|
105
|
+
// knows where the channel's committed files live.
|
|
106
|
+
const durablePath = durableDir ? path.join(durableDir, 'INSTALL.md') : '';
|
|
103
107
|
if (!dryRun) {
|
|
104
108
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
105
109
|
fs.writeFileSync(installPath, content, 'utf8');
|
|
110
|
+
if (durablePath) {
|
|
111
|
+
fs.mkdirSync(durableDir, { recursive: true });
|
|
112
|
+
fs.writeFileSync(durablePath, content, 'utf8');
|
|
113
|
+
}
|
|
106
114
|
}
|
|
107
|
-
return { installPath, content };
|
|
115
|
+
return { installPath, content, durablePath: durablePath || undefined };
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
/**
|
|
@@ -130,6 +138,7 @@ function generateCopilotChannelArtifact(params, templatesDir, outputPath) {
|
|
|
130
138
|
* @param {string} params.channelTarget
|
|
131
139
|
* @param {Object} params.runtimeValues
|
|
132
140
|
* @param {boolean} [params.dryRun]
|
|
141
|
+
* @param {string} [params.durableDir] - Committed channel directory for the persisted INSTALL.md
|
|
133
142
|
* @returns {{ outputPath: string, artifactHash: string, content: string, installPath?: string }}
|
|
134
143
|
*/
|
|
135
144
|
function generateChannelArtifact(params) {
|
|
@@ -158,7 +167,8 @@ function generateChannelArtifact(params) {
|
|
|
158
167
|
templatesDir,
|
|
159
168
|
runtimeValues,
|
|
160
169
|
outputDir,
|
|
161
|
-
params.dryRun === true
|
|
170
|
+
params.dryRun === true,
|
|
171
|
+
params.durableDir
|
|
162
172
|
);
|
|
163
173
|
|
|
164
174
|
const installReady = runtimeValues?.installReady === true;
|
|
@@ -46,11 +46,12 @@ async function resolvePlatformControllerUrlForDisplay() {
|
|
|
46
46
|
return resolvePlatformControllerUrl();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function logPlatformReadyFooter() {
|
|
50
|
-
const [controllerUrl, dataplaneUrl, keycloakUrl] = await Promise.all([
|
|
49
|
+
async function logPlatformReadyFooter(options = {}) {
|
|
50
|
+
const [controllerUrl, dataplaneUrl, keycloakUrl, integrationServerUrl] = await Promise.all([
|
|
51
51
|
resolvePlatformControllerUrlForDisplay(),
|
|
52
52
|
computeAppBaseUrl('dataplane'),
|
|
53
|
-
computeAppBaseUrl('keycloak')
|
|
53
|
+
computeAppBaseUrl('keycloak'),
|
|
54
|
+
options.integrationServer === true ? computeAppBaseUrl('integration-server') : null
|
|
54
55
|
]);
|
|
55
56
|
const cfg = await config.getConfig();
|
|
56
57
|
const env = cfg && cfg.environment ? cfg.environment : 'dev';
|
|
@@ -60,7 +61,7 @@ async function logPlatformReadyFooter() {
|
|
|
60
61
|
logSection('Miso Controller', controllerUrl);
|
|
61
62
|
logSection('Dataplane (DEV)', dataplaneUrl);
|
|
62
63
|
logSection('Keycloak', keycloakUrl);
|
|
63
|
-
logSection('
|
|
64
|
+
if (integrationServerUrl) logSection('Integration Server', `${integrationServerUrl}/download`);
|
|
64
65
|
logSection('Knowledge Base', 'https://docs.aifabrix.ai');
|
|
65
66
|
logSection('Getting Started', 'https://docs.aifabrix.ai/get-started');
|
|
66
67
|
logFooterEnd();
|
|
@@ -91,20 +92,21 @@ async function logDataplaneReadyFooter() {
|
|
|
91
92
|
/**
|
|
92
93
|
* @param {'platform'|'dataplane'} [scope]
|
|
93
94
|
*/
|
|
94
|
-
function logPlatformSetupDurationHint(scope = 'platform') {
|
|
95
|
+
function logPlatformSetupDurationHint(scope = 'platform', options = {}) {
|
|
96
|
+
const optionalApp = options.integrationServer === true ? ', Builder API, and Integration Server' : ', and Builder API';
|
|
95
97
|
const line =
|
|
96
98
|
scope === 'dataplane'
|
|
97
99
|
? 'Installation may take a few minutes. Authenticating and starting Dataplane — please wait.'
|
|
98
|
-
:
|
|
100
|
+
: `Installation may take a few minutes. Keycloak, Miso Controller, Dataplane${optionalApp} are starting — please wait.`;
|
|
99
101
|
logger.log(chalk.yellow(`⚠ ${line}`));
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
function logGuidedPlatformSetupHeader() {
|
|
104
|
+
function logGuidedPlatformSetupHeader(options = {}) {
|
|
103
105
|
logger.log('');
|
|
104
106
|
logger.log(title('AI Fabrix Platform Setup'));
|
|
105
107
|
logger.log(SEPARATOR);
|
|
106
108
|
logger.log('');
|
|
107
|
-
logPlatformSetupDurationHint('platform');
|
|
109
|
+
logPlatformSetupDurationHint('platform', options);
|
|
108
110
|
logger.log('');
|
|
109
111
|
}
|
|
110
112
|
|
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,
|
|
@@ -295,8 +299,25 @@ async function runGuidedDataplaneInstallPhase(options, handleUpDataplane, handle
|
|
|
295
299
|
await refreshDataplaneVersionAfterPlatformInstall();
|
|
296
300
|
}
|
|
297
301
|
|
|
302
|
+
async function runGuidedIntegrationServerPhase(options, selected) {
|
|
303
|
+
if (!selected) return;
|
|
304
|
+
const spin = startSpinner('Starting Integration Server...');
|
|
305
|
+
await withMutedLogger(() => handleUpIntegrationServer({ ...options, platformInstall: true }));
|
|
306
|
+
stopSpinnerSuccess(spin, 'Integration Server ready');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function runGuidedKeycloakRealmPhase(options) {
|
|
310
|
+
if (!options || options.requireKeycloakRealm !== true) return;
|
|
311
|
+
const { waitForKeycloakRealmReady } = require('../utils/keycloak-realm-ready');
|
|
312
|
+
const spin = startSpinner('Waiting for Keycloak realm (aifabrix)...');
|
|
313
|
+
await waitForKeycloakRealmReady({ timeoutSeconds: 180 });
|
|
314
|
+
stopSpinnerSuccess(spin, 'Keycloak realm ready');
|
|
315
|
+
logger.log('');
|
|
316
|
+
}
|
|
317
|
+
|
|
298
318
|
async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, handleLogin, forceCleanSummary = null) {
|
|
299
|
-
|
|
319
|
+
const integrationServerSelected = await isIntegrationServerSelected();
|
|
320
|
+
logGuidedPlatformSetupHeader({ integrationServer: integrationServerSelected });
|
|
300
321
|
if (forceCleanSummary && forceCleanSummary.forceSummary) {
|
|
301
322
|
logUpPlatformForceCleanSummary(forceCleanSummary.forceSummary, forceCleanSummary.cleanedApps);
|
|
302
323
|
}
|
|
@@ -308,21 +329,20 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
|
|
|
308
329
|
|
|
309
330
|
await withMutedLogger(() => prepareUrlsLocalRegistryForUpPlatform());
|
|
310
331
|
await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
|
|
311
|
-
|
|
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
|
-
}
|
|
332
|
+
await runGuidedKeycloakRealmPhase(options);
|
|
318
333
|
await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
|
|
319
334
|
const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
|
|
320
335
|
await runGuidedDataplaneInstallPhase(options, handleUpDataplane, handleLogin, controllerUrl);
|
|
321
336
|
const baSpin = startSpinner('Starting Builder API...');
|
|
322
337
|
await withMutedLogger(() => handleUpBuilderApi({ ...options, platformInstall: true }));
|
|
323
338
|
stopSpinnerSuccess(baSpin, 'Builder API ready');
|
|
324
|
-
await
|
|
325
|
-
|
|
339
|
+
await runGuidedIntegrationServerPhase(options, integrationServerSelected);
|
|
340
|
+
const appNames = [
|
|
341
|
+
'miso-controller', 'dataplane', 'builder-api',
|
|
342
|
+
...(integrationServerSelected ? ['integration-server'] : [])
|
|
343
|
+
];
|
|
344
|
+
await validateAppErrorLogs(appNames);
|
|
345
|
+
await logPlatformReadyFooter({ integrationServer: integrationServerSelected });
|
|
326
346
|
}
|
|
327
347
|
|
|
328
348
|
async function runGuidedDownInfra(appName, options, infraMod, appLib) {
|
|
@@ -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
|
}
|
|
@@ -61,6 +61,8 @@ function registerSetup(program) {
|
|
|
61
61
|
.option('--pro-password-mode <mode>', 'Pro profile only: autogen or manual')
|
|
62
62
|
.option('--admin-email <email>', 'Admin email (non-interactive fresh install)')
|
|
63
63
|
.option('--admin-password <password>', 'Admin password for dev or pro manual-single (non-interactive)')
|
|
64
|
+
.option('--integration-server', 'Install the integration server (runs once in the miso environment)')
|
|
65
|
+
.option('--no-integration-server', 'Do not install the integration server (default)')
|
|
64
66
|
.option('--mode <mode>', 'Non-interactive mode: fresh (no infra) or update-images (infra running)')
|
|
65
67
|
.option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
|
|
66
68
|
.addHelpText('after', SETUP_HELP_AFTER)
|
|
@@ -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 {
|