@orchestraworks/cli 0.5.7 → 0.6.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/README.md +1 -1
- package/dist/cli/api/approval.js +3 -3
- package/dist/cli/commands/outpost/local.js +3 -3
- package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
- package/dist/cli/commands/outpost/machine-names.js +22 -0
- package/dist/cli/commands/outpost/up.js +7 -5
- package/dist/cli/commands/worker/enroll.d.ts +4 -3
- package/dist/cli/commands/worker/enroll.js +4 -3
- package/dist/cli/core/auth.d.ts +22 -2
- package/dist/cli/core/auth.js +69 -8
- package/dist/cli/core/errors.js +4 -0
- package/dist/cli/core/platform-client.d.ts +3 -2
- package/dist/cli/core/session-body.d.ts +1 -1
- package/dist/cli/core/session-body.js +1 -1
- package/dist/cli/firstRun.d.ts +3 -3
- package/dist/cli/firstRun.js +7 -5
- package/dist/cli/main.js +4 -3
- package/dist/constructs/environment.d.ts +24 -11
- package/dist/constructs/environment.js +76 -12
- package/dist/constructs/secret.d.ts +17 -7
- package/dist/constructs/secret.js +24 -16
- package/dist/core/labels.d.ts +32 -6
- package/dist/core/labels.js +70 -8
- package/dist/engine/diff.js +38 -11
- package/dist/engine/gateway-key-reconcile.d.ts +1 -2
- package/dist/engine/gateway-key-reconcile.js +19 -15
- package/dist/engine/pipeline-reconcile.js +2 -2
- package/dist/engine/pipeline-workflow-reconcile.js +15 -9
- package/dist/engine/reconcile.js +48 -15
- package/dist/engine/refusals.d.ts +0 -5
- package/dist/engine/refusals.js +1 -5
- package/dist/engine/secrets-apply.d.ts +10 -14
- package/dist/engine/secrets-apply.js +12 -17
- package/dist/engine/workflow-reconcile.d.ts +1 -1
- package/dist/generated/cli-commands.json +170 -34
- package/dist/generated/providers.d.ts +40 -0
- package/dist/generated/providers.js +30 -0
- package/docs/REFERENCE.md +26 -12
- package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/package.json +1 -1
- package/package.json +4 -4
- package/skills/orchestra/SKILL.md +38 -17
- package/skills/orchestra-fdk/references/authoring.md +34 -26
- package/skills/orchestra-fdk/references/environment.md +49 -12
- package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
- package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
- package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
- package/skills/orchestra-fdk/references/troubleshooting.md +5 -3
- package/templates/bug-fix/main.ts +7 -4
- package/templates/engineering-team/lib/environment.ts +6 -3
- package/templates/engineering-team/package.json +1 -1
- package/templates/todo-factory/main.ts +7 -4
- package/templates/todo-factory/package.json +1 -1
|
@@ -130,10 +130,30 @@ function normalizePaths(ruleName, paths) {
|
|
|
130
130
|
}
|
|
131
131
|
return [...paths];
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The variables {@link EgressPolicy.routeModelsViaGateway} sets to the key Secret's reference: claude sends the
|
|
135
|
+
* first as its key, and the worker writes the second into codex's own auth file.
|
|
136
|
+
*/
|
|
137
|
+
const GATEWAY_KEY_ENV = ["ANTHROPIC_API_KEY", "ORCHESTRA_WORKER_OPENAI_API_KEY"];
|
|
138
|
+
/** The edge reads this prefix where a request's token goes, and adds the credential the rest names. */
|
|
139
|
+
const REFERENCE_PREFIX = "orchestra:";
|
|
140
|
+
/**
|
|
141
|
+
* A Secret or a Connection as the platform names it, `secret/<id>` or `connection/<id>`, the id a reference
|
|
142
|
+
* token or the import as written until apply resolves it. Undefined for anything else.
|
|
143
|
+
*/
|
|
144
|
+
function credentialReference(value) {
|
|
145
|
+
if (typeof value !== "object" || value === null)
|
|
146
|
+
return undefined;
|
|
147
|
+
if ("connectionId" in value && typeof value.connectionId === "string")
|
|
148
|
+
return `connection/${value.connectionId}`;
|
|
149
|
+
if ("secretId" in value && typeof value.secretId === "string")
|
|
150
|
+
return `secret/${value.secretId}`;
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
133
153
|
/**
|
|
134
154
|
* A rule that still names a credential is an authored statement that can no longer apply, so it fails here
|
|
135
|
-
* with the Secret declaration that replaces it: the
|
|
136
|
-
*
|
|
155
|
+
* with the Secret declaration that replaces it: the binder's form becomes the Secret's `injection`, and the rule's
|
|
156
|
+
* hosts its `injection.hosts`.
|
|
137
157
|
*/
|
|
138
158
|
function refuseRuleCredential(ruleName, options, hosts) {
|
|
139
159
|
const declared = options.credentials !== undefined
|
|
@@ -148,7 +168,7 @@ function refuseRuleCredential(ruleName, options, hosts) {
|
|
|
148
168
|
const remedies = declared.map((entry) => secretRemedy(entry, hosts));
|
|
149
169
|
throw new Error(`egress rule '${ruleName}': a rule carries no credential, so 'credential'/'credentials' is not accepted. ` +
|
|
150
170
|
`A credential declares where and how it is presented on its own resource: ` +
|
|
151
|
-
`${remedies.length > 0 ? remedies.join("; ") : "give the Secret '
|
|
171
|
+
`${remedies.length > 0 ? remedies.join("; ") : "give the Secret an 'injection' recipe with its 'hosts'"}. ` +
|
|
152
172
|
`Then keep this rule as a network rule, with no credential.`);
|
|
153
173
|
}
|
|
154
174
|
/** The Secret declaration one binder stands for, with no value: a recipe and host names only. */
|
|
@@ -157,7 +177,7 @@ function secretRemedy(entry, hosts) {
|
|
|
157
177
|
? entry._credentialBinding()
|
|
158
178
|
: undefined;
|
|
159
179
|
if (binding === undefined)
|
|
160
|
-
return "give the Secret '
|
|
180
|
+
return "give the Secret an 'injection' recipe with its 'hosts'";
|
|
161
181
|
const name = isRefToken(binding.secretId)
|
|
162
182
|
? `Secret '${parseRefToken(binding.secretId).path.join("/")}'`
|
|
163
183
|
: `secret '${binding.secretId}'`;
|
|
@@ -166,7 +186,7 @@ function secretRemedy(entry, hosts) {
|
|
|
166
186
|
: binding.header !== undefined
|
|
167
187
|
? JSON.stringify({ headers: [{ name: binding.header, value: binding.valueTemplate }] })
|
|
168
188
|
: "its own injection recipe";
|
|
169
|
-
return `${name} takes hosts: ${JSON.stringify(hosts)}
|
|
189
|
+
return `${name} takes injection: ${injection}, with hosts: ${JSON.stringify(hosts)} inside it`;
|
|
170
190
|
}
|
|
171
191
|
/**
|
|
172
192
|
* **The builder is mutating, and every chaining method returns `this`, not a copy.** `denyAll()` or
|
|
@@ -181,6 +201,7 @@ function secretRemedy(entry, hosts) {
|
|
|
181
201
|
export class EgressPolicy {
|
|
182
202
|
defaultAction;
|
|
183
203
|
rules = [];
|
|
204
|
+
gatewayKey;
|
|
184
205
|
constructor(defaultAction) {
|
|
185
206
|
this.defaultAction = defaultAction;
|
|
186
207
|
}
|
|
@@ -300,18 +321,27 @@ export class EgressPolicy {
|
|
|
300
321
|
}
|
|
301
322
|
if (key instanceof Secret) {
|
|
302
323
|
const remedy = `Declare it as new Secret(scope, id, { value: SecretValue.fromGatewayKey(gatewayKey), ` +
|
|
303
|
-
`
|
|
304
|
-
`removeHeaders: ["x-api-key"] } }).`;
|
|
324
|
+
`injection: { headers: [{ name: "Authorization", value: "Bearer \${secret}" }], ` +
|
|
325
|
+
`removeHeaders: ["x-api-key"], hosts: ["${host}"] } }).`;
|
|
305
326
|
if (!key._presentsHeader()) {
|
|
306
327
|
throw new Error(`routeModelsViaGateway(): key Secret '${key.node.path.join("/")}' has no 'injection' header, so the ` +
|
|
307
328
|
`gateway key is never presented. ${remedy}`);
|
|
308
329
|
}
|
|
309
330
|
if (!key._hosts().some((pattern) => hostPatternCovers(pattern, host))) {
|
|
310
331
|
throw new Error(`routeModelsViaGateway(): key Secret '${key.node.path.join("/")}' does not cover '${host}' in its ` +
|
|
311
|
-
`'hosts' ${JSON.stringify(key._hosts())}, so model calls would leave with no key. ${remedy}`);
|
|
332
|
+
`'injection.hosts' ${JSON.stringify(key._hosts())}, so model calls would leave with no key. ${remedy}`);
|
|
312
333
|
}
|
|
313
334
|
}
|
|
314
|
-
|
|
335
|
+
this.allow(DEFAULT_MODEL_GATEWAY_RULE_NAME, { hosts: [host], priority: DEFAULT_MODEL_GATEWAY_PRIORITY });
|
|
336
|
+
this.gatewayKey = key;
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
/** @internal The variables this policy names a credential in, each set to its reference. */
|
|
340
|
+
_declaredEnv() {
|
|
341
|
+
if (this.gatewayKey === undefined)
|
|
342
|
+
return {};
|
|
343
|
+
const reference = REFERENCE_PREFIX + credentialReference(this.gatewayKey);
|
|
344
|
+
return Object.fromEntries(GATEWAY_KEY_ENV.map((name) => [name, reference]));
|
|
315
345
|
}
|
|
316
346
|
/**
|
|
317
347
|
* Each entry lands as an ordinary allow rule — the same wire shape, in the same `rules` array, in the same
|
|
@@ -403,6 +433,16 @@ export class Environment extends Construct {
|
|
|
403
433
|
constructor(scope, id, props) {
|
|
404
434
|
super(scope, id);
|
|
405
435
|
validateExplicitSlug(this, props.slug, "environment");
|
|
436
|
+
for (const [name, value] of Object.entries(props.env ?? {})) {
|
|
437
|
+
if (typeof value !== "string" && credentialReference(value) === undefined) {
|
|
438
|
+
throw new Error(`Environment '${id}': env '${name}' must be text, a Secret or a Connection.`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
props.credentials?.forEach((entry, index) => {
|
|
442
|
+
if (credentialReference(entry?.credential) === undefined) {
|
|
443
|
+
throw new Error(`Environment '${id}': credentials[${index}]: 'credential' must be a Secret or a Connection.`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
406
446
|
const declaredOnStartMachine = props.hooks?.onStartMachine?.script;
|
|
407
447
|
if (props.setup !== undefined && declaredOnStartMachine !== undefined && props.setup !== declaredOnStartMachine) {
|
|
408
448
|
throw new Error(`Environment '${id}' declares both setup and hooks.onStartMachine with different scripts. ` +
|
|
@@ -435,6 +475,9 @@ export class Environment extends Construct {
|
|
|
435
475
|
// Copied so a key added after the construct accepted the literal cannot ride a later synth into a
|
|
436
476
|
// machine's env.
|
|
437
477
|
...(props.env !== undefined ? { env: { ...props.env } } : {}),
|
|
478
|
+
...(props.credentials !== undefined
|
|
479
|
+
? { credentials: props.credentials.map((entry) => ({ ...entry })) }
|
|
480
|
+
: {}),
|
|
438
481
|
};
|
|
439
482
|
this.explicitSlug = props.slug;
|
|
440
483
|
this.displayName = props.name ?? id;
|
|
@@ -464,6 +507,19 @@ export class Environment extends Construct {
|
|
|
464
507
|
const resources = this.props.resources ?? {};
|
|
465
508
|
const cpu = resources.cpu ?? STANDARD_CPU;
|
|
466
509
|
const onStartMachine = this.props.hooks?.onStartMachine ?? (this.props.setup ? { script: this.props.setup } : undefined);
|
|
510
|
+
// A FRESH object each synth: emitting the stored map by reference would let a caller mutating one returned
|
|
511
|
+
// plan's `payload.env` change every later synth.
|
|
512
|
+
const env = {};
|
|
513
|
+
for (const [name, value] of Object.entries(this.props.env ?? {})) {
|
|
514
|
+
env[name] = typeof value === "string" ? value : REFERENCE_PREFIX + credentialReference(value);
|
|
515
|
+
}
|
|
516
|
+
for (const [name, reference] of Object.entries(this.props.egress._declaredEnv())) {
|
|
517
|
+
if (env[name] !== undefined && env[name] !== reference) {
|
|
518
|
+
throw new Error(`Environment '${this.node.id}': env '${name}' is declared by routeModelsViaGateway() as the gateway ` +
|
|
519
|
+
`key's reference, ${reference}. Drop it from env.`);
|
|
520
|
+
}
|
|
521
|
+
env[name] = reference;
|
|
522
|
+
}
|
|
467
523
|
// The egress rules, `ingressPolicy.endpoints`, `volumeMounts` and `env` are ALWAYS emitted — empty when
|
|
468
524
|
// unset — so clearing any of them reconciles through the platform's whole-object PATCH replace instead of
|
|
469
525
|
// being masked as omitted. `env` inherits the known map residual: emptying a declared map as the SOLE
|
|
@@ -518,9 +574,7 @@ export class Environment extends Construct {
|
|
|
518
574
|
}),
|
|
519
575
|
...(this.props.disks ?? []).map((disk) => ({ type: "disk", mountPath: disk.mountPath, size: disk.size })),
|
|
520
576
|
],
|
|
521
|
-
|
|
522
|
-
// returned plan's `payload.env` change every later synth.
|
|
523
|
-
env: { ...(this.props.env ?? {}) },
|
|
577
|
+
env,
|
|
524
578
|
// Always emitted — a token or an explicit null — so removing the credential reconciles to a cleared
|
|
525
579
|
// field. An omitted key would be dropped by JSON.stringify and the PATCH would read "unchanged".
|
|
526
580
|
imagePullSecretId: this.props.imagePullCredential?.secretId ?? null,
|
|
@@ -531,6 +585,16 @@ export class Environment extends Construct {
|
|
|
531
585
|
? { computeProviderId: this.props.computeProvider?.computeProviderId ?? null }
|
|
532
586
|
: {}),
|
|
533
587
|
},
|
|
588
|
+
// Only when declared, `[]` included: an omitted list must not clear defaults set in the console or by
|
|
589
|
+
// the platform's own seed.
|
|
590
|
+
...(this.props.credentials !== undefined
|
|
591
|
+
? {
|
|
592
|
+
credentials: this.props.credentials.map((entry) => ({
|
|
593
|
+
credential: credentialReference(entry.credential),
|
|
594
|
+
replace: entry.replace ?? true,
|
|
595
|
+
})),
|
|
596
|
+
}
|
|
597
|
+
: {}),
|
|
534
598
|
...(this.props.team !== undefined ? { teamId: this.props.team.teamId } : {}),
|
|
535
599
|
};
|
|
536
600
|
return { kind: "environment", path: this.node.path.join("/"), slug, managed: true, payload };
|
|
@@ -3,9 +3,14 @@ import { Construct } from "../core/construct.js";
|
|
|
3
3
|
import type { PlanResource, ValueFrom } from "../core/plan.js";
|
|
4
4
|
import type { CredentialBindingData, IGatewayKey, ISecret, ITeam } from "./interfaces.js";
|
|
5
5
|
/**
|
|
6
|
-
* The request mutations the broker applies from this secret: `headers` are injected after credential
|
|
7
|
-
* rendering, `removeHeaders` strips client-sent headers
|
|
8
|
-
*
|
|
6
|
+
* The request mutations the broker applies from this secret, and where: `headers` are injected after credential
|
|
7
|
+
* rendering, `removeHeaders` strips client-sent headers first, and `hosts` names the hosts the recipe applies on.
|
|
8
|
+
* Both lists are optional here and `[]` on the wire, so an inject-only recipe round-trips against the server's
|
|
9
|
+
* shape. A secret with no recipe is stored only: the broker presents it on no request.
|
|
10
|
+
*
|
|
11
|
+
* For git over HTTPS, store `base64("<user>:<token>")` and present it as `Basic ${secret}`; GitHub's user is
|
|
12
|
+
* `x-access-token`. Encode it first, `printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n'`, or let
|
|
13
|
+
* `SecretValue.basicAuth` encode it at apply.
|
|
9
14
|
*/
|
|
10
15
|
export interface InjectionRecipeProps {
|
|
11
16
|
readonly headers: {
|
|
@@ -13,6 +18,12 @@ export interface InjectionRecipeProps {
|
|
|
13
18
|
readonly value: string;
|
|
14
19
|
}[];
|
|
15
20
|
readonly removeHeaders?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* The hosts this secret is presented on, in egress rule host syntax: an exact host, `*.suffix`, or `*`. A
|
|
23
|
+
* request to a covered host gets the `headers` at the egress edge; an environment's rules only decide whether
|
|
24
|
+
* the request may leave. Omitted, the recipe covers no host.
|
|
25
|
+
*/
|
|
26
|
+
readonly hosts?: string[];
|
|
16
27
|
}
|
|
17
28
|
/**
|
|
18
29
|
* Shared with the egress `inject()` binding factory so a bad recipe fails at synth identically whether it
|
|
@@ -84,12 +95,11 @@ export interface SecretProps {
|
|
|
84
95
|
slug?: string;
|
|
85
96
|
name?: string;
|
|
86
97
|
description?: string;
|
|
98
|
+
/** How and where the secret is presented. Omitted, the secret is stored only. */
|
|
87
99
|
injection?: InjectionRecipeProps;
|
|
88
100
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* decide whether the request may leave. Omitted, the secret covers no host, and apply sends `[]`, so
|
|
92
|
-
* removing the prop clears the list.
|
|
101
|
+
* @deprecated Declare `injection.hosts` instead. Accepted for one release and moved into the recipe; refused
|
|
102
|
+
* when the recipe declares a different list.
|
|
93
103
|
*/
|
|
94
104
|
hosts?: string[];
|
|
95
105
|
/** Fixed at create; undeclared, the secret is created in Shared. */
|
|
@@ -77,6 +77,7 @@ export function cloneInjectionRecipe(recipe) {
|
|
|
77
77
|
return {
|
|
78
78
|
headers: (recipe.headers ?? []).map((h) => ({ name: h.name, value: h.value })),
|
|
79
79
|
removeHeaders: [...recipe.removeHeaders],
|
|
80
|
+
...(recipe.hosts !== undefined ? { hosts: [...recipe.hosts] } : {}),
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
@@ -319,16 +320,25 @@ function isIpv6Literal(host) {
|
|
|
319
320
|
/**
|
|
320
321
|
* The client-side mirror of the platform's write-time `hosts` checks, so a bad list fails at synth: each
|
|
321
322
|
* entry is an exact host, `*.` and a suffix, or `*`, and a secret that covers a host needs a recipe header to
|
|
322
|
-
* be presented by. The
|
|
323
|
+
* be presented by. The deprecated top-level `hosts` is moved into the recipe, and refused when the recipe
|
|
324
|
+
* declares a different list, an empty one included. The platform-origin and object-store refusals need the deployment's own addresses, so they
|
|
323
325
|
* stay the platform's `400`.
|
|
324
326
|
*/
|
|
325
|
-
function normalizeSecretHosts(
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
function normalizeSecretHosts(recipeHosts, deprecatedHosts, injection, label) {
|
|
328
|
+
if (recipeHosts !== undefined)
|
|
329
|
+
normalizeCredentialHosts(recipeHosts, `${label}: injection`);
|
|
330
|
+
if (deprecatedHosts !== undefined)
|
|
331
|
+
normalizeCredentialHosts(deprecatedHosts, label);
|
|
332
|
+
if (recipeHosts !== undefined &&
|
|
333
|
+
deprecatedHosts !== undefined &&
|
|
334
|
+
(recipeHosts.length !== deprecatedHosts.length || recipeHosts.some((host, index) => host !== deprecatedHosts[index]))) {
|
|
335
|
+
throw new Error(`${label}: 'hosts' and 'injection.hosts' name different hosts; declare 'injection.hosts' alone.`);
|
|
336
|
+
}
|
|
337
|
+
const hosts = recipeHosts ?? deprecatedHosts ?? [];
|
|
329
338
|
if (hosts.length > 0 && (injection === undefined || (injection.headers ?? []).length === 0)) {
|
|
330
|
-
throw new Error(`${label}: a secret with
|
|
331
|
-
`is presented, such as injection: { headers: [{ name: "Authorization", value: "Bearer \${secret}" }]
|
|
339
|
+
throw new Error(`${label}: a secret with hosts needs an 'injection' recipe with at least one header, which is how it ` +
|
|
340
|
+
`is presented, such as injection: { headers: [{ name: "Authorization", value: "Bearer \${secret}" }], ` +
|
|
341
|
+
`hosts: ["api.example.com"] }.`);
|
|
332
342
|
}
|
|
333
343
|
return [...hosts];
|
|
334
344
|
}
|
|
@@ -337,7 +347,6 @@ export class Secret extends Construct {
|
|
|
337
347
|
secretId;
|
|
338
348
|
#valueFrom;
|
|
339
349
|
#injection;
|
|
340
|
-
#hosts;
|
|
341
350
|
explicitSlug;
|
|
342
351
|
displayName;
|
|
343
352
|
description;
|
|
@@ -347,8 +356,9 @@ export class Secret extends Construct {
|
|
|
347
356
|
validateExplicitSlug(this, props.slug, "secret");
|
|
348
357
|
const label = `secret '${this.node.path.join("/")}'`;
|
|
349
358
|
this.#valueFrom = props.value._toValueFrom();
|
|
350
|
-
|
|
351
|
-
|
|
359
|
+
const recipe = props.injection === undefined ? undefined : normalizeInjectionRecipe(props.injection, label);
|
|
360
|
+
const hosts = normalizeSecretHosts(props.injection?.hosts, props.hosts, recipe, label);
|
|
361
|
+
this.#injection = recipe === undefined ? undefined : { ...recipe, hosts };
|
|
352
362
|
this.explicitSlug = props.slug;
|
|
353
363
|
this.displayName = props.name ?? id;
|
|
354
364
|
this.description = props.description ?? "";
|
|
@@ -368,7 +378,7 @@ export class Secret extends Construct {
|
|
|
368
378
|
}
|
|
369
379
|
/** @internal — a fresh copy of the declared hosts, for synth checks that read a declared secret. */
|
|
370
380
|
_hosts() {
|
|
371
|
-
return [...this.#hosts];
|
|
381
|
+
return [...(this.#injection?.hosts ?? [])];
|
|
372
382
|
}
|
|
373
383
|
/** @internal — whether the declared recipe has a header to present this secret by. */
|
|
374
384
|
_presentsHeader() {
|
|
@@ -385,16 +395,14 @@ export class Secret extends Construct {
|
|
|
385
395
|
const slug = this.slug();
|
|
386
396
|
// `description` is ALWAYS emitted, empty when unset, so it is never omit-masked; the only Secret PATCH
|
|
387
397
|
// rides a value overwrite that sends name+description together, so a clear travels with that write.
|
|
388
|
-
// `
|
|
389
|
-
// converges on the declaration.
|
|
390
|
-
//
|
|
391
|
-
// is DEEP-COPIED because the stored recipe is shared across synths: emitting it by reference would let a
|
|
398
|
+
// `injection` is emitted only when declared, its `hosts` always, `[]` when undeclared, so a live recipe with
|
|
399
|
+
// other hosts converges on the declaration and a dropped recipe is cleared, leaving the secret stored only.
|
|
400
|
+
// It is DEEP-COPIED because the stored recipe is shared across synths: emitting it by reference would let a
|
|
392
401
|
// caller mutating one returned plan corrupt the validated recipe for every later synth.
|
|
393
402
|
const payload = {
|
|
394
403
|
name: this.displayName,
|
|
395
404
|
slug,
|
|
396
405
|
description: this.description,
|
|
397
|
-
hosts: [...this.#hosts],
|
|
398
406
|
...(this.#injection !== undefined ? { injection: cloneInjectionRecipe(this.#injection) } : {}),
|
|
399
407
|
...(this.team !== undefined ? { teamId: this.team.teamId } : {}),
|
|
400
408
|
};
|
package/dist/core/labels.d.ts
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
* Reserving the whole prefix rather than one literal key is what makes the ownership marker unforgeable and
|
|
3
3
|
* leaves room for the other markers `apply` writes without a second rule.
|
|
4
4
|
*/
|
|
5
|
-
export declare const LABEL_KEY_PREFIX = "
|
|
5
|
+
export declare const LABEL_KEY_PREFIX = "orchestra.";
|
|
6
|
+
/**
|
|
7
|
+
* The prefix `apply` wrote before the rename. A factory applied then carries its markers under it until its next
|
|
8
|
+
* apply moves them, so every marker is read under both prefixes, the new key winning, and both stay reserved.
|
|
9
|
+
*/
|
|
10
|
+
export declare const OLD_LABEL_KEY_PREFIX = "sfdk.";
|
|
6
11
|
/**
|
|
7
12
|
* Stamped on every resource `apply` creates or adopts among the kinds whose API request exposes `labels`.
|
|
8
13
|
* Its value is the root factory name, which is always present — even for a factory that declares no team — so a CLI
|
|
9
14
|
* write against a managed resource can name the owning factory verbatim.
|
|
10
15
|
*/
|
|
11
|
-
export declare const MANAGED_BY_LABEL_KEY = "
|
|
16
|
+
export declare const MANAGED_BY_LABEL_KEY = "orchestra.managedBy";
|
|
12
17
|
export declare function rejectTemplateBraces(map: Record<string, string> | undefined, where: string): void;
|
|
13
18
|
/**
|
|
14
19
|
* A synth-time rejection rather than an apply-time overwrite on purpose: a declared key that could shadow
|
|
15
|
-
*
|
|
16
|
-
* key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
20
|
+
* the managed-by marker, under either prefix, would make the ownership marker forgeable and make a re-apply
|
|
21
|
+
* fight itself over the same key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
17
22
|
*/
|
|
18
23
|
export declare function rejectReservedLabelKeys(map: Record<string, string> | undefined, where: string): void;
|
|
19
24
|
/** Constructs call this rather than the two halves, so a new rule lands everywhere at once. */
|
|
@@ -28,12 +33,33 @@ export declare function stampManagedBy(labels: Record<string, string> | undefine
|
|
|
28
33
|
* an explicit `null`. This record of the keys `apply` last wrote from a declaration is how a later apply
|
|
29
34
|
* tells a key the declaration dropped, which it removes, from a key someone else set, which it leaves alone.
|
|
30
35
|
*/
|
|
31
|
-
export declare const DECLARED_LABELS_KEY = "
|
|
36
|
+
export declare const DECLARED_LABELS_KEY = "orchestra.declaredLabels";
|
|
37
|
+
/**
|
|
38
|
+
* Set on a gateway key's paired Secret while a mint is in flight, so a mint whose token write never landed is
|
|
39
|
+
* seen and refused by name rather than read as a usable Secret.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = "orchestra.pendingGatewayKeyToken";
|
|
42
|
+
/** The key a marker had under the old prefix. */
|
|
43
|
+
export declare function oldMarkerKey(key: string): string;
|
|
44
|
+
/** A marker's value: under its key, else under the old prefix's, else `undefined`. */
|
|
45
|
+
export declare function readMarker(labels: Record<string, unknown> | null | undefined, key: string): string | undefined;
|
|
46
|
+
/** A merge patch removing each marker these labels carry under the old prefix. */
|
|
47
|
+
export declare function oldMarkersCleared(labels: Record<string, unknown> | null | undefined): Record<string, null>;
|
|
48
|
+
/**
|
|
49
|
+
* The merge patch that moves each marker found under the old prefix, for a resource whose labels apply does not
|
|
50
|
+
* otherwise reconcile: the old key sent as `null`, and the new key given the old value unless it is already
|
|
51
|
+
* live. `undefined` when no marker is under the old prefix.
|
|
52
|
+
*/
|
|
53
|
+
export declare function oldMarkersMoved(labels: Record<string, unknown> | null | undefined): Record<string, string | null> | undefined;
|
|
54
|
+
/** The factory whose apply stamped these labels, read under either prefix. */
|
|
55
|
+
export declare function managedByOf(labels: Record<string, unknown> | null | undefined): string | undefined;
|
|
32
56
|
/** A create's labels: the declaration, the record of its keys when it declares any, and the stamps. */
|
|
33
57
|
export declare function createLabels(declared: Record<string, string> | undefined, stamps?: Record<string, string>): Record<string, string>;
|
|
34
58
|
/**
|
|
35
59
|
* The merge patch that brings the live labels to the declaration, or `undefined` when they already agree: a
|
|
36
60
|
* declared value that differs, a key an earlier apply declared and this one does not (as `null`), the record
|
|
37
|
-
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift.
|
|
61
|
+
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift. A marker found under
|
|
62
|
+
* the old prefix is moved: its old key is sent as `null`, and its new key carries the stamp, or the old value
|
|
63
|
+
* when nothing stamps it.
|
|
38
64
|
*/
|
|
39
65
|
export declare function labelsPatch(declared: Record<string, string> | undefined, live: Record<string, string> | null | undefined, stamps?: Record<string, string>): Record<string, string | null> | undefined;
|
package/dist/core/labels.js
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Reserving the whole prefix rather than one literal key is what makes the ownership marker unforgeable and
|
|
5
5
|
* leaves room for the other markers `apply` writes without a second rule.
|
|
6
6
|
*/
|
|
7
|
-
export const LABEL_KEY_PREFIX = "
|
|
7
|
+
export const LABEL_KEY_PREFIX = "orchestra.";
|
|
8
|
+
/**
|
|
9
|
+
* The prefix `apply` wrote before the rename. A factory applied then carries its markers under it until its next
|
|
10
|
+
* apply moves them, so every marker is read under both prefixes, the new key winning, and both stay reserved.
|
|
11
|
+
*/
|
|
12
|
+
export const OLD_LABEL_KEY_PREFIX = "sfdk.";
|
|
13
|
+
const RESERVED_LABEL_KEY_PREFIXES = [LABEL_KEY_PREFIX, OLD_LABEL_KEY_PREFIX];
|
|
8
14
|
/**
|
|
9
15
|
* Stamped on every resource `apply` creates or adopts among the kinds whose API request exposes `labels`.
|
|
10
16
|
* Its value is the root factory name, which is always present — even for a factory that declares no team — so a CLI
|
|
@@ -24,16 +30,17 @@ export function rejectTemplateBraces(map, where) {
|
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* A synth-time rejection rather than an apply-time overwrite on purpose: a declared key that could shadow
|
|
27
|
-
*
|
|
28
|
-
* key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
33
|
+
* the managed-by marker, under either prefix, would make the ownership marker forgeable and make a re-apply
|
|
34
|
+
* fight itself over the same key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
29
35
|
*/
|
|
30
36
|
export function rejectReservedLabelKeys(map, where) {
|
|
31
37
|
if (map === undefined)
|
|
32
38
|
return;
|
|
33
39
|
for (const key of Object.keys(map)) {
|
|
34
|
-
|
|
40
|
+
const prefix = reservedPrefixOf(key);
|
|
41
|
+
if (prefix !== undefined) {
|
|
35
42
|
throw new Error(`Label key ${JSON.stringify(key)} in ${where} uses the reserved ` +
|
|
36
|
-
`"${
|
|
43
|
+
`"${prefix}" prefix, which belongs to orchestra itself (it is how apply marks the ` +
|
|
37
44
|
`resources it manages, e.g. ${JSON.stringify(MANAGED_BY_LABEL_KEY)}). Rename the key to ` +
|
|
38
45
|
`anything outside that prefix.`);
|
|
39
46
|
}
|
|
@@ -57,6 +64,51 @@ export function stampManagedBy(labels, factory) {
|
|
|
57
64
|
* tells a key the declaration dropped, which it removes, from a key someone else set, which it leaves alone.
|
|
58
65
|
*/
|
|
59
66
|
export const DECLARED_LABELS_KEY = `${LABEL_KEY_PREFIX}declaredLabels`;
|
|
67
|
+
/**
|
|
68
|
+
* Set on a gateway key's paired Secret while a mint is in flight, so a mint whose token write never landed is
|
|
69
|
+
* seen and refused by name rather than read as a usable Secret.
|
|
70
|
+
*/
|
|
71
|
+
export const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = `${LABEL_KEY_PREFIX}pendingGatewayKeyToken`;
|
|
72
|
+
const MARKER_KEYS = [MANAGED_BY_LABEL_KEY, DECLARED_LABELS_KEY, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY];
|
|
73
|
+
/** The key a marker had under the old prefix. */
|
|
74
|
+
export function oldMarkerKey(key) {
|
|
75
|
+
return OLD_LABEL_KEY_PREFIX + key.slice(LABEL_KEY_PREFIX.length);
|
|
76
|
+
}
|
|
77
|
+
/** A marker's value: under its key, else under the old prefix's, else `undefined`. */
|
|
78
|
+
export function readMarker(labels, key) {
|
|
79
|
+
const value = labels?.[key] ?? labels?.[oldMarkerKey(key)];
|
|
80
|
+
return typeof value === "string" ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
/** A merge patch removing each marker these labels carry under the old prefix. */
|
|
83
|
+
export function oldMarkersCleared(labels) {
|
|
84
|
+
return Object.fromEntries(MARKER_KEYS.map(oldMarkerKey)
|
|
85
|
+
.filter((key) => labels?.[key] !== undefined)
|
|
86
|
+
.map((key) => [key, null]));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The merge patch that moves each marker found under the old prefix, for a resource whose labels apply does not
|
|
90
|
+
* otherwise reconcile: the old key sent as `null`, and the new key given the old value unless it is already
|
|
91
|
+
* live. `undefined` when no marker is under the old prefix.
|
|
92
|
+
*/
|
|
93
|
+
export function oldMarkersMoved(labels) {
|
|
94
|
+
const patch = {};
|
|
95
|
+
for (const key of MARKER_KEYS) {
|
|
96
|
+
const old = labels?.[oldMarkerKey(key)];
|
|
97
|
+
if (old === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
patch[oldMarkerKey(key)] = null;
|
|
100
|
+
if (labels?.[key] === undefined && typeof old === "string")
|
|
101
|
+
patch[key] = old;
|
|
102
|
+
}
|
|
103
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
104
|
+
}
|
|
105
|
+
/** The factory whose apply stamped these labels, read under either prefix. */
|
|
106
|
+
export function managedByOf(labels) {
|
|
107
|
+
return readMarker(labels, MANAGED_BY_LABEL_KEY);
|
|
108
|
+
}
|
|
109
|
+
function reservedPrefixOf(key) {
|
|
110
|
+
return RESERVED_LABEL_KEY_PREFIXES.find((prefix) => key.startsWith(prefix));
|
|
111
|
+
}
|
|
60
112
|
/** A create's labels: the declaration, the record of its keys when it declares any, and the stamps. */
|
|
61
113
|
export function createLabels(declared, stamps = {}) {
|
|
62
114
|
return { ...withDeclaredKeys(declared ?? {}), ...stamps };
|
|
@@ -64,7 +116,9 @@ export function createLabels(declared, stamps = {}) {
|
|
|
64
116
|
/**
|
|
65
117
|
* The merge patch that brings the live labels to the declaration, or `undefined` when they already agree: a
|
|
66
118
|
* declared value that differs, a key an earlier apply declared and this one does not (as `null`), the record
|
|
67
|
-
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift.
|
|
119
|
+
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift. A marker found under
|
|
120
|
+
* the old prefix is moved: its old key is sent as `null`, and its new key carries the stamp, or the old value
|
|
121
|
+
* when nothing stamps it.
|
|
68
122
|
*/
|
|
69
123
|
export function labelsPatch(declared, live, stamps = {}) {
|
|
70
124
|
const current = live ?? {};
|
|
@@ -80,6 +134,14 @@ export function labelsPatch(declared, live, stamps = {}) {
|
|
|
80
134
|
}
|
|
81
135
|
if (!(DECLARED_LABELS_KEY in desired) && DECLARED_LABELS_KEY in current)
|
|
82
136
|
patch[DECLARED_LABELS_KEY] = null;
|
|
137
|
+
for (const key of MARKER_KEYS) {
|
|
138
|
+
const old = oldMarkerKey(key);
|
|
139
|
+
if (!(old in current))
|
|
140
|
+
continue;
|
|
141
|
+
patch[old] = null;
|
|
142
|
+
if (key !== DECLARED_LABELS_KEY && !(key in desired) && !(key in current))
|
|
143
|
+
patch[key] = current[old];
|
|
144
|
+
}
|
|
83
145
|
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
84
146
|
}
|
|
85
147
|
function withDeclaredKeys(declared) {
|
|
@@ -88,13 +150,13 @@ function withDeclaredKeys(declared) {
|
|
|
88
150
|
}
|
|
89
151
|
/** A malformed record removes nothing, and a reserved key is never removed through it. */
|
|
90
152
|
function previouslyDeclaredKeys(live) {
|
|
91
|
-
const record = live
|
|
153
|
+
const record = readMarker(live, DECLARED_LABELS_KEY);
|
|
92
154
|
if (record === undefined)
|
|
93
155
|
return [];
|
|
94
156
|
try {
|
|
95
157
|
const keys = JSON.parse(record);
|
|
96
158
|
return Array.isArray(keys)
|
|
97
|
-
? keys.filter((key) => typeof key === "string" &&
|
|
159
|
+
? keys.filter((key) => typeof key === "string" && reservedPrefixOf(key) === undefined)
|
|
98
160
|
: [];
|
|
99
161
|
}
|
|
100
162
|
catch {
|
package/dist/engine/diff.js
CHANGED
|
@@ -10,13 +10,12 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { CONNECTOR_SECRET_WIRE_FIELDS } from "../constructs/connector.js";
|
|
11
11
|
import { isEnvImportMarker, parseEnvImportMarker } from "../constructs/references.js";
|
|
12
12
|
import { gatewayKeyRefOf } from "../core/gateway-key-recipe.js";
|
|
13
|
-
import { labelsPatch, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
|
|
13
|
+
import { labelsPatch, MANAGED_BY_LABEL_KEY, oldMarkersMoved, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, readMarker, } from "../core/labels.js";
|
|
14
14
|
import { isRefToken, refToken } from "../core/tokens.js";
|
|
15
15
|
import { createTeamResolver, mapError, modelSettingsUnchanged, standbyCapacityUnchanged, platformErrorMessage, } from "./apply-core.js";
|
|
16
16
|
import { httpStatusOf } from "./client.js";
|
|
17
17
|
import { describeRefusal, describeStageEffects, leftoverRefusals, pipelineChange, pipelineUnchanged, previewPipelineChange, resolveGateReferences, workflowStagesAfterApply, } from "./pipeline-reconcile.js";
|
|
18
18
|
import { describeEntryChange, describeOverlaps, entriesUnchanged, pipelineEntryChange, proposedWorkflows, } from "./pipeline-workflow-reconcile.js";
|
|
19
|
-
import { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "./gateway-key-reconcile.js";
|
|
20
19
|
import { scanModelProviderKeys } from "./model-provider-key-reconcile.js";
|
|
21
20
|
import { computeProviderPatch, } from "./compute-provider-reconcile.js";
|
|
22
21
|
import { paginate } from "./pages.js";
|
|
@@ -25,7 +24,7 @@ import { reservedProviderSecretIds } from "./preflight.js";
|
|
|
25
24
|
import { gatewayKeyPairRefusal, refusalOf, } from "./refusals.js";
|
|
26
25
|
import { connectionUnchanged, environmentUnchanged, getBySlugOrUndefined, subsetDeepEqual, } from "./reconcile.js";
|
|
27
26
|
import { removedConfigurationNotice } from "./connector-configuration.js";
|
|
28
|
-
import { containsCommandLineRecipe,
|
|
27
|
+
import { containsCommandLineRecipe, declaredSecretInjection, metadataInjectionChange, pendingCredentialWrite, replacesLiveCredentialValue, resolveSecretValueSource, rootEnvName, } from "./secrets-apply.js";
|
|
29
28
|
/** Overridable for tests. */
|
|
30
29
|
export const ORPHAN_SCAN_CAP = 5;
|
|
31
30
|
/**
|
|
@@ -428,20 +427,30 @@ async function diffSecret(resource, pass) {
|
|
|
428
427
|
}
|
|
429
428
|
return entry(resource, "no-change", unsetNote, marks);
|
|
430
429
|
}
|
|
431
|
-
/** Value-free: a recipe and
|
|
430
|
+
/** Value-free: a recipe and its host list are shape, never credential material. */
|
|
432
431
|
const INJECTION_RECIPE_CHANGE = "injection recipe changes";
|
|
433
432
|
const HOSTS_CHANGE = "hosts change";
|
|
433
|
+
const OWNERSHIP_LABELS_MOVE = "ownership labels move to the orchestra. prefix";
|
|
434
434
|
function secretMetadataDetail(resource, live) {
|
|
435
435
|
const details = [
|
|
436
|
-
...(
|
|
437
|
-
|
|
438
|
-
: []),
|
|
439
|
-
...(metadataHostsChange(declaredSecretHosts(resource.payload), live.hosts) !== undefined ? [HOSTS_CHANGE] : []),
|
|
436
|
+
...secretRecipeDetails(resource, live),
|
|
437
|
+
...(oldMarkersMoved(live.labels) !== undefined ? [OWNERSHIP_LABELS_MOVE] : []),
|
|
440
438
|
];
|
|
441
439
|
return details.length > 0 ? details.join("; ") : undefined;
|
|
442
440
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
441
|
+
function secretRecipeDetails(resource, live) {
|
|
442
|
+
const declared = declaredSecretInjection(resource.payload);
|
|
443
|
+
if (metadataInjectionChange(declared, live.injection) === undefined)
|
|
444
|
+
return [];
|
|
445
|
+
if (declared === undefined || live.injection === undefined)
|
|
446
|
+
return [INJECTION_RECIPE_CHANGE];
|
|
447
|
+
const { hosts: desiredHosts = [], ...desiredRecipe } = declared;
|
|
448
|
+
const { hosts: liveHosts = [], ...liveRecipe } = live.injection;
|
|
449
|
+
const sameHosts = desiredHosts.length === liveHosts.length && desiredHosts.every((host, index) => host === liveHosts[index]);
|
|
450
|
+
return [
|
|
451
|
+
...(metadataInjectionChange(desiredRecipe, liveRecipe) !== undefined ? [INJECTION_RECIPE_CHANGE] : []),
|
|
452
|
+
...(sameHosts ? [] : [HOSTS_CHANGE]),
|
|
453
|
+
];
|
|
445
454
|
}
|
|
446
455
|
function withDetail(note, detail) {
|
|
447
456
|
return note === undefined ? detail : `${note}; ${detail}`;
|
|
@@ -480,7 +489,7 @@ async function diffGatewayKey(resource, plan, pass) {
|
|
|
480
489
|
return entry(resource, keyUnchanged(body, live) ? "no-change" : "update", undefined, marks);
|
|
481
490
|
}
|
|
482
491
|
function hasPendingMarker(secret) {
|
|
483
|
-
return secret?.labels
|
|
492
|
+
return readMarker(secret?.labels, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY) !== undefined;
|
|
484
493
|
}
|
|
485
494
|
/**
|
|
486
495
|
* Mirrors apply's `pairedSecretResource` lookup but returns `undefined` instead of throwing when the plan
|
|
@@ -630,6 +639,16 @@ async function diffEnvironment(resource, pass) {
|
|
|
630
639
|
if (typeof providerRef === "string" && providerRef.length > 0) {
|
|
631
640
|
launchSpec.computeProviderId = await resolveIdForDiff(providerRef, "compute-provider", pass);
|
|
632
641
|
}
|
|
642
|
+
// Each credential reference an env value or a default names, resolved to the id apply writes.
|
|
643
|
+
const env = (launchSpec.env ?? {});
|
|
644
|
+
for (const [name, value] of Object.entries(env)) {
|
|
645
|
+
if (!value.startsWith("orchestra:"))
|
|
646
|
+
continue;
|
|
647
|
+
env[name] = `orchestra:${await credentialReferenceForDiff(value.slice("orchestra:".length), pass)}`;
|
|
648
|
+
}
|
|
649
|
+
for (const entry of (body.credentials ?? [])) {
|
|
650
|
+
entry.credential = await credentialReferenceForDiff(String(entry.credential), pass);
|
|
651
|
+
}
|
|
633
652
|
const bound = pullSecrets.length === 0
|
|
634
653
|
? null
|
|
635
654
|
: verdictFor(resource, live, {
|
|
@@ -929,6 +948,14 @@ async function orphanReport(plan, apis, cap) {
|
|
|
929
948
|
function refKeyOf(resource) {
|
|
930
949
|
return refToken(resource.kind, resource.path.split("/"));
|
|
931
950
|
}
|
|
951
|
+
/** `secret/<ref>` or `connection/<ref>` with its ref resolved as {@link resolveIdForDiff} resolves one. */
|
|
952
|
+
async function credentialReferenceForDiff(text, pass) {
|
|
953
|
+
const match = /^(secret|connection)\/(.+)$/.exec(text);
|
|
954
|
+
if (match === null)
|
|
955
|
+
return text;
|
|
956
|
+
const kind = match[1];
|
|
957
|
+
return `${kind}/${await resolveIdForDiff(match[2], kind, pass)}`;
|
|
958
|
+
}
|
|
932
959
|
/** Lenient: any miss leaves the ref unchanged, so the compare simply reports `update`. */
|
|
933
960
|
async function resolveIdForDiff(ref, literalKind, pass) {
|
|
934
961
|
if (isRefToken(ref)) {
|
|
@@ -8,7 +8,7 @@ import { PreflightError } from "./preflight.js";
|
|
|
8
8
|
* second column — and is removed by the final token PATCH. It lives in a leaf module because the aggregate
|
|
9
9
|
* refusal classifier's rotation message names it too.
|
|
10
10
|
*/
|
|
11
|
-
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "
|
|
11
|
+
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "../core/labels.js";
|
|
12
12
|
/**
|
|
13
13
|
* The platform requires a non-empty `value` on create and the token does not exist yet, so a reservation
|
|
14
14
|
* writes this inert literal and the final PATCH replaces it. The Secret is detectably pending for exactly as
|
|
@@ -38,7 +38,6 @@ export interface LiveSecretRow {
|
|
|
38
38
|
id: string;
|
|
39
39
|
teamId?: string;
|
|
40
40
|
injection?: InjectionRecipe;
|
|
41
|
-
hosts?: string[];
|
|
42
41
|
labels?: Record<string, string>;
|
|
43
42
|
}
|
|
44
43
|
/** Value-free. */
|