@varde-flyt/vfac 0.5.0 → 0.7.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 +31 -0
- package/dist/vfac.mjs +295 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,6 +101,37 @@ service, exactly as it resolves the service's own credentials. So there is no
|
|
|
101
101
|
second copy to rotate, and no value for you to read — `vfac get product-instance`
|
|
102
102
|
shows one as `[secret]`. `vfac guide` is authoritative.
|
|
103
103
|
|
|
104
|
+
## Settings that go together
|
|
105
|
+
|
|
106
|
+
A Product may declare that a group of settings is all-or-nothing: leave every one
|
|
107
|
+
of them alone, or supply all of them. A provider connection is the usual shape —
|
|
108
|
+
an address without its credential is worse than neither, because the service
|
|
109
|
+
starts and then fails on its first call.
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
spec:
|
|
113
|
+
configuration:
|
|
114
|
+
upstreamUrl: https://upstream.example.com
|
|
115
|
+
upstreamAccount: acct-9
|
|
116
|
+
# and the credential, beside the apply rather than in the file:
|
|
117
|
+
# vfac apply -f resource.yaml --secret upstreamToken=env:UPSTREAM_TOKEN
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Supply the whole group or none of it. A partial answer is refused, and the
|
|
121
|
+
refusal names the setting that is missing and the one that made it necessary.
|
|
122
|
+
|
|
123
|
+
`vfac manifest init` lists each group and seeds nothing for it — half a group is
|
|
124
|
+
exactly the state that gets refused, so a placeholder would only produce a file
|
|
125
|
+
that fails `vfac validate`.
|
|
126
|
+
|
|
127
|
+
**`validate` and `plan` answer different questions here, and the difference is
|
|
128
|
+
not a bug.** A manifest may never carry a secret, so `vfac validate` — which
|
|
129
|
+
reads a file with no Project and no credential — cannot tell whether the
|
|
130
|
+
credential in a group is configured, and does not refuse a document over it.
|
|
131
|
+
`vfac plan` knows what the apply would carry and what the resource already holds,
|
|
132
|
+
so it is where a missing member is reported. A credential the resource already
|
|
133
|
+
holds does not have to be sent again to change one of its siblings.
|
|
134
|
+
|
|
104
135
|
## Connecting to another resource
|
|
105
136
|
|
|
106
137
|
A Product may declare a **service dependency** — a connection to another service
|
package/dist/vfac.mjs
CHANGED
|
@@ -7880,14 +7880,26 @@ function renderOperation(operationId, data) {
|
|
|
7880
7880
|
lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
|
|
7881
7881
|
const summary = failure["summary"];
|
|
7882
7882
|
if (typeof summary === "string") lines.push(` ${summary}`);
|
|
7883
|
-
const detail = failure["detail"];
|
|
7884
|
-
if (typeof detail === "string" && detail !== "") lines.push(` ${detail}`);
|
|
7885
7883
|
const cause = failure["cause"];
|
|
7886
7884
|
if (typeof cause === "string") lines.push(` cause: ${cause}`);
|
|
7885
|
+
const services = failure["services"];
|
|
7886
|
+
if (Array.isArray(services)) {
|
|
7887
|
+
for (const entry of services) {
|
|
7888
|
+
const one = asRecord(entry);
|
|
7889
|
+
if (one === null) continue;
|
|
7890
|
+
const dependency = one["dependency"];
|
|
7891
|
+
if (typeof dependency !== "string" || dependency === "") continue;
|
|
7892
|
+
const code = one["code"];
|
|
7893
|
+
lines.push(
|
|
7894
|
+
typeof code === "string" && code !== "" ? ` ${dependency} \u2014 the service reported: ${code}` : ` ${dependency} \u2014 the service reported nothing usable`
|
|
7895
|
+
);
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7887
7898
|
const retryable = failure["retryable"];
|
|
7888
7899
|
if (typeof retryable === "boolean") {
|
|
7900
|
+
const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
|
|
7889
7901
|
lines.push(
|
|
7890
|
-
retryable ?
|
|
7902
|
+
retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}.`
|
|
7891
7903
|
);
|
|
7892
7904
|
}
|
|
7893
7905
|
}
|
|
@@ -12261,6 +12273,27 @@ var CustomerConfigurationObjectSchema = external_exports.object({
|
|
|
12261
12273
|
type: external_exports.literal("object"),
|
|
12262
12274
|
properties: external_exports.record(PropertyNameSchema, ConfigurationPropertySchema).describe("Customer-editable settings, keyed by camelCase property name."),
|
|
12263
12275
|
required: external_exports.array(PropertyNameSchema).max(40).optional(),
|
|
12276
|
+
/**
|
|
12277
|
+
* Settings that must be configured TOGETHER, keyed by the one that triggers
|
|
12278
|
+
* the requirement.
|
|
12279
|
+
*
|
|
12280
|
+
* `required` says a setting must always have a value. This says a group is
|
|
12281
|
+
* either wholly absent or wholly complete — the shape a provider connection
|
|
12282
|
+
* has, where an endpoint without its credential is worse than neither.
|
|
12283
|
+
*
|
|
12284
|
+
* JSON SCHEMA 2020-12 SEMANTICS, DELIBERATELY AND EXACTLY: if the trigger
|
|
12285
|
+
* key is present in the submitted object, every name in its list must be
|
|
12286
|
+
* present too. The key is published under its standard name in
|
|
12287
|
+
* `schemas/product-definition.schema.json`, inside a node that already looks
|
|
12288
|
+
* like JSON Schema, so a publisher running an off-the-shelf validator over
|
|
12289
|
+
* that file must get the answer the platform gives. The refinement below is
|
|
12290
|
+
* what keeps that promise true — see the `default` rule, which exists for
|
|
12291
|
+
* this reason and no other.
|
|
12292
|
+
*
|
|
12293
|
+
* CYCLES ARE LEGAL. `{a: ["b"], b: ["a"]}` is how "all or none" is written,
|
|
12294
|
+
* and refusing it would remove the primitive's main use.
|
|
12295
|
+
*/
|
|
12296
|
+
dependentRequired: external_exports.record(PropertyNameSchema, external_exports.array(PropertyNameSchema).min(1).max(40)).optional().describe("Settings that must be supplied together, keyed by the triggering property."),
|
|
12264
12297
|
/**
|
|
12265
12298
|
* Literal `false`, not merely defaulted: docs/CONVENTIONS.md §8 requires
|
|
12266
12299
|
* contracts to reject unknown fields, and an unknown key accepted here
|
|
@@ -12455,7 +12488,129 @@ var CustomerConfigurationSchema = CustomerConfigurationObjectSchema.superRefine(
|
|
|
12455
12488
|
message: `Duplicate required entries: ${[...new Set(duplicates)].join(", ")}.`
|
|
12456
12489
|
});
|
|
12457
12490
|
}
|
|
12491
|
+
checkDependentRequired(config, ctx);
|
|
12458
12492
|
});
|
|
12493
|
+
function checkDependentRequired(config, ctx) {
|
|
12494
|
+
const clauses = Object.entries(config.dependentRequired ?? {});
|
|
12495
|
+
if (clauses.length === 0)
|
|
12496
|
+
return;
|
|
12497
|
+
if (clauses.length > MAX_CONFIGURATION_PROPERTIES) {
|
|
12498
|
+
ctx.addIssue({
|
|
12499
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12500
|
+
path: ["dependentRequired"],
|
|
12501
|
+
message: `A Product may declare at most ${MAX_CONFIGURATION_PROPERTIES} dependent-required groups; found ${clauses.length}.`
|
|
12502
|
+
});
|
|
12503
|
+
}
|
|
12504
|
+
for (const [trigger, dependents] of clauses) {
|
|
12505
|
+
const path = ["dependentRequired", trigger];
|
|
12506
|
+
const declaredTrigger = config.properties[trigger];
|
|
12507
|
+
if (declaredTrigger === void 0) {
|
|
12508
|
+
ctx.addIssue({
|
|
12509
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12510
|
+
path,
|
|
12511
|
+
message: `"${trigger}" requires other settings but is not a declared property.`
|
|
12512
|
+
});
|
|
12513
|
+
} else if (declaredTrigger.type !== "string") {
|
|
12514
|
+
ctx.addIssue({
|
|
12515
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12516
|
+
path,
|
|
12517
|
+
message: `"${trigger}" is a ${declaredTrigger.type} setting and cannot require others. Only a string setting can, because presence is the question this rule asks and a boolean or an empty list is an answer the customer gave rather than a setting they left alone.`
|
|
12518
|
+
});
|
|
12519
|
+
} else if (declaredTrigger.default !== void 0) {
|
|
12520
|
+
ctx.addIssue({
|
|
12521
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12522
|
+
path,
|
|
12523
|
+
message: `"${trigger}" declares a default, so it is never absent and would require the settings below of every customer unconditionally. Remove the default, or make the requirement unconditional with \`required\`.`
|
|
12524
|
+
});
|
|
12525
|
+
}
|
|
12526
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12527
|
+
for (const dependent of dependents) {
|
|
12528
|
+
if (dependent === trigger) {
|
|
12529
|
+
ctx.addIssue({
|
|
12530
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12531
|
+
path,
|
|
12532
|
+
message: `"${trigger}" cannot require itself.`
|
|
12533
|
+
});
|
|
12534
|
+
continue;
|
|
12535
|
+
}
|
|
12536
|
+
if (seen.has(dependent)) {
|
|
12537
|
+
ctx.addIssue({
|
|
12538
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12539
|
+
path,
|
|
12540
|
+
message: `"${trigger}" lists "${dependent}" more than once.`
|
|
12541
|
+
});
|
|
12542
|
+
continue;
|
|
12543
|
+
}
|
|
12544
|
+
seen.add(dependent);
|
|
12545
|
+
const declared = config.properties[dependent];
|
|
12546
|
+
if (declared === void 0) {
|
|
12547
|
+
ctx.addIssue({
|
|
12548
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12549
|
+
path,
|
|
12550
|
+
message: `"${trigger}" requires "${dependent}", which is not a declared property.`
|
|
12551
|
+
});
|
|
12552
|
+
continue;
|
|
12553
|
+
}
|
|
12554
|
+
if (declared.default !== void 0) {
|
|
12555
|
+
ctx.addIssue({
|
|
12556
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12557
|
+
path,
|
|
12558
|
+
message: `"${dependent}" declares a default, so it is never absent and this requirement could never fail. Remove the default, or drop it from this group.`
|
|
12559
|
+
});
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
}
|
|
12563
|
+
checkOneSecretPerGroup(config, ctx);
|
|
12564
|
+
}
|
|
12565
|
+
function checkOneSecretPerGroup(config, ctx) {
|
|
12566
|
+
const clauses = Object.entries(config.dependentRequired ?? {});
|
|
12567
|
+
if (clauses.length === 0)
|
|
12568
|
+
return;
|
|
12569
|
+
const neighbours = /* @__PURE__ */ new Map();
|
|
12570
|
+
const link = (a, b) => {
|
|
12571
|
+
if (!neighbours.has(a))
|
|
12572
|
+
neighbours.set(a, /* @__PURE__ */ new Set());
|
|
12573
|
+
neighbours.get(a).add(b);
|
|
12574
|
+
};
|
|
12575
|
+
for (const [trigger, dependents] of clauses) {
|
|
12576
|
+
for (const dependent of dependents) {
|
|
12577
|
+
link(trigger, dependent);
|
|
12578
|
+
link(dependent, trigger);
|
|
12579
|
+
}
|
|
12580
|
+
}
|
|
12581
|
+
const isSecret2 = (name) => {
|
|
12582
|
+
const property = config.properties[name];
|
|
12583
|
+
return property !== void 0 && property.type === "string" && property["x-secret"] === true;
|
|
12584
|
+
};
|
|
12585
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12586
|
+
for (const start of neighbours.keys()) {
|
|
12587
|
+
if (seen.has(start))
|
|
12588
|
+
continue;
|
|
12589
|
+
const component = [];
|
|
12590
|
+
const queue = [start];
|
|
12591
|
+
seen.add(start);
|
|
12592
|
+
while (queue.length > 0) {
|
|
12593
|
+
const name = queue.pop();
|
|
12594
|
+
component.push(name);
|
|
12595
|
+
for (const next of neighbours.get(name) ?? []) {
|
|
12596
|
+
if (seen.has(next))
|
|
12597
|
+
continue;
|
|
12598
|
+
seen.add(next);
|
|
12599
|
+
queue.push(next);
|
|
12600
|
+
}
|
|
12601
|
+
}
|
|
12602
|
+
const secrets = component.filter(isSecret2).sort();
|
|
12603
|
+
if (secrets.length <= 1)
|
|
12604
|
+
continue;
|
|
12605
|
+
ctx.addIssue({
|
|
12606
|
+
code: external_exports.ZodIssueCode.custom,
|
|
12607
|
+
// The component has no single name, so the issue is reported against the
|
|
12608
|
+
// map rather than against whichever member happened to be visited first.
|
|
12609
|
+
path: ["dependentRequired"],
|
|
12610
|
+
message: `These settings are configured together and include more than one secret (${secrets.join(", ")}). Only one secret can be supplied per change, so a group must ask for at most one \u2014 otherwise completing it on a running resource takes several, in an order the customer has to work out.`
|
|
12611
|
+
});
|
|
12612
|
+
}
|
|
12613
|
+
}
|
|
12459
12614
|
function enumerableValues(property) {
|
|
12460
12615
|
const out = [];
|
|
12461
12616
|
if (property.type === "string") {
|
|
@@ -12490,8 +12645,9 @@ function isPlainObject(value) {
|
|
|
12490
12645
|
function isSecretProperty(property) {
|
|
12491
12646
|
return property.type === "string" && property["x-secret"] === true;
|
|
12492
12647
|
}
|
|
12493
|
-
function validateSubmittedConfiguration(schema, submitted) {
|
|
12648
|
+
function validateSubmittedConfiguration(schema, submitted, presence = {}) {
|
|
12494
12649
|
const issues = [];
|
|
12650
|
+
const supplied = /* @__PURE__ */ new Set();
|
|
12495
12651
|
if (submitted !== void 0 && submitted !== null && !isPlainObject(submitted)) {
|
|
12496
12652
|
return {
|
|
12497
12653
|
ok: false,
|
|
@@ -12522,6 +12678,8 @@ function validateSubmittedConfiguration(schema, submitted) {
|
|
|
12522
12678
|
const blank = typeof input[name] === "string" && input[name].trim().length === 0;
|
|
12523
12679
|
const present = Object.hasOwn(input, name) && input[name] !== void 0 && !blank;
|
|
12524
12680
|
const secret = isSecretProperty(property);
|
|
12681
|
+
if (present)
|
|
12682
|
+
supplied.add(name);
|
|
12525
12683
|
if (!present) {
|
|
12526
12684
|
const fallback = secret ? void 0 : defaultOf(property);
|
|
12527
12685
|
if (fallback !== void 0) {
|
|
@@ -12546,10 +12704,48 @@ function validateSubmittedConfiguration(schema, submitted) {
|
|
|
12546
12704
|
}
|
|
12547
12705
|
configuration[name] = checked;
|
|
12548
12706
|
}
|
|
12707
|
+
issues.push(...crossFieldIssues(schema, supplied, presence));
|
|
12549
12708
|
if (issues.length > 0)
|
|
12550
12709
|
return { ok: false, issues, partial: configuration };
|
|
12551
12710
|
return { ok: true, configuration, secrets };
|
|
12552
12711
|
}
|
|
12712
|
+
function crossFieldIssues(schema, supplied, presence) {
|
|
12713
|
+
const clauses = Object.entries(schema.dependentRequired ?? {});
|
|
12714
|
+
if (clauses.length === 0)
|
|
12715
|
+
return [];
|
|
12716
|
+
const alsoPresent = new Set(presence.present ?? []);
|
|
12717
|
+
const unknown = new Set(presence.unknown ?? []);
|
|
12718
|
+
const known = (name) => !unknown.has(name);
|
|
12719
|
+
const held = (name) => supplied.has(name) || alsoPresent.has(name);
|
|
12720
|
+
const issues = [];
|
|
12721
|
+
const reported = /* @__PURE__ */ new Set();
|
|
12722
|
+
for (const [trigger, dependents] of clauses) {
|
|
12723
|
+
if (!known(trigger) || !held(trigger))
|
|
12724
|
+
continue;
|
|
12725
|
+
for (const dependent of dependents) {
|
|
12726
|
+
if (!known(dependent) || held(dependent))
|
|
12727
|
+
continue;
|
|
12728
|
+
if (reported.has(dependent))
|
|
12729
|
+
continue;
|
|
12730
|
+
reported.add(dependent);
|
|
12731
|
+
issues.push({
|
|
12732
|
+
field: dependent,
|
|
12733
|
+
message: `This setting is required when ${labelOf(schema, trigger)} is configured.`
|
|
12734
|
+
});
|
|
12735
|
+
}
|
|
12736
|
+
}
|
|
12737
|
+
return issues;
|
|
12738
|
+
}
|
|
12739
|
+
function labelOf(schema, name) {
|
|
12740
|
+
const property = schema.properties[name];
|
|
12741
|
+
return property === void 0 ? `"${name}"` : `"${property.title}"`;
|
|
12742
|
+
}
|
|
12743
|
+
function withoutCrossFieldRules(schema) {
|
|
12744
|
+
if (schema.dependentRequired === void 0)
|
|
12745
|
+
return schema;
|
|
12746
|
+
const { dependentRequired: _removed, ...rest } = schema;
|
|
12747
|
+
return rest;
|
|
12748
|
+
}
|
|
12553
12749
|
function defaultOf(property) {
|
|
12554
12750
|
switch (property.type) {
|
|
12555
12751
|
case "string":
|
|
@@ -12698,6 +12894,14 @@ function seedConfiguration(configuration) {
|
|
|
12698
12894
|
const values = {};
|
|
12699
12895
|
const open = [];
|
|
12700
12896
|
const requiredSecrets = [];
|
|
12897
|
+
const groups = Object.entries(configuration?.dependentRequired ?? {}).map(([trigger, dependents]) => {
|
|
12898
|
+
const members = [trigger, ...dependents];
|
|
12899
|
+
return {
|
|
12900
|
+
trigger,
|
|
12901
|
+
settings: members.filter((name) => !isSecret(properties[name] ?? {})),
|
|
12902
|
+
secrets: members.filter((name) => isSecret(properties[name] ?? {}))
|
|
12903
|
+
};
|
|
12904
|
+
}).sort((a, b) => a.trigger.localeCompare(b.trigger));
|
|
12701
12905
|
for (const [name, declaration] of Object.entries(properties)) {
|
|
12702
12906
|
if (isSecret(declaration)) {
|
|
12703
12907
|
if (required.has(name))
|
|
@@ -12713,7 +12917,7 @@ function seedConfiguration(configuration) {
|
|
|
12713
12917
|
open.push({ name, type: declaration.type ?? null });
|
|
12714
12918
|
}
|
|
12715
12919
|
}
|
|
12716
|
-
return { values, open, requiredSecrets };
|
|
12920
|
+
return { values, open, requiredSecrets, groups };
|
|
12717
12921
|
}
|
|
12718
12922
|
function productInstanceExample(input) {
|
|
12719
12923
|
return {
|
|
@@ -13199,6 +13403,16 @@ var ProductOutputSchema = external_exports.object({
|
|
|
13199
13403
|
*/
|
|
13200
13404
|
secret: external_exports.boolean().optional()
|
|
13201
13405
|
}).strict();
|
|
13406
|
+
var ProductConditionSchema = external_exports.object({
|
|
13407
|
+
key: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, "Condition keys are camelCase.").describe("Stable key. A running instance reports its state under this key."),
|
|
13408
|
+
displayName: external_exports.string().min(2).max(60),
|
|
13409
|
+
/**
|
|
13410
|
+
* What the customer must understand, and — when it is ACTION_REQUIRED —
|
|
13411
|
+
* enough to act on. The platform never writes this sentence: it does not
|
|
13412
|
+
* know what the condition is about.
|
|
13413
|
+
*/
|
|
13414
|
+
description: external_exports.string().min(10).max(300)
|
|
13415
|
+
}).strict();
|
|
13202
13416
|
var ProductDependencySchema = external_exports.object({
|
|
13203
13417
|
/**
|
|
13204
13418
|
* OPTIONAL ONLY WHEN `interface` IS PRESENT, enforced below.
|
|
@@ -13427,6 +13641,16 @@ var ProductDefinitionObjectSchema = external_exports.object({
|
|
|
13427
13641
|
// --- Results, relationships, operations -------------------------------
|
|
13428
13642
|
outputs: external_exports.array(ProductOutputSchema).min(1).max(MAX_PRODUCT_OUTPUTS),
|
|
13429
13643
|
dependencies: external_exports.array(ProductDependencySchema).max(8),
|
|
13644
|
+
/**
|
|
13645
|
+
* Operational conditions a running instance of this Product may report.
|
|
13646
|
+
*
|
|
13647
|
+
* `.optional()`, NEVER `.default([])`. A default materialises the key,
|
|
13648
|
+
* `contractDigest` digests the parsed object, and a republish of unchanged
|
|
13649
|
+
* bytes would then be refused as `RELEASED_CONTRACT_MUTATED`. Absent means
|
|
13650
|
+
* this Product reports none, which is what every Definition published
|
|
13651
|
+
* before this field already means.
|
|
13652
|
+
*/
|
|
13653
|
+
conditions: external_exports.array(ProductConditionSchema).min(1).max(8).optional(),
|
|
13430
13654
|
/**
|
|
13431
13655
|
* The service interfaces this Product provides to other Products.
|
|
13432
13656
|
*
|
|
@@ -13493,6 +13717,15 @@ var ProductDefinitionSchema = ProductDefinitionObjectSchema.superRefine((definit
|
|
|
13493
13717
|
});
|
|
13494
13718
|
}
|
|
13495
13719
|
});
|
|
13720
|
+
const conditionKeys = (definition.conditions ?? []).map((condition) => condition.key);
|
|
13721
|
+
const duplicateConditions = conditionKeys.filter((key, i) => conditionKeys.indexOf(key) !== i);
|
|
13722
|
+
if (duplicateConditions.length > 0) {
|
|
13723
|
+
ctx.addIssue({
|
|
13724
|
+
code: external_exports.ZodIssueCode.custom,
|
|
13725
|
+
path: ["conditions"],
|
|
13726
|
+
message: `Duplicate condition keys: ${[...new Set(duplicateConditions)].join(", ")}.`
|
|
13727
|
+
});
|
|
13728
|
+
}
|
|
13496
13729
|
const dependencyIds = definition.dependencies.map((dependency) => dependency.productId).filter((id) => id !== void 0);
|
|
13497
13730
|
const duplicateDependencies = dependencyIds.filter((id, i) => dependencyIds.indexOf(id) !== i);
|
|
13498
13731
|
if (duplicateDependencies.length > 0) {
|
|
@@ -13584,11 +13817,11 @@ var ProductDefinitionSchema = ProductDefinitionObjectSchema.superRefine((definit
|
|
|
13584
13817
|
});
|
|
13585
13818
|
return;
|
|
13586
13819
|
}
|
|
13587
|
-
const single = {
|
|
13820
|
+
const single = withoutCrossFieldRules({
|
|
13588
13821
|
...definition.configuration,
|
|
13589
13822
|
properties: { [condition.property]: property },
|
|
13590
13823
|
required: [condition.property]
|
|
13591
|
-
};
|
|
13824
|
+
});
|
|
13592
13825
|
const outside = condition.anyOf.filter((value) => !validateSubmittedConfiguration(single, { [condition.property]: value }).ok);
|
|
13593
13826
|
if (outside.length > 0) {
|
|
13594
13827
|
ctx.addIssue({
|
|
@@ -14676,6 +14909,33 @@ var DeploymentBlueprintObjectSchema = external_exports.object({
|
|
|
14676
14909
|
serviceExports: external_exports.array(ServiceExportSchema).max(MAX_SERVICE_EXPORTS).optional(),
|
|
14677
14910
|
/** Service interfaces this Product consumes. Absent means none. */
|
|
14678
14911
|
serviceBindings: external_exports.array(ServiceBindingSchema).max(MAX_SERVICE_BINDINGS).optional(),
|
|
14912
|
+
/**
|
|
14913
|
+
* Which of your own components may report an operational condition.
|
|
14914
|
+
*
|
|
14915
|
+
* NOT DECORATION, and the same policy `dynamicResources[].consumers` is. The
|
|
14916
|
+
* Control Plane mints the condition-reporting credential only for a named
|
|
14917
|
+
* component, so a Blueprint that names none has declared conditions in its
|
|
14918
|
+
* Definition that nothing can reach. Delivery is the enforcement.
|
|
14919
|
+
*
|
|
14920
|
+
* `.optional()`, NEVER `.default([])` — `contractDigest` digests the parsed
|
|
14921
|
+
* object, and a default would materialise the key on every Blueprint
|
|
14922
|
+
* published before this field existed.
|
|
14923
|
+
*
|
|
14924
|
+
* EXACTLY ONE, and that is a property of what a condition IS rather than a
|
|
14925
|
+
* limit somebody will want raised. A condition is keyed per INSTANCE, not
|
|
14926
|
+
* per component: two components reporting `sso` write the same row and
|
|
14927
|
+
* decide it between them by whichever clock is ahead, which is a race with
|
|
14928
|
+
* a customer-visible answer. And "is this Product usable" is a statement
|
|
14929
|
+
* about the instance — CLAUDE.md's "One Product Instance is ONE runtime
|
|
14930
|
+
* unit" — so a second voice is a second opinion, not more coverage.
|
|
14931
|
+
*
|
|
14932
|
+
* It also keeps the platform's withdrawal honest. The watermark that stops
|
|
14933
|
+
* a superseded runtime's answer being served is per instance, because what
|
|
14934
|
+
* replaced the container is an instance-level event; with two reporters,
|
|
14935
|
+
* redeploying one would withdraw the other's still-correct answer and it
|
|
14936
|
+
* would never speak again.
|
|
14937
|
+
*/
|
|
14938
|
+
conditionReporters: external_exports.array(ComponentIdSchema).min(1).max(1).optional(),
|
|
14679
14939
|
secrets: external_exports.array(SecretWiringSchema).max(20),
|
|
14680
14940
|
dataStores: external_exports.array(DataStoreSchema).max(8),
|
|
14681
14941
|
egress: external_exports.array(EgressRuleSchema).max(24),
|
|
@@ -16420,6 +16680,27 @@ async function runGet(parsed) {
|
|
|
16420
16680
|
const declaredNames = declared.map((entry) => entry["key"]).filter((key2) => typeof key2 === "string");
|
|
16421
16681
|
const names = [.../* @__PURE__ */ new Set([...Object.keys(outputs), ...declaredNames])].sort();
|
|
16422
16682
|
const isSecret2 = (name) => declared.find((entry) => entry["key"] === name)?.["secret"] === true;
|
|
16683
|
+
const conditions = Array.isArray(resource["conditions"]) ? resource["conditions"] : [];
|
|
16684
|
+
if (conditions.length > 0) {
|
|
16685
|
+
lines.push("", " Operational");
|
|
16686
|
+
const width = Math.max(
|
|
16687
|
+
...conditions.map((one) => String(one["displayName"] ?? one["key"] ?? "").length)
|
|
16688
|
+
);
|
|
16689
|
+
for (const one of conditions) {
|
|
16690
|
+
const label = String(one["displayName"] ?? one["key"] ?? "");
|
|
16691
|
+
const state = String(one["state"] ?? "UNKNOWN");
|
|
16692
|
+
const reason = one["reason"];
|
|
16693
|
+
lines.push(
|
|
16694
|
+
` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}`
|
|
16695
|
+
);
|
|
16696
|
+
const description = one["description"];
|
|
16697
|
+
if (typeof description === "string") {
|
|
16698
|
+
for (const line of wrapOutput(description, 68)) {
|
|
16699
|
+
lines.push(` ${" ".repeat(width)} ${line}`);
|
|
16700
|
+
}
|
|
16701
|
+
}
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16423
16704
|
if (names.length > 0) {
|
|
16424
16705
|
lines.push("", " Outputs");
|
|
16425
16706
|
const width = Math.max(...names.map((name) => name.length));
|
|
@@ -16893,6 +17174,13 @@ async function runManifestInit(parsed) {
|
|
|
16893
17174
|
`spec.configuration.${setting.name} \u2014 required${setting.type ? ` (${setting.type})` : ""}`
|
|
16894
17175
|
);
|
|
16895
17176
|
}
|
|
17177
|
+
for (const group of seed.groups) {
|
|
17178
|
+
const settings = group.settings.map((name2) => `spec.configuration.${name2}`);
|
|
17179
|
+
const secrets = group.secrets.map((name2) => `--secret ${name2}=env:VAR`);
|
|
17180
|
+
notes.push(
|
|
17181
|
+
`${[...settings, ...secrets].join(" + ")} \u2014 all of these together, or none of them`
|
|
17182
|
+
);
|
|
17183
|
+
}
|
|
16896
17184
|
const document = productInstanceExample({
|
|
16897
17185
|
name,
|
|
16898
17186
|
...key ? { key } : {},
|