@superblocksteam/shared 0.9603.2 → 0.9604.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/dist/database-lifecycle/capacityPolicy.d.ts +51 -0
- package/dist/database-lifecycle/capacityPolicy.d.ts.map +1 -0
- package/dist/database-lifecycle/capacityPolicy.js +159 -0
- package/dist/database-lifecycle/capacityPolicy.js.map +1 -0
- package/dist/database-lifecycle/capacityPolicy.test.d.ts +2 -0
- package/dist/database-lifecycle/capacityPolicy.test.d.ts.map +1 -0
- package/dist/database-lifecycle/capacityPolicy.test.js +159 -0
- package/dist/database-lifecycle/capacityPolicy.test.js.map +1 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.d.ts +18 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.d.ts.map +1 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.js +32 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.js.map +1 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.test.d.ts +2 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.test.d.ts.map +1 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.test.js +91 -0
- package/dist/database-lifecycle/edit-lifecycle-capability.test.js.map +1 -0
- package/dist/database-lifecycle/index.d.ts +50 -27
- package/dist/database-lifecycle/index.d.ts.map +1 -1
- package/dist/database-lifecycle/index.js +78 -54
- package/dist/database-lifecycle/index.js.map +1 -1
- package/dist/database-lifecycle/index.test.js +120 -77
- package/dist/database-lifecycle/index.test.js.map +1 -1
- package/dist/database-lifecycle/physicalPoolIdentity.d.ts +29 -0
- package/dist/database-lifecycle/physicalPoolIdentity.d.ts.map +1 -0
- package/dist/database-lifecycle/physicalPoolIdentity.js +64 -0
- package/dist/database-lifecycle/physicalPoolIdentity.js.map +1 -0
- package/dist/database-lifecycle/physicalPoolIdentity.test.d.ts +2 -0
- package/dist/database-lifecycle/physicalPoolIdentity.test.d.ts.map +1 -0
- package/dist/database-lifecycle/physicalPoolIdentity.test.js +51 -0
- package/dist/database-lifecycle/physicalPoolIdentity.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/audit/ocsf.d.ts +60 -6
- package/dist/types/audit/ocsf.d.ts.map +1 -1
- package/dist/types/audit/ocsf.js +12 -1
- package/dist/types/audit/ocsf.js.map +1 -1
- package/dist/types/deployment/deployQueueStatus.d.ts +13 -0
- package/dist/types/deployment/deployQueueStatus.d.ts.map +1 -1
- package/dist/types/deployment/index.d.ts +1 -1
- package/dist/types/deployment/index.d.ts.map +1 -1
- package/dist/types/deployment/index.js.map +1 -1
- package/dist/types/policyGate/index.d.ts +5 -0
- package/dist/types/policyGate/index.d.ts.map +1 -1
- package/dist/types/policyGate/index.js +5 -1
- package/dist/types/policyGate/index.js.map +1 -1
- package/dist/types/publishPipeline/plan.js +1 -1
- package/dist/types/publishPipeline/plan.js.map +1 -1
- package/dist/types/publishPipeline/plan.test.js +3 -3
- package/dist/types/publishPipeline/plan.test.js.map +1 -1
- package/dist/types/rbac/index.d.ts +2 -0
- package/dist/types/rbac/index.d.ts.map +1 -1
- package/dist/types/rbac/index.js +3 -1
- package/dist/types/rbac/index.js.map +1 -1
- package/dist/types/reviewPolicy/index.d.ts +30 -4
- package/dist/types/reviewPolicy/index.d.ts.map +1 -1
- package/dist/types/reviewPolicy/index.js +8 -1
- package/dist/types/reviewPolicy/index.js.map +1 -1
- package/dist-esm/database-lifecycle/capacityPolicy.d.ts +51 -0
- package/dist-esm/database-lifecycle/capacityPolicy.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/capacityPolicy.js +150 -0
- package/dist-esm/database-lifecycle/capacityPolicy.js.map +1 -0
- package/dist-esm/database-lifecycle/capacityPolicy.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/capacityPolicy.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/capacityPolicy.test.js +157 -0
- package/dist-esm/database-lifecycle/capacityPolicy.test.js.map +1 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.d.ts +18 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.js +29 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.js.map +1 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.test.js +89 -0
- package/dist-esm/database-lifecycle/edit-lifecycle-capability.test.js.map +1 -0
- package/dist-esm/database-lifecycle/index.d.ts +50 -27
- package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/index.js +62 -51
- package/dist-esm/database-lifecycle/index.js.map +1 -1
- package/dist-esm/database-lifecycle/index.test.js +90 -47
- package/dist-esm/database-lifecycle/index.test.js.map +1 -1
- package/dist-esm/database-lifecycle/physicalPoolIdentity.d.ts +29 -0
- package/dist-esm/database-lifecycle/physicalPoolIdentity.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/physicalPoolIdentity.js +59 -0
- package/dist-esm/database-lifecycle/physicalPoolIdentity.js.map +1 -0
- package/dist-esm/database-lifecycle/physicalPoolIdentity.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/physicalPoolIdentity.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/physicalPoolIdentity.test.js +49 -0
- package/dist-esm/database-lifecycle/physicalPoolIdentity.test.js.map +1 -0
- package/dist-esm/index.d.ts +1 -0
- package/dist-esm/index.d.ts.map +1 -1
- package/dist-esm/index.js +1 -0
- package/dist-esm/index.js.map +1 -1
- package/dist-esm/types/audit/ocsf.d.ts +60 -6
- package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
- package/dist-esm/types/audit/ocsf.js +12 -1
- package/dist-esm/types/audit/ocsf.js.map +1 -1
- package/dist-esm/types/deployment/deployQueueStatus.d.ts +13 -0
- package/dist-esm/types/deployment/deployQueueStatus.d.ts.map +1 -1
- package/dist-esm/types/deployment/index.d.ts +1 -1
- package/dist-esm/types/deployment/index.d.ts.map +1 -1
- package/dist-esm/types/deployment/index.js.map +1 -1
- package/dist-esm/types/policyGate/index.d.ts +5 -0
- package/dist-esm/types/policyGate/index.d.ts.map +1 -1
- package/dist-esm/types/policyGate/index.js +5 -1
- package/dist-esm/types/policyGate/index.js.map +1 -1
- package/dist-esm/types/publishPipeline/plan.js +1 -1
- package/dist-esm/types/publishPipeline/plan.js.map +1 -1
- package/dist-esm/types/publishPipeline/plan.test.js +3 -3
- package/dist-esm/types/publishPipeline/plan.test.js.map +1 -1
- package/dist-esm/types/rbac/index.d.ts +2 -0
- package/dist-esm/types/rbac/index.d.ts.map +1 -1
- package/dist-esm/types/rbac/index.js +3 -1
- package/dist-esm/types/rbac/index.js.map +1 -1
- package/dist-esm/types/reviewPolicy/index.d.ts +30 -4
- package/dist-esm/types/reviewPolicy/index.d.ts.map +1 -1
- package/dist-esm/types/reviewPolicy/index.js +7 -0
- package/dist-esm/types/reviewPolicy/index.js.map +1 -1
- package/package.json +1 -1
- package/src/database-lifecycle/capacityPolicy.test.ts +209 -0
- package/src/database-lifecycle/capacityPolicy.ts +197 -0
- package/src/database-lifecycle/edit-lifecycle-capability.test.ts +116 -0
- package/src/database-lifecycle/edit-lifecycle-capability.ts +34 -0
- package/src/database-lifecycle/index.test.ts +105 -48
- package/src/database-lifecycle/index.ts +157 -90
- package/src/database-lifecycle/physicalPoolIdentity.test.ts +64 -0
- package/src/database-lifecycle/physicalPoolIdentity.ts +75 -0
- package/src/index.ts +1 -0
- package/src/types/audit/ocsf.ts +73 -7
- package/src/types/deployment/deployQueueStatus.ts +14 -0
- package/src/types/deployment/index.ts +2 -1
- package/src/types/policyGate/index.ts +5 -1
- package/src/types/publishPipeline/plan.test.ts +3 -3
- package/src/types/publishPipeline/plan.ts +1 -1
- package/src/types/rbac/index.ts +3 -1
- package/src/types/reviewPolicy/index.ts +44 -5
|
@@ -1,43 +1,69 @@
|
|
|
1
1
|
import { sha256Base64 } from '../signing/hashing.js';
|
|
2
|
+
import { ApiTriggerType } from '../types/api/index.js';
|
|
3
|
+
import type { DatabaseEngine } from './capacityPolicy.js';
|
|
2
4
|
|
|
3
5
|
export const LIFECYCLE_TERMINAL_STATES = ['ready', 'failed', 'cancelled'] as const;
|
|
4
6
|
export const LIFECYCLE_NON_TERMINAL_STATES = ['pending', 'provisioning', 'migrating', 'retiring'] as const;
|
|
5
7
|
export const LIFECYCLE_STATES = [...LIFECYCLE_NON_TERMINAL_STATES, ...LIFECYCLE_TERMINAL_STATES] as const;
|
|
6
8
|
export const LIFECYCLE_MIGRATION_STATES = ['pending', 'migrated', 'failed'] as const;
|
|
7
|
-
//
|
|
8
|
-
// single `ensure_database` covers what used to be split into
|
|
9
|
+
// A single `ensure_database` covers what used to be split into
|
|
9
10
|
// ensure_dev_database / ensure_prod_database. `migrate_schema` and
|
|
10
11
|
// `retire_database` were always environment-agnostic.
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
export const
|
|
17
|
-
export const DATABASE_ENGINES = ['postgres', 'snowflake', 'snowflake_postgres', 'lakebase'] as const;
|
|
12
|
+
// `ensure_physical_database_instance` is claimable pool-level work (wizard /
|
|
13
|
+
// eager provisioning). It is not bound to a database_binding row; the claim
|
|
14
|
+
// payload carries provisioningId instead of binding identity. Appended last
|
|
15
|
+
// because Postgres `ALTER TYPE ... ADD VALUE` appends, and TypeORM's
|
|
16
|
+
// entity-vs-schema check requires this array to match that live order.
|
|
17
|
+
export const LIFECYCLE_OPERATIONS = ['ensure_database', 'migrate_schema', 'retire_database', 'ensure_physical_database_instance'] as const;
|
|
18
18
|
export const DATABASE_LIFECYCLE_MANAGED_BY = 'database_lifecycle';
|
|
19
|
-
export const NATIVE_DB_AUTH_DESCRIPTOR_VERSION =
|
|
20
|
-
export const NATIVE_DB_AUTH_DESCRIPTOR_VERSIONS = [1, NATIVE_DB_AUTH_DESCRIPTOR_VERSION] as const;
|
|
19
|
+
export const NATIVE_DB_AUTH_DESCRIPTOR_VERSION = 3 as const;
|
|
21
20
|
export const NATIVE_DB_CONNECTOR_ROLE_NAME_PREFIX = 'superblocks-native-db-connector';
|
|
22
21
|
export const NATIVE_DB_IAM_AUTH_MODE = 'aws_iam_role' as const;
|
|
23
22
|
export const NATIVE_DB_IDENTIFIER_HASH_DOMAIN = 'superblocks-native-db:v1';
|
|
24
23
|
export const DATABASE_LIFECYCLE_CAPABILITY_MANAGED_IAM_V1 = 'managed-IAM-v1';
|
|
25
24
|
|
|
25
|
+
export function isLifecycleManagedIntegrationAllowedForApiTrigger(
|
|
26
|
+
triggerType: ApiTriggerType,
|
|
27
|
+
managedBy: string | null | undefined
|
|
28
|
+
): boolean {
|
|
29
|
+
if (managedBy !== DATABASE_LIFECYCLE_MANAGED_BY) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return triggerType === ApiTriggerType.UI;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
// Agent capability tag keys. A lifecycle worker publishes these in the
|
|
27
36
|
// `tags` map of its agent registration (merged into — never replacing — the
|
|
28
|
-
// tag map the agent already publishes
|
|
29
|
-
// datatag coverage, same meaning it has for execution routing
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// on "some active org agent supports this".
|
|
37
|
+
// tag map the agent already publishes). The existing `profile` tag carries
|
|
38
|
+
// datatag coverage, same meaning it has for execution routing, and is the
|
|
39
|
+
// sole lifecycle routing key. The server matches pending lifecycle requests
|
|
40
|
+
// against these at poll/claim time and gates task creation on "some active
|
|
41
|
+
// org agent supports this".
|
|
34
42
|
export const DATABASE_LIFECYCLE_TAG_OPERATIONS = 'databaseLifecycle:operations';
|
|
35
43
|
export const DATABASE_LIFECYCLE_TAG_ENGINES = 'databaseLifecycle:engines';
|
|
36
|
-
export const DATABASE_LIFECYCLE_TAG_ENVIRONMENT_PROFILES = 'databaseLifecycle:environmentProfiles';
|
|
37
44
|
export const DATABASE_LIFECYCLE_TAG_CAPABILITIES = 'databaseLifecycle:capabilities';
|
|
45
|
+
export {
|
|
46
|
+
CapacityPolicyValidationError,
|
|
47
|
+
DATABASE_ENGINES,
|
|
48
|
+
DATABASE_LIFECYCLE_TAG_CAPACITY_POLICIES,
|
|
49
|
+
PHYSICAL_DATABASE_CAPACITY_THRESHOLD_TYPES,
|
|
50
|
+
computePhysicalDatabasePoolKey,
|
|
51
|
+
encodeCapacityPolicyTag,
|
|
52
|
+
parseCapacityPolicyTag,
|
|
53
|
+
parseCapacityPolicyTags,
|
|
54
|
+
validateCapacityPolicyCompatibility,
|
|
55
|
+
type DatabaseEngine,
|
|
56
|
+
type PhysicalDatabaseCapacityPolicy,
|
|
57
|
+
type PhysicalDatabaseCapacityThresholdType,
|
|
58
|
+
type PhysicalDatabasePoolKey
|
|
59
|
+
} from './capacityPolicy.js';
|
|
60
|
+
// The stable, customer-managed name of the deployment a worker belongs to. It
|
|
61
|
+
// says which physical pool that worker owns, which is what keeps one
|
|
62
|
+
// deployment from being handed work meant for another. The claim path also
|
|
63
|
+
// treats it as the binding-ownership authority, so a client-supplied query
|
|
64
|
+
// value cannot bypass it.
|
|
65
|
+
export { DATABASE_LIFECYCLE_TAG_AGENT_NAME, PhysicalPoolIdentityError, parsePhysicalPoolAgentNameTag } from './physicalPoolIdentity.js';
|
|
38
66
|
|
|
39
|
-
export type LifecycleEnvironment = (typeof LIFECYCLE_ENVIRONMENTS)[number];
|
|
40
|
-
export type DatabaseEngine = (typeof DATABASE_ENGINES)[number];
|
|
41
67
|
export type LifecycleOperation = (typeof LIFECYCLE_OPERATIONS)[number];
|
|
42
68
|
export type LifecycleTerminalState = (typeof LIFECYCLE_TERMINAL_STATES)[number];
|
|
43
69
|
export type LifecycleNonTerminalState = (typeof LIFECYCLE_NON_TERMINAL_STATES)[number];
|
|
@@ -58,18 +84,15 @@ export type LifecycleErrorCode =
|
|
|
58
84
|
// the request is cancelled rather than applied to the current binding.
|
|
59
85
|
| 'stale_dispatch';
|
|
60
86
|
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export type CredentialRef = {
|
|
65
|
-
resolver: CredentialResolver;
|
|
87
|
+
export type PhysicalMasterCredentialRef = {
|
|
88
|
+
resolver: 'aws_secrets_manager';
|
|
66
89
|
ref: string;
|
|
67
90
|
field?: string;
|
|
68
91
|
};
|
|
69
92
|
|
|
70
93
|
export type NativeDbIamDescriptor = {
|
|
71
94
|
application_id: string;
|
|
72
|
-
auth_descriptor_version:
|
|
95
|
+
auth_descriptor_version: typeof NATIVE_DB_AUTH_DESCRIPTOR_VERSION;
|
|
73
96
|
auth_mode: typeof NATIVE_DB_IAM_AUTH_MODE;
|
|
74
97
|
aws_account_id: string;
|
|
75
98
|
binding_id: string;
|
|
@@ -98,31 +121,25 @@ type DatabaseBindingBase = {
|
|
|
98
121
|
requirementKey: string;
|
|
99
122
|
logicalName: string;
|
|
100
123
|
applicationId: string;
|
|
101
|
-
environment: LifecycleEnvironment;
|
|
102
124
|
// The datatag key (Profile.key, e.g. 'staging' / 'production') this
|
|
103
|
-
// binding serves
|
|
104
|
-
//
|
|
125
|
+
// binding serves, and the only axis that scopes it. One profile is one
|
|
126
|
+
// logical database with one DB user.
|
|
105
127
|
profile: string;
|
|
106
128
|
desiredSpecHash: string;
|
|
107
129
|
migrationState: LifecycleMigrationState;
|
|
108
130
|
};
|
|
109
131
|
|
|
110
|
-
// A binding that has completed provisioning.
|
|
111
|
-
// guarantees that the OPA returned both `connectionMetadata` and
|
|
112
|
-
// `credentialRefs`, so consumers can rely on those without null checks.
|
|
132
|
+
// A binding that has completed IAM provisioning.
|
|
113
133
|
export type ReadyDatabaseBinding = DatabaseBindingBase & {
|
|
114
134
|
lifecycleState: 'ready';
|
|
115
135
|
connectionMetadata: Record<string, string | number | boolean>;
|
|
116
|
-
credentialRefs: Record<string, CredentialRef>;
|
|
117
136
|
};
|
|
118
137
|
|
|
119
|
-
// A binding in any non-ready state. Connection metadata
|
|
120
|
-
//
|
|
121
|
-
// never reached `ready`.
|
|
138
|
+
// A binding in any non-ready state. Connection metadata may be present from a
|
|
139
|
+
// prior `ready` transition or absent if the binding never reached `ready`.
|
|
122
140
|
export type IncompleteDatabaseBinding = DatabaseBindingBase & {
|
|
123
141
|
lifecycleState: Exclude<LifecycleState, 'ready'>;
|
|
124
142
|
connectionMetadata?: Record<string, string | number | boolean>;
|
|
125
|
-
credentialRefs?: Record<string, CredentialRef>;
|
|
126
143
|
};
|
|
127
144
|
|
|
128
145
|
export type DatabaseBinding = ReadyDatabaseBinding | IncompleteDatabaseBinding;
|
|
@@ -152,13 +169,15 @@ export type PhysicalDatabaseInstance = {
|
|
|
152
169
|
id: string;
|
|
153
170
|
organizationId: string;
|
|
154
171
|
region: string;
|
|
155
|
-
environment: LifecycleEnvironment;
|
|
156
172
|
engine: DatabaseEngine; // postgres-only in V1
|
|
157
173
|
endpoint: string;
|
|
158
|
-
masterCredentialRef:
|
|
174
|
+
masterCredentialRef: PhysicalMasterCredentialRef;
|
|
159
175
|
capacityMax: number;
|
|
160
176
|
capacityUsed: number;
|
|
161
177
|
status: PhysicalDatabaseInstanceStatus;
|
|
178
|
+
// Stable customer OPA deployment identity that owns this physical pool.
|
|
179
|
+
// Absent on legacy rows (no backfill); agent-scoped lookups skip those.
|
|
180
|
+
agentName?: string;
|
|
162
181
|
metadata: Record<string, unknown>;
|
|
163
182
|
created?: Date;
|
|
164
183
|
updated?: Date;
|
|
@@ -183,6 +202,57 @@ export type LifecycleDispatchContinuation = {
|
|
|
183
202
|
reservationId?: string;
|
|
184
203
|
};
|
|
185
204
|
|
|
205
|
+
// Wizard-facing view of a physical pool provisioning run. Narrower than
|
|
206
|
+
// `LIFECYCLE_STATES` on purpose: the UI only distinguishes "nothing has run"
|
|
207
|
+
// from in-progress, done, and failed. `not_started` is the absence of a row;
|
|
208
|
+
// queued and running both read as `provisioning`.
|
|
209
|
+
export const PHYSICAL_POOL_PROVISIONING_STATUSES = ['not_started', 'provisioning', 'active', 'failed'] as const;
|
|
210
|
+
export type PhysicalPoolProvisioningStatus = (typeof PHYSICAL_POOL_PROVISIONING_STATUSES)[number];
|
|
211
|
+
|
|
212
|
+
export type PhysicalPoolProvisioningActor = {
|
|
213
|
+
displayName: string;
|
|
214
|
+
id: string;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type PhysicalPoolProvisioningError = {
|
|
218
|
+
code: string;
|
|
219
|
+
message: string;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Stable wizard/admin response for POST/GET physical-pool-provisioning.
|
|
223
|
+
export type PhysicalPoolProvisioningState = {
|
|
224
|
+
alreadyInProgress?: boolean;
|
|
225
|
+
completedAt?: string;
|
|
226
|
+
error?: PhysicalPoolProvisioningError;
|
|
227
|
+
physicalDatabaseInstanceId?: string;
|
|
228
|
+
provisioningId?: string;
|
|
229
|
+
startedAt?: string;
|
|
230
|
+
startedBy?: PhysicalPoolProvisioningActor;
|
|
231
|
+
status: PhysicalPoolProvisioningStatus;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// One physical instance as the admin surface reports it. Narrower than
|
|
235
|
+
// `PhysicalDatabaseInstance`: credentials and provider metadata are not part of
|
|
236
|
+
// an admin listing.
|
|
237
|
+
export type PhysicalPoolInstanceSummary = {
|
|
238
|
+
capacityMax: number;
|
|
239
|
+
capacityUsed: number;
|
|
240
|
+
endpoint: string;
|
|
241
|
+
id: string;
|
|
242
|
+
region: string;
|
|
243
|
+
status: PhysicalDatabaseInstanceStatus;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Every pool an organization has, whether it finished provisioning or not.
|
|
247
|
+
// A pool with no instances is one that is still running or that failed; a pool
|
|
248
|
+
// with instances and no run was provisioned before the wizard existed.
|
|
249
|
+
export type PhysicalPoolSummary = {
|
|
250
|
+
agentName: string;
|
|
251
|
+
engine: DatabaseEngine;
|
|
252
|
+
instances: PhysicalPoolInstanceSummary[];
|
|
253
|
+
provisioning: PhysicalPoolProvisioningState;
|
|
254
|
+
};
|
|
255
|
+
|
|
186
256
|
// Canonical wire payload for a lifecycle dispatch sent from the server to a
|
|
187
257
|
// lifecycle worker. The worker's `DispatchPayload` struct in
|
|
188
258
|
// orchestrator/pkg/databaselifecycle/dispatch.go decodes this JSON shape;
|
|
@@ -191,13 +261,17 @@ export type LifecycleDispatchContinuation = {
|
|
|
191
261
|
// The server describes WHAT (binding identity, desired spec, migrations,
|
|
192
262
|
// connection/credential context); the worker owns HOW (Terraform modules,
|
|
193
263
|
// state backends, credential resolvers, shared physical database instances — all resolved from
|
|
194
|
-
// the worker's local config
|
|
264
|
+
// the worker's single local lifecycle config, selected by the payload's profile).
|
|
195
265
|
//
|
|
196
266
|
// `migrations` is present iff the operation should consider migration
|
|
197
267
|
// state — `ensure_database` / `migrate_schema` dispatches carry an array
|
|
198
268
|
// (possibly empty); `retire_database` omits it. An omitted slice keeps the
|
|
199
269
|
// worker's `MigrationState` at the default "pending"; an empty slice means
|
|
200
270
|
// "vacuous truth, mark migrated"; a non-empty slice triggers the runner.
|
|
271
|
+
//
|
|
272
|
+
// For `ensure_physical_database_instance`, binding identity fields are empty
|
|
273
|
+
// placeholders (the OPA keys the lock on provisioningId) and `provisioningId`
|
|
274
|
+
// is required.
|
|
201
275
|
export type LifecycleDispatchPayload = {
|
|
202
276
|
applicationId: string;
|
|
203
277
|
bindingId: string;
|
|
@@ -206,46 +280,46 @@ export type LifecycleDispatchPayload = {
|
|
|
206
280
|
continuation?: LifecycleDispatchContinuation;
|
|
207
281
|
desiredSpec: DatabaseRequirement;
|
|
208
282
|
desiredSpecHash: string;
|
|
209
|
-
|
|
283
|
+
// Emitted only for `ensure_physical_database_instance`, and named to match
|
|
284
|
+
// the worker's `environment` field. The control plane no longer routes on an
|
|
285
|
+
// environment, but the worker still validates one before resolving pool
|
|
286
|
+
// topology and deriving its Terraform state key. It is a fixed value the
|
|
287
|
+
// server supplies rather than anything a caller picks, and it goes away with
|
|
288
|
+
// the worker's remaining shims in ENG-5127.
|
|
289
|
+
environment?: string;
|
|
210
290
|
migrations?: LifecycleMigration[];
|
|
211
291
|
operation: LifecycleOperation;
|
|
212
292
|
profile: string;
|
|
293
|
+
provisioningId?: string;
|
|
213
294
|
requestId: string;
|
|
214
295
|
resourceKey: string;
|
|
215
|
-
runtimeCredentialRefs?: Record<string, CredentialRef>;
|
|
216
|
-
migrationCredentialRefs?: Record<string, CredentialRef>;
|
|
217
296
|
};
|
|
218
297
|
|
|
298
|
+
export function physicalPoolProvisioningResourceKey(provisioningId: string): string {
|
|
299
|
+
return `physical-pool-provisioning:${provisioningId}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
219
302
|
export function computeRequirementKey(requirement: Pick<DatabaseRequirement, 'logicalName' | 'engine'>): string {
|
|
220
303
|
return `${slugify(requirement.logicalName)}~${encodeURIComponent(requirement.logicalName)}:${requirement.engine}`;
|
|
221
304
|
}
|
|
222
305
|
|
|
223
306
|
// binding_key is the product identity of a database in the control plane, and
|
|
224
|
-
// is deliberately scoped to (organization, application,
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
export function computeBindingKey(input: {
|
|
231
|
-
organizationId: string;
|
|
232
|
-
applicationId: string;
|
|
233
|
-
environment: LifecycleEnvironment;
|
|
234
|
-
profile: string;
|
|
235
|
-
}): string {
|
|
307
|
+
// is deliberately scoped to (organization, application, profile) only — NOT the
|
|
308
|
+
// logical name. This is what makes "one database per profile" enforceable by the
|
|
309
|
+
// plain UNIQUE (binding_key) constraint: asking for another database under a
|
|
310
|
+
// different logical name resolves to the same binding_key instead of minting a
|
|
311
|
+
// new one. The logical name still lives on the binding row and in resource_key
|
|
312
|
+
// (physical identity); it just doesn't grant a second slot.
|
|
313
|
+
export function computeBindingKey(input: { organizationId: string; applicationId: string; profile: string }): string {
|
|
236
314
|
return [input.organizationId, ...bindingKeySegments(input)].join(':');
|
|
237
315
|
}
|
|
238
316
|
|
|
239
|
-
export function computeLegacyBindingKeyWithoutOrganization(input: {
|
|
240
|
-
applicationId: string;
|
|
241
|
-
environment: LifecycleEnvironment;
|
|
242
|
-
profile: string;
|
|
243
|
-
}): string {
|
|
317
|
+
export function computeLegacyBindingKeyWithoutOrganization(input: { applicationId: string; profile: string }): string {
|
|
244
318
|
return bindingKeySegments(input).join(':');
|
|
245
319
|
}
|
|
246
320
|
|
|
247
|
-
function bindingKeySegments(input: { applicationId: string;
|
|
248
|
-
return [input.applicationId,
|
|
321
|
+
function bindingKeySegments(input: { applicationId: string; profile: string }): string[] {
|
|
322
|
+
return [input.applicationId, `${slugify(input.profile)}~${encodeURIComponent(input.profile)}`];
|
|
249
323
|
}
|
|
250
324
|
|
|
251
325
|
// Hashes only the infrastructure spec. migrationDirectory is deliberately
|
|
@@ -279,9 +353,9 @@ export function parseNativeDbIamDescriptor(value: unknown): NativeDbIamDescripto
|
|
|
279
353
|
return undefined;
|
|
280
354
|
}
|
|
281
355
|
|
|
282
|
-
const databaseIdentifier = typeof value.database === 'string' ? /^sbndb_([0-9a-f]{
|
|
356
|
+
const databaseIdentifier = typeof value.database === 'string' ? /^sbndb_([0-9a-f]{16})_([0-9a-f]{24})$/.exec(value.database) : null;
|
|
283
357
|
const usernameIdentifier =
|
|
284
|
-
typeof value.username === 'string' ? /^sbndb_([0-9a-f]{
|
|
358
|
+
typeof value.username === 'string' ? /^sbndb_([0-9a-f]{16})_([0-9a-f]{24})_runtime$/.exec(value.username) : null;
|
|
285
359
|
const connectorRoleArn =
|
|
286
360
|
typeof value.connector_role_arn === 'string'
|
|
287
361
|
? /^arn:aws:iam::(\d{12}):role\/(?:[\w+=,.@-]+\/)*[\w+=,.@-]+$/.exec(value.connector_role_arn)
|
|
@@ -300,7 +374,7 @@ export function parseNativeDbIamDescriptor(value: unknown): NativeDbIamDescripto
|
|
|
300
374
|
!databaseIdentifier ||
|
|
301
375
|
!usernameIdentifier ||
|
|
302
376
|
databaseIdentifier[1] !== usernameIdentifier[1] ||
|
|
303
|
-
|
|
377
|
+
usernameIdentifier[0] !== databaseIdentifier[0] + '_runtime' ||
|
|
304
378
|
!isValidNativeDbRdsHostname(value.host, value.region) ||
|
|
305
379
|
typeof value.port !== 'number' ||
|
|
306
380
|
!Number.isInteger(value.port) ||
|
|
@@ -331,18 +405,15 @@ export async function computeNativeDbApplicationToken(trustedApplicationId: stri
|
|
|
331
405
|
return (await computeNativeDbIdentifierToken('application', trustedApplicationId)).slice(0, 24);
|
|
332
406
|
}
|
|
333
407
|
|
|
334
|
-
export async function
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
throw new Error('Native database deployment token must be 12 lowercase hexadecimal characters');
|
|
341
|
-
}
|
|
342
|
-
return `sbndb_${deploymentToken}_${await computeNativeDbBindingToken(trustedDatabaseId)}`;
|
|
408
|
+
export async function computeNativeDbProfileToken(profile: string): Promise<string> {
|
|
409
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(profile.toLowerCase()));
|
|
410
|
+
return Array.from(new Uint8Array(digest))
|
|
411
|
+
.slice(0, 8)
|
|
412
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
413
|
+
.join('');
|
|
343
414
|
}
|
|
344
415
|
|
|
345
|
-
async function computeNativeDbIdentifierToken(kind: 'application'
|
|
416
|
+
async function computeNativeDbIdentifierToken(kind: 'application', trustedId: string): Promise<string> {
|
|
346
417
|
if (!isNonEmptyString(trustedId)) {
|
|
347
418
|
throw new Error(`Trusted native database ${kind} ID must be a non-empty string`);
|
|
348
419
|
}
|
|
@@ -364,7 +435,6 @@ export function computeResourceKey(input: {
|
|
|
364
435
|
organizationId: string;
|
|
365
436
|
applicationId: string;
|
|
366
437
|
requirementKey: string;
|
|
367
|
-
environment: LifecycleEnvironment;
|
|
368
438
|
profile: string;
|
|
369
439
|
actorScope?: string;
|
|
370
440
|
}): string {
|
|
@@ -372,7 +442,6 @@ export function computeResourceKey(input: {
|
|
|
372
442
|
input.organizationId,
|
|
373
443
|
input.applicationId,
|
|
374
444
|
input.requirementKey,
|
|
375
|
-
input.environment,
|
|
376
445
|
`${slugify(input.profile)}~${encodeURIComponent(input.profile)}`,
|
|
377
446
|
input.actorScope ?? 'default'
|
|
378
447
|
].join('/');
|
|
@@ -424,7 +493,7 @@ function isCommercialAwsRegion(value: unknown): value is string {
|
|
|
424
493
|
}
|
|
425
494
|
|
|
426
495
|
function isNativeDbAuthDescriptorVersion(value: unknown): value is NativeDbIamDescriptor['auth_descriptor_version'] {
|
|
427
|
-
return value ===
|
|
496
|
+
return value === NATIVE_DB_AUTH_DESCRIPTOR_VERSION;
|
|
428
497
|
}
|
|
429
498
|
|
|
430
499
|
function isNonEmptyString(value: unknown): value is string {
|
|
@@ -445,20 +514,20 @@ function isValidNativeDbRdsHostname(host: unknown, region: unknown): host is str
|
|
|
445
514
|
return host.length <= 253 && host === host.toLowerCase() && prefix !== '' && host.split('.').every((label) => validLabel.test(label));
|
|
446
515
|
}
|
|
447
516
|
|
|
448
|
-
function redact(value: unknown,
|
|
517
|
+
function redact(value: unknown, preservePhysicalMasterCredentialRef = false): unknown {
|
|
449
518
|
if (Array.isArray(value)) {
|
|
450
|
-
return value.map((entry) => redact(entry,
|
|
519
|
+
return value.map((entry) => redact(entry, preservePhysicalMasterCredentialRef));
|
|
451
520
|
}
|
|
452
521
|
if (value && typeof value === 'object') {
|
|
453
522
|
return Object.fromEntries(
|
|
454
523
|
Object.entries(value).map(([key, entryValue]) => {
|
|
455
|
-
if (
|
|
524
|
+
if (isPhysicalMasterCredentialRefKey(key)) {
|
|
456
525
|
return [key, redact(entryValue, true)];
|
|
457
526
|
}
|
|
458
|
-
if (!
|
|
527
|
+
if (!preservePhysicalMasterCredentialRef && SECRET_KEY_PATTERN.test(key)) {
|
|
459
528
|
return [key, '[REDACTED]'];
|
|
460
529
|
}
|
|
461
|
-
return [key, redact(entryValue,
|
|
530
|
+
return [key, redact(entryValue, preservePhysicalMasterCredentialRef)];
|
|
462
531
|
})
|
|
463
532
|
);
|
|
464
533
|
}
|
|
@@ -467,11 +536,9 @@ function redact(value: unknown, preserveCredentialRefs = false): unknown {
|
|
|
467
536
|
|
|
468
537
|
// Shared secret-key tripwire: keys whose name strongly suggests a raw secret
|
|
469
538
|
// value. Used by `redactLifecycleSecrets` to scrub logs/error reporting and by
|
|
470
|
-
// the server-side `assertNoRawCredentialMaterial` guard to refuse callbacks
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
// `connectionMetadata` and `CredentialRef` for `credentialRefs`; this catches
|
|
474
|
-
// obvious misuse only.
|
|
539
|
+
// the server-side `assertNoRawCredentialMaterial` guard to refuse callbacks.
|
|
540
|
+
// NOT a security boundary -- callers must still send primitive
|
|
541
|
+
// `connectionMetadata`; this catches obvious misuse only.
|
|
475
542
|
export const SECRET_KEY_PATTERN = /password|secret|token|private[_-]?key|dsn/i;
|
|
476
543
|
|
|
477
544
|
// Schemes whose URI form embeds credentials before `@`. Used as a value-side
|
|
@@ -513,6 +580,6 @@ const DSN_WITH_INLINE_AUTH = new RegExp(`(?:${DSN_SCHEME_GROUP}):\\/\\/[^/@\\s:]
|
|
|
513
580
|
const CREDENTIAL_KV_PATTERN = /(?:password|pwd|token|secret|private[_-]?key|api[_-]?key)\s*=\s*[^;&\s"]+/i;
|
|
514
581
|
const JSON_ENCODED_SECRET_PATTERN = /["']\s*(?:password|secret|token|private[_-]?key|dsn)\s*["']\s*:/i;
|
|
515
582
|
|
|
516
|
-
function
|
|
517
|
-
return
|
|
583
|
+
function isPhysicalMasterCredentialRefKey(key: string): boolean {
|
|
584
|
+
return /^master[_-]?credential[_-]?ref$/i.test(key);
|
|
518
585
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { PhysicalDatabaseCapacityPolicy } from './capacityPolicy.js';
|
|
4
|
+
import { PhysicalPoolIdentityError, parsePhysicalPoolAgentNameTag } from './physicalPoolIdentity.js';
|
|
5
|
+
|
|
6
|
+
const policy = (agentName: string): PhysicalDatabaseCapacityPolicy => ({
|
|
7
|
+
agentName,
|
|
8
|
+
engine: 'postgres',
|
|
9
|
+
thresholdType: 'minAvailableCapacityPercent',
|
|
10
|
+
thresholdValue: 20
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('parsePhysicalPoolAgentNameTag', () => {
|
|
14
|
+
test('reads the pool a deployment named', () => {
|
|
15
|
+
expect(parsePhysicalPoolAgentNameTag(['customer-opa'])).toBe('customer-opa');
|
|
16
|
+
expect(parsePhysicalPoolAgentNameTag([' customer-opa '])).toBe('customer-opa');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('reports no identity for a deployment that named no pool', () => {
|
|
20
|
+
expect(parsePhysicalPoolAgentNameTag(undefined)).toBeUndefined();
|
|
21
|
+
expect(parsePhysicalPoolAgentNameTag([])).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('rejects a tag that is not one non-empty name', () => {
|
|
25
|
+
expect(() => parsePhysicalPoolAgentNameTag([''])).toThrow(PhysicalPoolIdentityError);
|
|
26
|
+
expect(() => parsePhysicalPoolAgentNameTag([' '])).toThrow(PhysicalPoolIdentityError);
|
|
27
|
+
expect(() => parsePhysicalPoolAgentNameTag(['customer-opa', 'other-opa'])).toThrow(/must name exactly one/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Replicas of one deployment publish the same registration.
|
|
31
|
+
test('accepts a name repeated across replicas', () => {
|
|
32
|
+
expect(parsePhysicalPoolAgentNameTag(['customer-opa', 'customer-opa'])).toBe('customer-opa');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('agreement with capacity policies', () => {
|
|
36
|
+
test('accepts a policy for the pool the deployment named', () => {
|
|
37
|
+
expect(parsePhysicalPoolAgentNameTag(['customer-opa'], [policy('customer-opa')])).toBe('customer-opa');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('rejects a policy for a pool the deployment does not own', () => {
|
|
41
|
+
expect(() => parsePhysicalPoolAgentNameTag(['customer-opa'], [policy('other-opa')])).toThrow(/must name the same pool/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// A deployment scales the pool it owns and nothing else. Accepting a
|
|
45
|
+
// foreign policy alongside a matching one would let proactive evaluation
|
|
46
|
+
// key off a pool the wizard would never resolve to.
|
|
47
|
+
test('rejects a foreign policy published alongside the deployments own', () => {
|
|
48
|
+
expect(() => parsePhysicalPoolAgentNameTag(['customer-opa'], [policy('customer-opa'), policy('other-opa')])).toThrow(
|
|
49
|
+
/must name the same pool/
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Older workers publish identity only inside the policy.
|
|
54
|
+
test('falls back to the policy name when the deployment published no tag', () => {
|
|
55
|
+
expect(parsePhysicalPoolAgentNameTag(undefined, [policy('customer-opa')])).toBe('customer-opa');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('refuses to guess when policies alone name two pools', () => {
|
|
59
|
+
expect(() => parsePhysicalPoolAgentNameTag(undefined, [policy('customer-opa'), policy('other-opa')])).toThrow(
|
|
60
|
+
/publish databaseLifecycle:agentName/
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PhysicalDatabaseCapacityPolicy } from './capacityPolicy.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent tag key for the customer-managed deployment name that identifies a
|
|
5
|
+
* worker's physical database pool, `(organizationId, agentName, engine)`.
|
|
6
|
+
*
|
|
7
|
+
* Published on its own rather than read out of
|
|
8
|
+
* `databaseLifecycle:capacityPolicies`, so a customer who wants no proactive
|
|
9
|
+
* scaling still gets a resolvable pool. One value, e.g. `customer-opa`.
|
|
10
|
+
*/
|
|
11
|
+
export const DATABASE_LIFECYCLE_TAG_AGENT_NAME = 'databaseLifecycle:agentName';
|
|
12
|
+
|
|
13
|
+
export class PhysicalPoolIdentityError extends Error {
|
|
14
|
+
readonly code = 'physical_pool_identity_invalid';
|
|
15
|
+
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'PhysicalPoolIdentityError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The single physical pool a worker's registration names, or undefined when it
|
|
24
|
+
* published none.
|
|
25
|
+
*
|
|
26
|
+
* Throws {@link PhysicalPoolIdentityError} when the registration names more
|
|
27
|
+
* than one pool. Guessing between them would provision infrastructure under
|
|
28
|
+
* one deployment's Terraform state and hand it to another, so callers must
|
|
29
|
+
* either refuse the registration or withhold the identity.
|
|
30
|
+
*
|
|
31
|
+
* Falls back to the capacity policy's name when the tag is absent, for a
|
|
32
|
+
* worker predating {@link DATABASE_LIFECYCLE_TAG_AGENT_NAME} that is still
|
|
33
|
+
* rolling: the policy names the same pool, so identity stays unambiguous.
|
|
34
|
+
*/
|
|
35
|
+
export function parsePhysicalPoolAgentNameTag(
|
|
36
|
+
published: string[] | undefined,
|
|
37
|
+
capacityPolicies: readonly PhysicalDatabaseCapacityPolicy[] = []
|
|
38
|
+
): string | undefined {
|
|
39
|
+
if (published !== undefined && !Array.isArray(published)) {
|
|
40
|
+
throw new PhysicalPoolIdentityError(`${DATABASE_LIFECYCLE_TAG_AGENT_NAME} must be an array holding one deployment name.`);
|
|
41
|
+
}
|
|
42
|
+
const values = published ?? [];
|
|
43
|
+
if (values.some((value) => typeof value !== 'string' || value.trim() === '')) {
|
|
44
|
+
throw new PhysicalPoolIdentityError(`${DATABASE_LIFECYCLE_TAG_AGENT_NAME} must be a non-empty string when provided.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const declared = new Set(values.map((value) => value.trim()));
|
|
48
|
+
if (declared.size > 1) {
|
|
49
|
+
throw new PhysicalPoolIdentityError(
|
|
50
|
+
`${DATABASE_LIFECYCLE_TAG_AGENT_NAME} names ${declared.size} pools (${[...declared].sort().join(', ')}); it must name exactly one.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const policyNames = new Set(capacityPolicies.map((policy) => policy.agentName));
|
|
55
|
+
const [declaredName] = declared;
|
|
56
|
+
if (declaredName !== undefined) {
|
|
57
|
+
// Every policy has to be for the pool this deployment owns. A policy for
|
|
58
|
+
// another pool would have proactive evaluation scaling something the
|
|
59
|
+
// deployment cannot provision into.
|
|
60
|
+
if ([...policyNames].some((policyName) => policyName !== declaredName)) {
|
|
61
|
+
throw new PhysicalPoolIdentityError(
|
|
62
|
+
`${DATABASE_LIFECYCLE_TAG_AGENT_NAME} is ${declaredName} but databaseLifecycle:capacityPolicies names ${[...policyNames].sort().join(', ')}; they must name the same pool.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return declaredName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (policyNames.size > 1) {
|
|
69
|
+
throw new PhysicalPoolIdentityError(
|
|
70
|
+
`databaseLifecycle:capacityPolicies names ${policyNames.size} pools; publish ${DATABASE_LIFECYCLE_TAG_AGENT_NAME} to say which one this deployment owns.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const [policyName] = policyNames;
|
|
74
|
+
return policyName;
|
|
75
|
+
}
|
package/src/index.ts
CHANGED