@wtfalch/foundry 0.22.2 → 0.22.4
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 -2
- package/dist/index.js +1 -1
- package/dist/plan/diff.js +79 -3
- package/dist/plan/edge-dns.d.ts +5 -0
- package/dist/plan/edge-dns.js +12 -0
- package/dist/plan/executor.d.ts +0 -2
- package/dist/plan/executor.js +7 -6
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +11 -1
- package/dist/plan/providers-readers.d.ts +19 -6
- package/dist/plan/providers-readers.js +18 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { FoundryStoreDb } from './store/postgres.js';
|
|
|
20
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
21
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
22
22
|
export { AdoptRefusedError } from './plan/index.js';
|
|
23
|
-
export { createEdgeAccess, createEdgeDns, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
23
|
+
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
24
24
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, DnsPort, EdgePlatformAccess, EdgePlatformDns, EdgePlatformRateLimits, EdgePlatformWorkers, EdgePlatformZones, EdgeZonesOptions, ZonePort, EdgeRateLimitsOptions, RateLimitPort, RateLimitRuleInput, EdgeWorkersOptions, EdgePlatformTunnels, TunnelPort, } from './plan/index.js';
|
|
25
25
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
26
26
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
@@ -32,7 +32,7 @@ export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from
|
|
|
32
32
|
export { HttpJobsSchedulerClient } from './plan/index.js';
|
|
33
33
|
export type { HealthTarget, HealthTargetDepth, HealthTargetExpect } from './plan/index.js';
|
|
34
34
|
export { healthTargets, platformHealthTargets } from './plan/index.js';
|
|
35
|
-
export type { ScopeResolver, RecordedIds } from './plan/index.js';
|
|
35
|
+
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './plan/index.js';
|
|
36
36
|
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './plan/index.js';
|
|
37
37
|
export type { ZitadelListAppsRawApp } from './plan/index.js';
|
|
38
38
|
export { adaptZitadelListApps } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, cre
|
|
|
12
12
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
13
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
14
|
export { AdoptRefusedError } from './plan/index.js';
|
|
15
|
-
export { createEdgeAccess, createEdgeDns, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
15
|
+
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
16
16
|
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
17
17
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
18
18
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
package/dist/plan/diff.js
CHANGED
|
@@ -3,13 +3,14 @@ import { APP_ENTRY_KEY, ownerMailboxRecorded, ownerMailboxes } from '../org.js';
|
|
|
3
3
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
4
4
|
import { BOULE_BROKER_ENTRY_KEY, bouleEnvDrift, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
|
+
import { chatInternalNames } from './chat-matrix.js';
|
|
6
7
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
7
8
|
import { planEdgeReservedLabels, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
8
9
|
import { planFilesKeys } from './entry-files-key.js';
|
|
9
10
|
import { appHostname, entryHostnames, tunnelDnsHostnames, tunnelDomainList } from './hostnames.js';
|
|
10
11
|
import { LOGIN_BINDING_SCOPE_KEY, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
11
12
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
12
|
-
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appEnvAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, } from './platform-diff.js';
|
|
13
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, } from './platform-diff.js';
|
|
13
14
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
14
15
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
15
16
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec } from './proxy-labels.js';
|
|
@@ -27,6 +28,74 @@ export function toPlan(detail) {
|
|
|
27
28
|
function resourceKey(system, kind, key) {
|
|
28
29
|
return `${system}:${kind}:${key}`;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether an unclaimed live resource is one foundry made for the org, so the
|
|
33
|
+
* plan does not list it as a stranger. Nothing here claims a resource: no step
|
|
34
|
+
* is planned from it and none is removed.
|
|
35
|
+
*
|
|
36
|
+
* - An id foundry recorded, under any key. One step often makes several
|
|
37
|
+
* resources and records the others under a flat key (`coolify_chat_synapse`,
|
|
38
|
+
* `zitadel_project`), which the readers do not map to a name. Only a
|
|
39
|
+
* resource still under its provider's own kind and id is matched: a mapped
|
|
40
|
+
* one carries foundry's key (the org id, a name), which other recorded
|
|
41
|
+
* values equal, and a planner that leaves it unclaimed wants it listed.
|
|
42
|
+
* - The environment of the org's own Coolify project.
|
|
43
|
+
* - The chat login app. Its id is recorded under `coolify:chat:<org>` until
|
|
44
|
+
* the introspection step records the chat service there instead.
|
|
45
|
+
* - A DNS record edge holds a row for (`config.owner`): foundry or the company
|
|
46
|
+
* wrote it through edge.
|
|
47
|
+
* - A DNS record written before edge held rows: a tunnel hostname foundry
|
|
48
|
+
* published that points at a tunnel, or the mail records of the recorded
|
|
49
|
+
* mail domain.
|
|
50
|
+
*/
|
|
51
|
+
function madeByFoundry(org, r, recordedIds, recordedValues) {
|
|
52
|
+
if (PROVIDER_KINDS[r.system]?.includes(r.kind) && recordedValues.has(r.key))
|
|
53
|
+
return true;
|
|
54
|
+
if (r.system === 'coolify') {
|
|
55
|
+
if (r.kind === 'environment')
|
|
56
|
+
return true;
|
|
57
|
+
return r.kind === 'application' && r.config.name === chatInternalNames(org.slug).mas;
|
|
58
|
+
}
|
|
59
|
+
if (r.system !== 'cloudflare' || r.kind !== 'dns_record')
|
|
60
|
+
return false;
|
|
61
|
+
if (typeof r.config.owner === 'string')
|
|
62
|
+
return true;
|
|
63
|
+
const name = typeof r.config.name === 'string' ? r.config.name.toLowerCase() : '';
|
|
64
|
+
if (name === '')
|
|
65
|
+
return false;
|
|
66
|
+
const type = r.config.type;
|
|
67
|
+
const content = typeof r.config.content === 'string' ? r.config.content.toLowerCase() : '';
|
|
68
|
+
const tunnelHostnames = (recordedIds[CLOUDFLARE_TUNNEL_DNS_HOSTNAMES_SCOPE_KEY] ?? '')
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.split(',');
|
|
71
|
+
if (type === 'CNAME' && content.endsWith('.cfargotunnel.com') && tunnelHostnames.includes(name)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const mailDomain = recordedIds[MAIL_DOMAIN_SCOPE_KEY]?.toLowerCase();
|
|
75
|
+
if (!mailDomain)
|
|
76
|
+
return false;
|
|
77
|
+
if (name === mailDomain) {
|
|
78
|
+
return type === 'MX' || (type === 'TXT' && /^"?v=spf1\b/.test(content));
|
|
79
|
+
}
|
|
80
|
+
return name === `_dmarc.${mailDomain}` || name.endsWith(`._domainkey.${mailDomain}`);
|
|
81
|
+
}
|
|
82
|
+
/** The kinds a provider lists a resource under before a recorded `system:kind:key` maps it to foundry's own. */
|
|
83
|
+
const PROVIDER_KINDS = {
|
|
84
|
+
coolify: ['application', 'database'],
|
|
85
|
+
zitadel: ['project', 'app'],
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* What a listed resource is called: the provider's own name for it, then its
|
|
89
|
+
* id, which tells two resources of one name apart. The id alone when it has
|
|
90
|
+
* no name.
|
|
91
|
+
*/
|
|
92
|
+
function liveResourceName(r) {
|
|
93
|
+
const name = typeof r.config.name === 'string' && r.config.name !== '' ? r.config.name : null;
|
|
94
|
+
if (name === null || name === r.key)
|
|
95
|
+
return r.key;
|
|
96
|
+
const type = r.kind === 'dns_record' && typeof r.config.type === 'string' ? `${r.config.type} ` : '';
|
|
97
|
+
return `${type}${name}, ${r.key}`;
|
|
98
|
+
}
|
|
30
99
|
/**
|
|
31
100
|
* The diff engine: #19's "a plan reads live state from Zitadel, Coolify and
|
|
32
101
|
* Cloudflare and diffs it against the declared org", built on #10's rule
|
|
@@ -356,10 +425,17 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
356
425
|
}
|
|
357
426
|
// #10 (resolved): "foundry manages only what the declared org names";
|
|
358
427
|
// anything left unclaimed is listed, never proposed for deletion.
|
|
428
|
+
// A resource foundry made for the org is not a finding: only a resource
|
|
429
|
+
// nobody recorded is listed, under the name its provider shows.
|
|
430
|
+
// The org's own id and names are recorded as values too, and are no provider id.
|
|
431
|
+
const recordedValues = new Set(Object.values(recordedIds));
|
|
432
|
+
for (const own of ['', org.id, org.slug, org.domain ?? ''])
|
|
433
|
+
recordedValues.delete(own);
|
|
359
434
|
const unmanaged = live
|
|
360
435
|
.filter((r) => !claimed.has(resourceKey(r.system, r.kind, r.key)))
|
|
436
|
+
.filter((r) => !madeByFoundry(org, r, recordedIds, recordedValues))
|
|
361
437
|
.map((r) => ({
|
|
362
|
-
description: `${r.system} ${r.kind} "${r
|
|
438
|
+
description: `${r.system} ${r.kind} "${liveResourceName(r)}" is live but not named by the declared org`,
|
|
363
439
|
}));
|
|
364
440
|
// A blocked Agora is reported here, not planned: a step the executor must
|
|
365
441
|
// refuse would halt every step after it. This is the plan's one non-step
|
|
@@ -2229,7 +2305,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2229
2305
|
key: blockedKey('port'),
|
|
2230
2306
|
});
|
|
2231
2307
|
}
|
|
2232
|
-
if (want
|
|
2308
|
+
if (appFolderDiffers(want, have) ||
|
|
2233
2309
|
want.autoDeploy !== have.autoDeploy ||
|
|
2234
2310
|
(want.port !== null && want.port !== have.port)) {
|
|
2235
2311
|
return {
|
package/dist/plan/edge-dns.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface DnsPort {
|
|
|
47
47
|
}>;
|
|
48
48
|
deleteDnsRecord(zone: string, recordId: string): Promise<void>;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The owner edge reports for each record of a zone that has an edge row, by
|
|
52
|
+
* record id. For `ProvidersCloudflareReader`'s `resolveDnsOwners`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function edgeDnsRecordOwners(client: Pick<EdgePlatformDns, 'list'>, zone: string): Promise<Record<string, string>>;
|
|
50
55
|
/** Edge answers its platform writes from one shared slot of 4 (company-less writes). */
|
|
51
56
|
export declare const EDGE_DNS_WRITE_CONCURRENCY = 4;
|
|
52
57
|
/**
|
package/dist/plan/edge-dns.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { EdgeApiError } from '@wtfalch/edge';
|
|
2
|
+
/**
|
|
3
|
+
* The owner edge reports for each record of a zone that has an edge row, by
|
|
4
|
+
* record id. For `ProvidersCloudflareReader`'s `resolveDnsOwners`.
|
|
5
|
+
*/
|
|
6
|
+
export async function edgeDnsRecordOwners(client, zone) {
|
|
7
|
+
const owners = {};
|
|
8
|
+
for (const record of (await client.list({ zone })).records) {
|
|
9
|
+
if (record.owner !== null)
|
|
10
|
+
owners[record.id] = record.owner;
|
|
11
|
+
}
|
|
12
|
+
return owners;
|
|
13
|
+
}
|
|
2
14
|
/** Edge answers its platform writes from one shared slot of 4 (company-less writes). */
|
|
3
15
|
export const EDGE_DNS_WRITE_CONCURRENCY = 4;
|
|
4
16
|
/**
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -117,8 +117,6 @@ export interface CoolifyPort {
|
|
|
117
117
|
ports_exposes?: string;
|
|
118
118
|
/** `false` for an internal entry: Coolify makes no domain for it. */
|
|
119
119
|
autogenerate_domain?: boolean;
|
|
120
|
-
/** The folder a `static` entry serves (`HostingEntry.folder`). */
|
|
121
|
-
publish_directory?: string;
|
|
122
120
|
/** Whether a push to `git_branch` deploys (`HostingEntry.autoDeploy`). */
|
|
123
121
|
is_auto_deploy_enabled?: boolean;
|
|
124
122
|
/**
|
package/dist/plan/executor.js
CHANGED
|
@@ -23,7 +23,7 @@ import { healthCheckSettings } from './health-check-settings.js';
|
|
|
23
23
|
import { appHostname, coolifyTunnelDomains, entryHostnames, tunnelDnsHostnames, tunnelDomainList, tunnelIngressHostnames, } from './hostnames.js';
|
|
24
24
|
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, customersEntryKeys, loginBindingState, loginHostClientIds, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
25
25
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
26
|
-
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, } from './platform-diff.js';
|
|
26
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appFolderDiffers, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, } from './platform-diff.js';
|
|
27
27
|
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, } from './platform-providers.js';
|
|
28
28
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryProxyLabels, sameLabelSets, } from './proxy-labels.js';
|
|
29
29
|
import { entryWorkerAppliedScopeKey, entryWorkerExclusionsScopeKey, entryWorkerRetiredScopeKey, entryWorkerRouteIdScopeKey, entryWorkerRoutePatternScopeKey, entryWorkerRoutePendingScopeKey, entryWorkerRouteZoneScopeKey, entryWorkerUploadedScopeKey, } from './worker.js';
|
|
@@ -1789,7 +1789,6 @@ export class FoundryStepExecutor {
|
|
|
1789
1789
|
const names = entryHostnames(org, entry);
|
|
1790
1790
|
const wantDomains = tunnelDomainList(names);
|
|
1791
1791
|
const isStatic = entry.buildPack === 'static';
|
|
1792
|
-
const folderField = isStatic ? 'publish_directory' : 'base_directory';
|
|
1793
1792
|
const settingsKey = appSettingsAppliedScopeKey(entry.key);
|
|
1794
1793
|
const domainsKey = appDomainsAppliedScopeKey(entry.key);
|
|
1795
1794
|
let haveDomains;
|
|
@@ -1833,7 +1832,10 @@ export class FoundryStepExecutor {
|
|
|
1833
1832
|
// A static site is served by Coolify's own web server on port 80.
|
|
1834
1833
|
ports_exposes: isStatic ? '80' : entry.port === null ? undefined : String(entry.port),
|
|
1835
1834
|
is_auto_deploy_enabled: entry.autoDeploy,
|
|
1836
|
-
|
|
1835
|
+
// Also the folder a static entry serves: Coolify's static build
|
|
1836
|
+
// copies `base_directory`, and reads `publish_directory` only for a
|
|
1837
|
+
// built app.
|
|
1838
|
+
base_directory: entry.folder === null ? undefined : `/${entry.folder}`,
|
|
1837
1839
|
// Only the shared standard app builds from this repo; a bespoke
|
|
1838
1840
|
// app's Dockerfile is at its own repo's root.
|
|
1839
1841
|
dockerfile_location: entry.repo ? undefined : DEFAULT_APP_DOCKERFILE,
|
|
@@ -1848,8 +1850,8 @@ export class FoundryStepExecutor {
|
|
|
1848
1850
|
// difference again.
|
|
1849
1851
|
const want = appSettingsOf(entry);
|
|
1850
1852
|
const patch = {};
|
|
1851
|
-
if (want
|
|
1852
|
-
patch
|
|
1853
|
+
if (appFolderDiffers(want, have))
|
|
1854
|
+
patch.base_directory = want.folder ? `/${want.folder}` : '/';
|
|
1853
1855
|
if (want.port !== have.port && want.port !== null && !isStatic) {
|
|
1854
1856
|
patch.ports_exposes = String(want.port);
|
|
1855
1857
|
}
|
|
@@ -1904,7 +1906,6 @@ export class FoundryStepExecutor {
|
|
|
1904
1906
|
await this.ports.coolify.updateApplication(appId, patch);
|
|
1905
1907
|
// The container builds and listens from these only at its next deploy.
|
|
1906
1908
|
const settingsNeedDeploy = Boolean(patch.base_directory ||
|
|
1907
|
-
patch.publish_directory ||
|
|
1908
1909
|
patch.ports_exposes ||
|
|
1909
1910
|
patch.domains !== undefined ||
|
|
1910
1911
|
patch.custom_internal_name ||
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type { CallsSfuConnection, CallsSfuConnectionPort, CallsSfuKeysPort, Call
|
|
|
26
26
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
27
27
|
export type { MailCopyJob, MailCopyPort, MailCopyStepResult } from './mail-copy.js';
|
|
28
28
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
29
|
-
export type { ScopeResolver, RecordedIds } from './providers-readers.js';
|
|
29
|
+
export type { ScopeResolver, RecordedIds, DnsRecordOwners } from './providers-readers.js';
|
|
30
30
|
export { ProvidersAiReader, ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
31
31
|
export type { AgoraHostPort, AgoraHostValues } from './agora.js';
|
|
32
32
|
export type { BrokerPort } from './broker-entries.js';
|
|
@@ -41,7 +41,7 @@ export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedu
|
|
|
41
41
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
42
42
|
export type { HealthTarget, HealthTargetDepth, HealthTargetExpect } from './health-targets.js';
|
|
43
43
|
export { healthTargets, platformHealthTargets } from './health-targets.js';
|
|
44
|
-
export { createEdgeDns, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
44
|
+
export { createEdgeDns, edgeDnsRecordOwners, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
45
45
|
export type { DnsPort, DnsRecordInput, DnsRecordSummary, EdgePlatformDns } from './edge-dns.js';
|
|
46
46
|
export { createEdgeAccess } from './edge-access.js';
|
|
47
47
|
export type { AccessAppSummary, AccessDecision, AccessPolicyRule, AccessPort, EdgePlatformAccess, } from './edge-access.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export { checkMailIdentityAudience } from './mail-identity.js';
|
|
|
21
21
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
22
22
|
export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
|
|
23
23
|
export { healthTargets, platformHealthTargets } from './health-targets.js';
|
|
24
|
-
export { createEdgeDns, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
24
|
+
export { createEdgeDns, edgeDnsRecordOwners, EDGE_DNS_WRITE_CONCURRENCY } from './edge-dns.js';
|
|
25
25
|
export { createEdgeAccess } from './edge-access.js';
|
|
26
26
|
export { createEdgeWorkers } from './edge-workers.js';
|
|
27
27
|
export { createEdgeRateLimits } from './edge-ratelimits.js';
|
|
@@ -482,8 +482,17 @@ export interface AppSettings {
|
|
|
482
482
|
folder: string | null;
|
|
483
483
|
port: number | null;
|
|
484
484
|
autoDeploy: boolean;
|
|
485
|
+
/**
|
|
486
|
+
* Set for a `static` entry whose folder is written as `base_directory`,
|
|
487
|
+
* the field Coolify's static build serves from. A static marker without it
|
|
488
|
+
* was written as `publish_directory`, which that build ignores (it served
|
|
489
|
+
* the repo root), so the app is patched and redeployed once.
|
|
490
|
+
*/
|
|
491
|
+
staticFolderIn?: 'base_directory';
|
|
485
492
|
}
|
|
486
493
|
export declare function appSettingsOf(entry: HostingEntry): AppSettings;
|
|
494
|
+
/** Whether the app's folder is not yet where Coolify reads it: a changed folder, or a static one written to the old field. */
|
|
495
|
+
export declare function appFolderDiffers(want: AppSettings, have: AppSettings): boolean;
|
|
487
496
|
/** What an app that predates the marker has: Coolify's own defaults, so it plans no change. */
|
|
488
497
|
export declare const DEFAULT_APP_SETTINGS: AppSettings;
|
|
489
498
|
export declare function appSettingsAppliedScopeKey(entryKey: string): string;
|
|
@@ -520,7 +520,17 @@ export function serviceDbEnvAppliedScopeKey(service) {
|
|
|
520
520
|
return `service_db_env_applied.${service}`;
|
|
521
521
|
}
|
|
522
522
|
export function appSettingsOf(entry) {
|
|
523
|
-
return {
|
|
523
|
+
return {
|
|
524
|
+
folder: entry.folder,
|
|
525
|
+
port: entry.port,
|
|
526
|
+
autoDeploy: entry.autoDeploy,
|
|
527
|
+
...(entry.buildPack === 'static' ? { staticFolderIn: 'base_directory' } : {}),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/** Whether the app's folder is not yet where Coolify reads it: a changed folder, or a static one written to the old field. */
|
|
531
|
+
export function appFolderDiffers(want, have) {
|
|
532
|
+
return (want.folder !== have.folder ||
|
|
533
|
+
(want.folder !== null && want.staticFolderIn !== have.staticFolderIn));
|
|
524
534
|
}
|
|
525
535
|
/** What an app that predates the marker has: Coolify's own defaults, so it plans no change. */
|
|
526
536
|
export const DEFAULT_APP_SETTINGS = { folder: null, port: null, autoDeploy: true };
|
|
@@ -99,16 +99,29 @@ export declare class ProvidersCoolifyReader implements CoolifyReader {
|
|
|
99
99
|
/** The project's resources as Coolify reports them, unmapped: what adopt checks against a declaration. */
|
|
100
100
|
listProject(projectUuid: string): Promise<LiveResource[]>;
|
|
101
101
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
* a zone's DNS records plus its email-routing status (v1 scope — see that
|
|
105
|
-
* method's own doc comment for what it leaves out).
|
|
106
|
-
*/
|
|
102
|
+
/** Record id to the owner edge reports for it (`platform` or `organisation`); a record with no edge row is absent. */
|
|
103
|
+
export type DnsRecordOwners = (orgId: OrgId) => Promise<Readonly<Record<string, string>>>;
|
|
107
104
|
export declare class ProvidersCloudflareReader implements CloudflareReader {
|
|
108
105
|
private readonly client;
|
|
109
106
|
private readonly resolveScope;
|
|
110
107
|
private readonly resolveRecordedIds;
|
|
111
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Who edge holds each DNS record of the org's zone for. A `dns_record`
|
|
110
|
+
* with a row gets `config.owner`, and the plan does not list it as a
|
|
111
|
+
* stranger. Left out, every record reads as having no row. A failing
|
|
112
|
+
* lookup fails the read: a plan that listed those records as strangers
|
|
113
|
+
* would not match the next one.
|
|
114
|
+
*/
|
|
115
|
+
private readonly resolveDnsOwners?;
|
|
116
|
+
constructor(client: CloudflareClient, resolveScope: ScopeResolver, resolveRecordedIds: RecordedIds,
|
|
117
|
+
/**
|
|
118
|
+
* Who edge holds each DNS record of the org's zone for. A `dns_record`
|
|
119
|
+
* with a row gets `config.owner`, and the plan does not list it as a
|
|
120
|
+
* stranger. Left out, every record reads as having no row. A failing
|
|
121
|
+
* lookup fails the read: a plan that listed those records as strangers
|
|
122
|
+
* would not match the next one.
|
|
123
|
+
*/
|
|
124
|
+
resolveDnsOwners?: DnsRecordOwners | undefined);
|
|
112
125
|
listResources(orgId: OrgId): Promise<LiveResource[]>;
|
|
113
126
|
}
|
|
114
127
|
/**
|
|
@@ -303,23 +303,39 @@ export class ProvidersCoolifyReader {
|
|
|
303
303
|
* a zone's DNS records plus its email-routing status (v1 scope — see that
|
|
304
304
|
* method's own doc comment for what it leaves out).
|
|
305
305
|
*/
|
|
306
|
+
const NO_DNS_OWNERS = {};
|
|
306
307
|
export class ProvidersCloudflareReader {
|
|
307
308
|
client;
|
|
308
309
|
resolveScope;
|
|
309
310
|
resolveRecordedIds;
|
|
310
|
-
|
|
311
|
+
resolveDnsOwners;
|
|
312
|
+
constructor(client, resolveScope, resolveRecordedIds,
|
|
313
|
+
/**
|
|
314
|
+
* Who edge holds each DNS record of the org's zone for. A `dns_record`
|
|
315
|
+
* with a row gets `config.owner`, and the plan does not list it as a
|
|
316
|
+
* stranger. Left out, every record reads as having no row. A failing
|
|
317
|
+
* lookup fails the read: a plan that listed those records as strangers
|
|
318
|
+
* would not match the next one.
|
|
319
|
+
*/
|
|
320
|
+
resolveDnsOwners) {
|
|
311
321
|
this.client = client;
|
|
312
322
|
this.resolveScope = resolveScope;
|
|
313
323
|
this.resolveRecordedIds = resolveRecordedIds;
|
|
324
|
+
this.resolveDnsOwners = resolveDnsOwners;
|
|
314
325
|
}
|
|
315
326
|
async listResources(orgId) {
|
|
316
327
|
const zoneId = await this.resolveScope(orgId);
|
|
317
328
|
if (!zoneId)
|
|
318
329
|
return [];
|
|
319
|
-
const [
|
|
330
|
+
const [listed, recorded, owners] = await Promise.all([
|
|
320
331
|
this.client.listResources(zoneId),
|
|
321
332
|
this.resolveRecordedIds(orgId),
|
|
333
|
+
this.resolveDnsOwners?.(orgId) ?? NO_DNS_OWNERS,
|
|
322
334
|
]);
|
|
335
|
+
const resources = listed.map((resource) => {
|
|
336
|
+
const owner = resource.kind === 'dns_record' ? owners[resource.key] : undefined;
|
|
337
|
+
return owner ? { ...resource, config: { ...resource.config, owner } } : resource;
|
|
338
|
+
});
|
|
323
339
|
// A zone needs no mapping: its key is already the zone name, which is
|
|
324
340
|
// foundry's own zone key (`diff.ts`'s `planZone`), so it never matches a
|
|
325
341
|
// recorded provider id and always passes through unchanged.
|