@wtfalch/foundry 0.16.5 → 0.16.7
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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/platform-diff.d.ts +7 -0
- package/dist/plan/platform-diff.js +28 -2
- package/dist/plan/platform-providers.js +97 -7
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
|
23
23
|
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, } from './plan/index.js';
|
|
24
24
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
25
25
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
26
|
+
export type { ZitadelListAppsRawApp } from './plan/index.js';
|
|
27
|
+
export { adaptZitadelListApps } from './plan/index.js';
|
|
26
28
|
export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js';
|
|
27
29
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
28
30
|
export type { MailAllowlistApp } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesF
|
|
|
10
10
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
11
11
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
12
12
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
13
|
+
export { adaptZitadelListApps } from './plan/index.js';
|
|
13
14
|
export { checkMailIdentityAudience } from './plan/index.js';
|
|
14
15
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
15
16
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -35,4 +35,6 @@ export type { MailAllowlistApp } from './mail-identity-allowlist.js';
|
|
|
35
35
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
36
36
|
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, } from './executor.js';
|
|
37
37
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
38
|
+
export type { ZitadelListAppsRawApp } from './executor.js';
|
|
39
|
+
export { adaptZitadelListApps } from './executor.js';
|
|
38
40
|
export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -18,4 +18,5 @@ export { loginHostnameFor } from './login-host.js';
|
|
|
18
18
|
export { checkMailIdentityAudience } from './mail-identity.js';
|
|
19
19
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
20
20
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
21
|
+
export { adaptZitadelListApps } from './executor.js';
|
|
21
22
|
export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
|
|
@@ -466,6 +466,13 @@ export declare const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = "ai_platform_migrat
|
|
|
466
466
|
* that throws) is repaired by the next one.
|
|
467
467
|
*/
|
|
468
468
|
export declare const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = "ai_platform_deploy_pending";
|
|
469
|
+
/**
|
|
470
|
+
* The Coolify deployment queued for the host (its `deployment_uuid`, or
|
|
471
|
+
* `untracked` when the deploy answered none), set when that deploy is queued
|
|
472
|
+
* and cleared once it finished. The mint waits for it, so a retry after a
|
|
473
|
+
* failed wait still waits instead of minting against the old container.
|
|
474
|
+
*/
|
|
475
|
+
export declare const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = "ai_platform_runner_deployment";
|
|
469
476
|
/**
|
|
470
477
|
* #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
|
|
471
478
|
* dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
|
|
@@ -523,6 +523,13 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
|
|
|
523
523
|
* that throws) is repaired by the next one.
|
|
524
524
|
*/
|
|
525
525
|
export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
|
|
526
|
+
/**
|
|
527
|
+
* The Coolify deployment queued for the host (its `deployment_uuid`, or
|
|
528
|
+
* `untracked` when the deploy answered none), set when that deploy is queued
|
|
529
|
+
* and cleared once it finished. The mint waits for it, so a retry after a
|
|
530
|
+
* failed wait still waits instead of minting against the old container.
|
|
531
|
+
*/
|
|
532
|
+
export const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = 'ai_platform_runner_deployment';
|
|
526
533
|
/**
|
|
527
534
|
* #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
|
|
528
535
|
* dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
|
|
@@ -1042,6 +1049,10 @@ function planSfu(platform) {
|
|
|
1042
1049
|
* the broker's own token as part of running this step, and a step that is
|
|
1043
1050
|
* never planned never runs. So this still plans the step -- surfacing
|
|
1044
1051
|
* `executorBlocked` in `blocked` for visibility, not to stop planning.
|
|
1052
|
+
* Exception (the auth-broker loop): once everything else is applied and
|
|
1053
|
+
* `otherInputsMissing` is non-empty, nothing this step can do changes the
|
|
1054
|
+
* missing inputs, so it is reported blocked and not planned -- else every
|
|
1055
|
+
* `driveApply` round replans it until `maxRounds`.
|
|
1045
1056
|
*
|
|
1046
1057
|
* #211 round 6: round 3's `!brokerApplied` guard assumed "no executor" and
|
|
1047
1058
|
* "broker not yet created" always coincide. They don't: a broker recorded on
|
|
@@ -1152,10 +1163,25 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
1152
1163
|
// #211 round 3: a blocked reason with no broker yet created also means the
|
|
1153
1164
|
// executor is unusable (no auth dependencies) -- nothing to plan.
|
|
1154
1165
|
// A blocked reason with the broker already created means only its deploy
|
|
1155
|
-
// is held back; plan the step anyway so it can
|
|
1156
|
-
// #
|
|
1166
|
+
// is held back; plan the step anyway, so it can mint what it can (#209) and,
|
|
1167
|
+
// once #210 lands, deploy on the very next apply.
|
|
1157
1168
|
if (executorBlocked && !brokerApplied)
|
|
1158
1169
|
return { step: null, blocked: executorBlocked };
|
|
1170
|
+
// Loop fix: everything but the broker deploy is applied and that deploy
|
|
1171
|
+
// waits on an input (`otherInputsMissing`, never the token this step mints)
|
|
1172
|
+
// that another run supplies -- re-applying changes nothing, so the step
|
|
1173
|
+
// planned and applied every `driveApply` round, with dozens of Coolify
|
|
1174
|
+
// calls each, until `maxRounds`. Report it blocked and plan nothing; the
|
|
1175
|
+
// next plan re-reads the live inputs, so it plans the step once they exist.
|
|
1176
|
+
// The login client is not required here: `provisionPlatformAuth` mints it
|
|
1177
|
+
// only once those same inputs exist, so it is never recorded before then.
|
|
1178
|
+
if (executorBlocked &&
|
|
1179
|
+
otherInputsMissing.length > 0 &&
|
|
1180
|
+
recordedIds[key] &&
|
|
1181
|
+
connectorApplied &&
|
|
1182
|
+
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS) {
|
|
1183
|
+
return { step: null, blocked: executorBlocked };
|
|
1184
|
+
}
|
|
1159
1185
|
return {
|
|
1160
1186
|
step: {
|
|
1161
1187
|
action: {
|
|
@@ -6,7 +6,7 @@ import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
|
6
6
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
7
7
|
import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
|
|
8
8
|
import { coolifyDomains } from './hostnames.js';
|
|
9
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
|
|
9
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
|
|
10
10
|
import { applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
11
11
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
12
12
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
@@ -3147,16 +3147,18 @@ const PLATFORM_AI_WORKER_SCOPE_KEY = 'coolify_platform_ai_worker_app';
|
|
|
3147
3147
|
* Written only when absent, never overwritten: an operator's own model list
|
|
3148
3148
|
* or worker bounds survive every apply. Values are `.env.example`'s, and
|
|
3149
3149
|
* `AI_MODELS=[]` is the README's "honest configuration for a host with no
|
|
3150
|
-
* approved text model".
|
|
3150
|
+
* approved text model". The runner needs `AI_WORKER_TIMEOUT_MS` too: ai's
|
|
3151
|
+
* `runtimeHost()` (`apps/host/src/lib/authz/ai-host.ts`) requires it, and
|
|
3152
|
+
* every route that checks readiness fails without it.
|
|
3151
3153
|
*/
|
|
3152
3154
|
const PLATFORM_AI_RUNNER_DEFAULTS = [
|
|
3153
3155
|
{ key: 'AI_MODELS', value: '[]' },
|
|
3154
3156
|
{ key: 'AI_FORGE_BINDINGS', value: '[]' },
|
|
3157
|
+
{ key: 'AI_WORKER_TIMEOUT_MS', value: '60000' },
|
|
3155
3158
|
];
|
|
3156
3159
|
const PLATFORM_AI_WORKER_DEFAULTS = [
|
|
3157
3160
|
...PLATFORM_AI_RUNNER_DEFAULTS,
|
|
3158
3161
|
{ key: 'AI_WORKER_BATCH', value: '10' },
|
|
3159
|
-
{ key: 'AI_WORKER_TIMEOUT_MS', value: '60000' },
|
|
3160
3162
|
];
|
|
3161
3163
|
/**
|
|
3162
3164
|
* #172: makes `coolify:app` keyed {@link PLATFORM_AI_KEY} true -- the ai host
|
|
@@ -3402,8 +3404,12 @@ async function provisionPlatformAi(context, deps) {
|
|
|
3402
3404
|
}
|
|
3403
3405
|
await coolify.raw('POST', `/applications/${migrate.appId}/stop`);
|
|
3404
3406
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
+
// `deploy` only queues a build that takes minutes, so the id of the host's
|
|
3408
|
+
// deployment is kept for the mint below to wait on.
|
|
3409
|
+
const runnerDeployment = deployRunner ? await coolify.deploy(runner.appId) : undefined;
|
|
3410
|
+
if (deployRunner) {
|
|
3411
|
+
await record(PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, queuedDeploymentUuid(runnerDeployment) ?? 'untracked');
|
|
3412
|
+
}
|
|
3407
3413
|
if (deployWorker)
|
|
3408
3414
|
await coolify.deploy(worker.appId);
|
|
3409
3415
|
await record(PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, '');
|
|
@@ -3422,6 +3428,25 @@ async function provisionPlatformAi(context, deps) {
|
|
|
3422
3428
|
if (lost) {
|
|
3423
3429
|
throw new Error(`platform:ai: AI management key ${lost} was minted but never delivered; revoke it and remove the recorded key ${PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY} to mint a new one`);
|
|
3424
3430
|
}
|
|
3431
|
+
// The host answers 503 (or nothing) until the deployment that carries its
|
|
3432
|
+
// management settings has finished.
|
|
3433
|
+
const queued = context.recordedIds()[PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY];
|
|
3434
|
+
if (queued) {
|
|
3435
|
+
try {
|
|
3436
|
+
await waitForAiDeployment(coolify, runner.appId, queued, runtime, sleep);
|
|
3437
|
+
}
|
|
3438
|
+
catch (error) {
|
|
3439
|
+
// Only a build still running is worth waiting on again. A deployment
|
|
3440
|
+
// that failed, was cancelled or cannot be read is not: mark the host
|
|
3441
|
+
// pending so the next apply queues a new one instead of polling it.
|
|
3442
|
+
if (!(error instanceof AiDeploymentTimeoutError)) {
|
|
3443
|
+
await record(PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, '1');
|
|
3444
|
+
await record(PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, '');
|
|
3445
|
+
}
|
|
3446
|
+
throw error;
|
|
3447
|
+
}
|
|
3448
|
+
await record(PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, '');
|
|
3449
|
+
}
|
|
3425
3450
|
// #201: a test injects its own minter; without one, build the real one
|
|
3426
3451
|
// over ai's own POST /v1/platform/keys, signed with the key pinned above.
|
|
3427
3452
|
const minter = deps.managementKeyMinter ??
|
|
@@ -3459,8 +3484,9 @@ async function provisionPlatformAi(context, deps) {
|
|
|
3459
3484
|
if (error instanceof AiManagementKeyUntrustedError) {
|
|
3460
3485
|
throw new Error(`platform:ai: ${error.message}`);
|
|
3461
3486
|
}
|
|
3462
|
-
// The cause is dropped in case it echoes a credential
|
|
3463
|
-
|
|
3487
|
+
// The cause is dropped in case it echoes a credential; only the HTTP
|
|
3488
|
+
// status, or a network error's name and code, say why.
|
|
3489
|
+
throw new Error(`platform:ai: the AI management key could not be minted (${describeMintFailure(error)}). Nothing was recorded; apply again`);
|
|
3464
3490
|
}
|
|
3465
3491
|
const { credentialId, organisationId, secret } = minted;
|
|
3466
3492
|
await record(PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, credentialId);
|
|
@@ -3549,6 +3575,70 @@ class MigrationNotHealthyError extends Error {
|
|
|
3549
3575
|
this.lastStatus = lastStatus;
|
|
3550
3576
|
}
|
|
3551
3577
|
}
|
|
3578
|
+
/** The `deployment_uuid` in the `{deployments:[...]}` / array answer of Coolify's deploy call, if any. */
|
|
3579
|
+
function queuedDeploymentUuid(result) {
|
|
3580
|
+
const rows = Array.isArray(result)
|
|
3581
|
+
? result
|
|
3582
|
+
: result?.deployments;
|
|
3583
|
+
const uuid = Array.isArray(rows)
|
|
3584
|
+
? rows[0]?.deployment_uuid
|
|
3585
|
+
: undefined;
|
|
3586
|
+
return typeof uuid === 'string' && uuid ? uuid : undefined;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Waits until the host's queued deployment has `finished`; throws on
|
|
3590
|
+
* `failed`/`cancelled-by-user` or once `migrateTimeoutMs` has passed. With no
|
|
3591
|
+
* deployment id (`untracked`) the only signal left is the app's own status,
|
|
3592
|
+
* which an old container can already satisfy, so that is best effort.
|
|
3593
|
+
*/
|
|
3594
|
+
/** The host's deployment was still running when the wait gave up. */
|
|
3595
|
+
class AiDeploymentTimeoutError extends Error {
|
|
3596
|
+
}
|
|
3597
|
+
async function waitForAiDeployment(coolify, appId, deploymentUuid, runtime, sleep) {
|
|
3598
|
+
const now = runtime.now ?? Date.now;
|
|
3599
|
+
const timeout = runtime.migrateTimeoutMs ?? 600_000;
|
|
3600
|
+
const interval = runtime.migrateIntervalMs ?? 5_000;
|
|
3601
|
+
const deadline = now() + timeout;
|
|
3602
|
+
let status;
|
|
3603
|
+
for (;;) {
|
|
3604
|
+
await sleep(interval);
|
|
3605
|
+
if (deploymentUuid === 'untracked') {
|
|
3606
|
+
status = (await coolify.getApplication(appId)).status;
|
|
3607
|
+
if (status?.startsWith('running:healthy'))
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
else {
|
|
3611
|
+
const found = (await coolify.raw('GET', `/deployments/${deploymentUuid}`));
|
|
3612
|
+
status = typeof found?.status === 'string' ? found.status : undefined;
|
|
3613
|
+
if (status === 'finished')
|
|
3614
|
+
return;
|
|
3615
|
+
if (status === 'failed' || status === 'cancelled-by-user') {
|
|
3616
|
+
throw new Error(`platform:ai: the host's deployment for ${appId} ended ${status}; the AI management key was not minted. Check its build log in Coolify, then apply again`);
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
if (now() >= deadline) {
|
|
3620
|
+
throw new AiDeploymentTimeoutError(`platform:ai: the host's deployment for ${appId} did not finish within ${Math.round(timeout / 1000)}s (last status ${status ?? 'unknown'}); the AI management key was not minted. Apply again`);
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* Why a mint failed, safe to show: the status ai answered (the minter's own
|
|
3626
|
+
* fixed message), or a network error's `name` and `cause.code`. Never a
|
|
3627
|
+
* `.message` or body, which could echo a credential.
|
|
3628
|
+
*/
|
|
3629
|
+
function describeMintFailure(error) {
|
|
3630
|
+
const status = /^ai's platform\/keys route answered (\d{3})$/.exec(error instanceof Error ? error.message : '')?.[1];
|
|
3631
|
+
if (status)
|
|
3632
|
+
return `ai answered HTTP ${status}`;
|
|
3633
|
+
if (error instanceof Error) {
|
|
3634
|
+
const code = error.cause?.code;
|
|
3635
|
+
// A plain `Error` is the minter's own (a malformed body), not the network.
|
|
3636
|
+
if (typeof code === 'string' || error.name !== 'Error') {
|
|
3637
|
+
return `network error ${error.name}${typeof code === 'string' ? ` ${code}` : ''}`;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return 'the minter failed; its error is not shown';
|
|
3641
|
+
}
|
|
3552
3642
|
/**
|
|
3553
3643
|
* Polls the app until Coolify reports it stopped (`exited`/`stopped`), so a
|
|
3554
3644
|
* healthy status read afterwards belongs to a deployment queued after this.
|