@varde-flyt/vfac 0.9.0 → 0.11.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 +50 -10
- package/dist/vfac.mjs +257 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,9 +27,11 @@ vfac whoami
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`doctor` checks the endpoint, which Project this terminal has selected, the API,
|
|
30
|
-
whether this client is new enough for the platform it is pointed at,
|
|
31
|
-
|
|
32
|
-
thing that is actually wrong.
|
|
30
|
+
whether this client is new enough for the platform it is pointed at, how this
|
|
31
|
+
terminal is signed in and what that credential reaches, in that order, and reports
|
|
32
|
+
the first thing that is actually wrong. Both ways of signing in count: a Context
|
|
33
|
+
Gate exchanged from `VFAC_CREDENTIAL_ID` and `VFAC_CREDENTIAL_SECRET`, or a
|
|
34
|
+
sign-in from `vfac login` that is still valid. A platform declares the oldest client it supports;
|
|
33
35
|
below that, `doctor` fails and says so before sending your credential anywhere.
|
|
34
36
|
|
|
35
37
|
The selected Project and the Projects your credential reaches are two different
|
|
@@ -89,13 +91,14 @@ identifier in a manifest that is yours: a resource key is 3–63 characters of
|
|
|
89
91
|
lowercase letters, digits and hyphens, starting with a letter and ending with a
|
|
90
92
|
letter or digit.
|
|
91
93
|
|
|
92
|
-
**A
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
**A live resource holds its key, and a finished delete releases it.** The key
|
|
95
|
+
stays claimed while the teardown runs, so nothing can take the name before the
|
|
96
|
+
original is gone. Once the delete completes the name is free and the same
|
|
97
|
+
manifest applies again — as a `CREATE`, building a NEW resource with nothing in
|
|
98
|
+
it. Reuse is not a restore, which is worth knowing before the delete rather than
|
|
99
|
+
after it: the apply succeeds, so nothing will stop you. `vfac lifecycle delete`
|
|
100
|
+
says the same thing at the prompt. If what you want is to stop the service and
|
|
101
|
+
keep its data, `vfac lifecycle suspend` is the reversible one.
|
|
99
102
|
|
|
100
103
|
## Reading a setting from another resource
|
|
101
104
|
|
|
@@ -191,6 +194,43 @@ credential the connection delivers never appears in your manifest, in a plan, or
|
|
|
191
194
|
in any `vfac` output — the providing service issues it and the platform stores
|
|
192
195
|
it.
|
|
193
196
|
|
|
197
|
+
## For an agent or a pipeline
|
|
198
|
+
|
|
199
|
+
Every command takes `--output json`, and it is the same data rather than a
|
|
200
|
+
prettier subset — the human format of `vfac product show` prints a readable
|
|
201
|
+
selection on purpose, and the JSON carries the whole Product contract.
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
vfac product show <productId> --output json
|
|
205
|
+
vfac plan -f resource.yaml --output json
|
|
206
|
+
vfac get product-instance <productInstanceId> --output json
|
|
207
|
+
vfac get product-instances --output json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Decide from the action, not from the exit code.** A plan that REFUSES still
|
|
211
|
+
exits 0: a plan reporting `UPGRADE_REQUIRED` did what it was asked, and the
|
|
212
|
+
answer is "not like this". A job that reads only the exit code treats that as
|
|
213
|
+
success and applies nothing.
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
ACTION="$(vfac plan -f resource.yaml --output json | jq -r .action)"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**And a non-zero exit is a real failure.** The code is not decoration: a
|
|
220
|
+
`NO_CHANGE` against a resource that is not actually serving exits 1, with
|
|
221
|
+
`--output json` as without it, because "nothing needed applying" and "it works"
|
|
222
|
+
are different answers. Read the action for what the plan MEANS and the exit code
|
|
223
|
+
for whether anything is WRONG — neither substitutes for the other.
|
|
224
|
+
|
|
225
|
+
The envelopes are stable at the top level:
|
|
226
|
+
|
|
227
|
+
| Command | Answers with |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| `vfac get product-instance <id> --output json` | `resource` and `lifecycle` — `status`, `outputs` and `conditions` are under `resource` |
|
|
230
|
+
| `vfac get product-instances --output json` | `productInstances` and `nextCursor` |
|
|
231
|
+
|
|
232
|
+
`vfac guide` is authoritative for what is inside them.
|
|
233
|
+
|
|
194
234
|
## Running it again
|
|
195
235
|
|
|
196
236
|
**Send no secret.** A pipeline re-applies the same manifest on every push:
|
package/dist/vfac.mjs
CHANGED
|
@@ -7793,7 +7793,7 @@ async function ensureSession(args) {
|
|
|
7793
7793
|
const endpoint = checked.endpoint;
|
|
7794
7794
|
const credential = credentialFromEnvironment();
|
|
7795
7795
|
if (args.forceExchange !== true && sessionUsable(stored.session, endpoint, args.now ?? /* @__PURE__ */ new Date(), credential?.keyId)) {
|
|
7796
|
-
return { ok: true, ready: { endpoint, token: stored.session.token, stored } };
|
|
7796
|
+
return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
|
|
7797
7797
|
}
|
|
7798
7798
|
const signedIn = await signIn({
|
|
7799
7799
|
endpoint,
|
|
@@ -7802,7 +7802,16 @@ async function ensureSession(args) {
|
|
|
7802
7802
|
if (!signedIn.ok) return { ok: false, message: signedIn.message };
|
|
7803
7803
|
const next = { ...stored, session: signedIn.session };
|
|
7804
7804
|
writeStored(next);
|
|
7805
|
-
return {
|
|
7805
|
+
return {
|
|
7806
|
+
ok: true,
|
|
7807
|
+
ready: {
|
|
7808
|
+
endpoint,
|
|
7809
|
+
token: signedIn.session.token,
|
|
7810
|
+
stored: next,
|
|
7811
|
+
via: "exchanged",
|
|
7812
|
+
...signedIn.gate ? { gate: signedIn.gate } : {}
|
|
7813
|
+
}
|
|
7814
|
+
};
|
|
7806
7815
|
}
|
|
7807
7816
|
function resolveProject(flag, stored) {
|
|
7808
7817
|
return flag ?? stored.context?.projectId ?? null;
|
|
@@ -7922,8 +7931,9 @@ function renderOperation(operationId, data) {
|
|
|
7922
7931
|
const retryable = failure["retryable"];
|
|
7923
7932
|
if (typeof retryable === "boolean") {
|
|
7924
7933
|
const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
|
|
7934
|
+
const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
|
|
7925
7935
|
lines.push(
|
|
7926
|
-
retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}.`
|
|
7936
|
+
retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
|
|
7927
7937
|
);
|
|
7928
7938
|
}
|
|
7929
7939
|
}
|
|
@@ -12224,6 +12234,16 @@ var ConfigurationGroupSchema = external_exports.enum([
|
|
|
12224
12234
|
]);
|
|
12225
12235
|
var CONFIGURATION_PROPERTY_NAME_REGEX = /^[a-z][a-zA-Z0-9]{1,47}$/;
|
|
12226
12236
|
var PropertyNameSchema = external_exports.string().regex(CONFIGURATION_PROPERTY_NAME_REGEX, "Configuration property names must be camelCase, start with a lowercase letter, and be 2\u201348 characters.");
|
|
12237
|
+
var ValueConditionSchema = external_exports.object({
|
|
12238
|
+
/**
|
|
12239
|
+
* The setting, as a canonical path — or a bare root key, which is the same
|
|
12240
|
+
* string at depth 1. With `anyRow`, a DECLARATION path: `/servers/kind`
|
|
12241
|
+
* names the setting each row answers, never one row's answer.
|
|
12242
|
+
*/
|
|
12243
|
+
path: external_exports.string().min(1).max(400),
|
|
12244
|
+
anyOf: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(20),
|
|
12245
|
+
anyRow: external_exports.boolean().optional()
|
|
12246
|
+
}).strict();
|
|
12227
12247
|
var TitleSchema = external_exports.string().min(2).max(80).describe("Customer-facing field label.");
|
|
12228
12248
|
var V1_LEAF_BOUNDS = {
|
|
12229
12249
|
help: 400,
|
|
@@ -12280,7 +12300,24 @@ function leafShapes(bounds) {
|
|
|
12280
12300
|
* without the other a validation error.
|
|
12281
12301
|
*/
|
|
12282
12302
|
writeOnly: external_exports.boolean().optional(),
|
|
12283
|
-
"x-secret": external_exports.boolean().optional()
|
|
12303
|
+
"x-secret": external_exports.boolean().optional(),
|
|
12304
|
+
/**
|
|
12305
|
+
* The setting VALUE that makes this credential necessary.
|
|
12306
|
+
*
|
|
12307
|
+
* WHY IT CANNOT BE `dependentRequired`. That mechanism is keyed on
|
|
12308
|
+
* PRESENCE — "if the author answered X, they must also answer Y" — and
|
|
12309
|
+
* says so in as many words: "PRESENCE, NEVER A VALUE. A caller passes
|
|
12310
|
+
* NAMES." A Product whose credential is needed only when a setting has one
|
|
12311
|
+
* particular value could not say so, so `vfac plan`'s `requiredSecrets`
|
|
12312
|
+
* could not warn before a deploy, and the refusal arrived at apply.
|
|
12313
|
+
*
|
|
12314
|
+
* ON A SECRET ONLY, and that is narrower than the mechanism could be. A
|
|
12315
|
+
* value-conditioned requirement for an ORDINARY setting is a real gap too,
|
|
12316
|
+
* but it changes what every door accepts for every property; this changes
|
|
12317
|
+
* what `requiredSecrets` reports, which is one answer with one reader.
|
|
12318
|
+
* Opening it wider is a separate decision with its own blast radius.
|
|
12319
|
+
*/
|
|
12320
|
+
"x-required-when": ValueConditionSchema.optional()
|
|
12284
12321
|
}).strict();
|
|
12285
12322
|
withJsonSchemaAnnex(string, SECRET_COUPLING_ANNEX);
|
|
12286
12323
|
const number = external_exports.object({
|
|
@@ -12332,7 +12369,7 @@ var ConfigurationPropertySchema = external_exports.discriminatedUnion("type", [
|
|
|
12332
12369
|
BooleanPropertySchema,
|
|
12333
12370
|
ArrayPropertySchema
|
|
12334
12371
|
]);
|
|
12335
|
-
var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true }).extend({ group: ConfigurationGroupSchema.optional() });
|
|
12372
|
+
var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true, "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() });
|
|
12336
12373
|
var V2_NESTED_LEAVES = {
|
|
12337
12374
|
string: NestedStringPropertySchema,
|
|
12338
12375
|
number: V2_LEAVES.number.extend({ group: ConfigurationGroupSchema.optional() }),
|
|
@@ -13038,6 +13075,9 @@ function pathSegments(path) {
|
|
|
13038
13075
|
const raw = path.startsWith("/") ? path.slice(1).split("/") : [path];
|
|
13039
13076
|
return raw.map((segment) => isIndexSegment(segment) ? Number(segment) : segment);
|
|
13040
13077
|
}
|
|
13078
|
+
function rootSegment(path) {
|
|
13079
|
+
return String(pathSegments(path)[0]);
|
|
13080
|
+
}
|
|
13041
13081
|
function declarationPathOf(path) {
|
|
13042
13082
|
const segments = pathSegments(path).filter((segment) => typeof segment !== "number");
|
|
13043
13083
|
return configurationPath(...segments) ?? path;
|
|
@@ -13151,11 +13191,71 @@ function declaresPath(schema, path) {
|
|
|
13151
13191
|
return true;
|
|
13152
13192
|
return configurationContainers(schema).some((container) => container.path === declarationPathOf(path));
|
|
13153
13193
|
}
|
|
13194
|
+
function isPlainObject(value) {
|
|
13195
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13196
|
+
}
|
|
13197
|
+
function valueAt(configuration, path) {
|
|
13198
|
+
let cursor = configuration;
|
|
13199
|
+
for (const segment of segmentsOf(path)) {
|
|
13200
|
+
if (typeof segment === "number") {
|
|
13201
|
+
if (!Array.isArray(cursor))
|
|
13202
|
+
return void 0;
|
|
13203
|
+
cursor = cursor[segment];
|
|
13204
|
+
continue;
|
|
13205
|
+
}
|
|
13206
|
+
if (!isPlainObject(cursor))
|
|
13207
|
+
return void 0;
|
|
13208
|
+
cursor = cursor[segment];
|
|
13209
|
+
}
|
|
13210
|
+
return cursor;
|
|
13211
|
+
}
|
|
13212
|
+
function valuesAt(configuration, path) {
|
|
13213
|
+
let cursors = [configuration];
|
|
13214
|
+
for (const segment of segmentsOf(path)) {
|
|
13215
|
+
const next = [];
|
|
13216
|
+
for (const cursor of cursors) {
|
|
13217
|
+
if (typeof segment === "number") {
|
|
13218
|
+
if (Array.isArray(cursor))
|
|
13219
|
+
next.push(cursor[segment]);
|
|
13220
|
+
continue;
|
|
13221
|
+
}
|
|
13222
|
+
if (!isPlainObject(cursor))
|
|
13223
|
+
continue;
|
|
13224
|
+
const value = cursor[segment];
|
|
13225
|
+
if (Array.isArray(value))
|
|
13226
|
+
next.push(...value);
|
|
13227
|
+
else
|
|
13228
|
+
next.push(value);
|
|
13229
|
+
}
|
|
13230
|
+
cursors = next;
|
|
13231
|
+
}
|
|
13232
|
+
return cursors.filter((value) => value !== void 0);
|
|
13233
|
+
}
|
|
13234
|
+
function segmentsOf(path) {
|
|
13235
|
+
const raw = path.startsWith("/") ? path.slice(1).split("/") : [path];
|
|
13236
|
+
return raw.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
|
|
13237
|
+
}
|
|
13238
|
+
|
|
13239
|
+
// ../product-configuration/dist/value-condition.js
|
|
13240
|
+
function conditionSetting(condition) {
|
|
13241
|
+
return "path" in condition ? condition.path : condition.property;
|
|
13242
|
+
}
|
|
13243
|
+
function conditionRoot(condition) {
|
|
13244
|
+
const path = parseConfigurationPath(conditionSetting(condition));
|
|
13245
|
+
return path === null ? conditionSetting(condition) : rootSegment(path);
|
|
13246
|
+
}
|
|
13247
|
+
function conditionHolds(condition, configuration) {
|
|
13248
|
+
const path = parseConfigurationPath(conditionSetting(condition));
|
|
13249
|
+
if (path === null)
|
|
13250
|
+
return false;
|
|
13251
|
+
const matches2 = (held) => typeof held === "string" && condition.anyOf.includes(held);
|
|
13252
|
+
return "anyRow" in condition && condition.anyRow === true ? valuesAt(configuration, path).some(matches2) : matches2(valueAt(configuration, path));
|
|
13253
|
+
}
|
|
13154
13254
|
|
|
13155
13255
|
// ../product-configuration/dist/customer-configuration-input.js
|
|
13156
13256
|
var ABSOLUTE_MAX_STRING = MAX_DECLARABLE_STRING_LENGTH;
|
|
13157
13257
|
var MAX_REPORTED_UNDECLARED_KEYS = 10;
|
|
13158
|
-
function
|
|
13258
|
+
function isPlainObject2(value) {
|
|
13159
13259
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13160
13260
|
}
|
|
13161
13261
|
function report(walk, field, message) {
|
|
@@ -13177,17 +13277,17 @@ function validateSubmittedConfiguration(schema, submitted, presence = {}) {
|
|
|
13177
13277
|
referenced: new Set(presence.referenced ?? [])
|
|
13178
13278
|
};
|
|
13179
13279
|
const supplied = /* @__PURE__ */ new Set();
|
|
13180
|
-
if (submitted !== void 0 && submitted !== null && !
|
|
13280
|
+
if (submitted !== void 0 && submitted !== null && !isPlainObject2(submitted)) {
|
|
13181
13281
|
return {
|
|
13182
13282
|
ok: false,
|
|
13183
13283
|
issues: [{ field: "", message: "Configuration must be an object." }],
|
|
13184
13284
|
partial: {}
|
|
13185
13285
|
};
|
|
13186
13286
|
}
|
|
13187
|
-
const input =
|
|
13287
|
+
const input = isPlainObject2(submitted) ? submitted : {};
|
|
13188
13288
|
const secrets = {};
|
|
13189
13289
|
const configuration = validateObject({ properties: schema.properties, required: schema.required }, input, null, walk, { secrets, supplied });
|
|
13190
|
-
drain(walk, crossFieldIssues(schema, supplied, presence));
|
|
13290
|
+
drain(walk, crossFieldIssues(schema, supplied, presence, configuration));
|
|
13191
13291
|
if (walk.suppressed > 0) {
|
|
13192
13292
|
walk.issues.push({
|
|
13193
13293
|
field: "",
|
|
@@ -13342,7 +13442,7 @@ function validateContainer(name, property, children, value, field, walk) {
|
|
|
13342
13442
|
if (children.kind === "object") {
|
|
13343
13443
|
if (value === void 0)
|
|
13344
13444
|
return void 0;
|
|
13345
|
-
if (!
|
|
13445
|
+
if (!isPlainObject2(value)) {
|
|
13346
13446
|
report(walk, field, "Expected a group of settings.");
|
|
13347
13447
|
return void 0;
|
|
13348
13448
|
}
|
|
@@ -13369,7 +13469,7 @@ function validateContainer(name, property, children, value, field, walk) {
|
|
|
13369
13469
|
report(walk, field, "This configuration has too many values.");
|
|
13370
13470
|
break;
|
|
13371
13471
|
}
|
|
13372
|
-
if (!
|
|
13472
|
+
if (!isPlainObject2(element)) {
|
|
13373
13473
|
report(walk, `${field}`, `Item ${index + 1} is not a group of settings.`);
|
|
13374
13474
|
continue;
|
|
13375
13475
|
}
|
|
@@ -13385,9 +13485,10 @@ function validateContainer(name, property, children, value, field, walk) {
|
|
|
13385
13485
|
}
|
|
13386
13486
|
return out;
|
|
13387
13487
|
}
|
|
13388
|
-
function crossFieldIssues(schema, supplied, presence) {
|
|
13488
|
+
function crossFieldIssues(schema, supplied, presence, configuration = {}) {
|
|
13389
13489
|
const clauses = Object.entries(schema.dependentRequired ?? {});
|
|
13390
|
-
|
|
13490
|
+
const conditional = Object.entries(schema.properties).filter(([, property]) => conditionOf(property) !== void 0);
|
|
13491
|
+
if (clauses.length === 0 && conditional.length === 0)
|
|
13391
13492
|
return [];
|
|
13392
13493
|
const alsoPresent = /* @__PURE__ */ new Set([...presence.present ?? [], ...presence.referenced ?? []]);
|
|
13393
13494
|
const unknown = new Set(presence.unknown ?? []);
|
|
@@ -13410,13 +13511,42 @@ function crossFieldIssues(schema, supplied, presence) {
|
|
|
13410
13511
|
});
|
|
13411
13512
|
}
|
|
13412
13513
|
}
|
|
13514
|
+
for (const [name, property] of conditional) {
|
|
13515
|
+
const condition = conditionOf(property);
|
|
13516
|
+
if (condition === void 0 || !known(name) || held(name))
|
|
13517
|
+
continue;
|
|
13518
|
+
if (!conditionHolds(condition, configuration))
|
|
13519
|
+
continue;
|
|
13520
|
+
if (reported.has(name))
|
|
13521
|
+
continue;
|
|
13522
|
+
reported.add(name);
|
|
13523
|
+
issues.push({
|
|
13524
|
+
field: name,
|
|
13525
|
+
// THE SETTING THAT MADE IT NECESSARY, never the value it holds. A message
|
|
13526
|
+
// naming the value would be fine here and wrong one refactor later, when
|
|
13527
|
+
// somebody conditions a secret on another secret — which the publish rules
|
|
13528
|
+
// refuse precisely because nothing may read one.
|
|
13529
|
+
message: `This setting is required because of what ${labelOf(schema, conditionRoot(condition))} is set to.`
|
|
13530
|
+
});
|
|
13531
|
+
}
|
|
13413
13532
|
return issues;
|
|
13414
13533
|
}
|
|
13534
|
+
function conditionOf(property) {
|
|
13535
|
+
return property?.["x-required-when"];
|
|
13536
|
+
}
|
|
13415
13537
|
function labelOf(schema, name) {
|
|
13416
13538
|
const property = schema.properties[name];
|
|
13417
13539
|
return property?.title === void 0 ? `"${name}"` : `"${property.title}"`;
|
|
13418
13540
|
}
|
|
13419
13541
|
function withoutCrossFieldRules(schema) {
|
|
13542
|
+
const conditioned = Object.entries(schema.properties).some(([, property]) => property["x-required-when"] !== void 0);
|
|
13543
|
+
if (conditioned) {
|
|
13544
|
+
const properties = Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => {
|
|
13545
|
+
const { "x-required-when": _dropped, ...bare } = property;
|
|
13546
|
+
return [name, bare];
|
|
13547
|
+
}));
|
|
13548
|
+
return withoutCrossFieldRules({ ...schema, properties });
|
|
13549
|
+
}
|
|
13420
13550
|
if (schema.dependentRequired === void 0)
|
|
13421
13551
|
return schema;
|
|
13422
13552
|
const { dependentRequired: _removed, ...rest } = schema;
|
|
@@ -14619,6 +14749,91 @@ function checkProductDefinition(definition, ctx) {
|
|
|
14619
14749
|
});
|
|
14620
14750
|
}
|
|
14621
14751
|
});
|
|
14752
|
+
for (const [name, property] of Object.entries(definition.configuration.properties ?? {})) {
|
|
14753
|
+
const condition = property["x-required-when"];
|
|
14754
|
+
if (condition === void 0)
|
|
14755
|
+
continue;
|
|
14756
|
+
const at = ["configuration", "properties", name, "x-required-when"];
|
|
14757
|
+
if (!isSecretProperty(property)) {
|
|
14758
|
+
ctx.addIssue({
|
|
14759
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14760
|
+
path: [...at],
|
|
14761
|
+
message: `"${name}" is not a secret, so "x-required-when" has nothing to report through. Declare it "x-secret": true with "writeOnly": true, or use "dependentRequired" to require one ordinary setting alongside another.`
|
|
14762
|
+
});
|
|
14763
|
+
continue;
|
|
14764
|
+
}
|
|
14765
|
+
if (new Set(condition.anyOf).size !== condition.anyOf.length) {
|
|
14766
|
+
ctx.addIssue({
|
|
14767
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14768
|
+
path: [...at, "anyOf"],
|
|
14769
|
+
message: "Repeated values in `anyOf`."
|
|
14770
|
+
});
|
|
14771
|
+
}
|
|
14772
|
+
if (condition.path === name) {
|
|
14773
|
+
ctx.addIssue({
|
|
14774
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14775
|
+
path: [...at, "path"],
|
|
14776
|
+
message: `"${name}" cannot be conditioned on its own value.`
|
|
14777
|
+
});
|
|
14778
|
+
continue;
|
|
14779
|
+
}
|
|
14780
|
+
const parsed = parseConfigurationPath(condition.path);
|
|
14781
|
+
const leaf = parsed === null ? null : leafAt(definition.configuration, parsed);
|
|
14782
|
+
if (leaf === null) {
|
|
14783
|
+
const isGroup = parsed !== null && declaresPath(definition.configuration, parsed);
|
|
14784
|
+
ctx.addIssue({
|
|
14785
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14786
|
+
path: [...at, "path"],
|
|
14787
|
+
message: isGroup ? `"${condition.path}" is a group of settings, which holds no single value to compare. Name one setting inside it.` : `"${condition.path}" is not a declared customer configuration property.`
|
|
14788
|
+
});
|
|
14789
|
+
continue;
|
|
14790
|
+
}
|
|
14791
|
+
if (underList(leaf) && condition.anyRow !== true) {
|
|
14792
|
+
ctx.addIssue({
|
|
14793
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14794
|
+
path: [...at, "path"],
|
|
14795
|
+
message: `"${condition.path}" is inside a list, so it names one value per item rather than one value. Add "anyRow": true to require the secret when ANY row chose one of these values, or name a setting the instance holds exactly once.`
|
|
14796
|
+
});
|
|
14797
|
+
continue;
|
|
14798
|
+
}
|
|
14799
|
+
if (!underList(leaf) && condition.anyRow === true) {
|
|
14800
|
+
ctx.addIssue({
|
|
14801
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14802
|
+
path: [...at, "anyRow"],
|
|
14803
|
+
message: `"${condition.path}" is not inside a list, so there are no rows to quantify over.`
|
|
14804
|
+
});
|
|
14805
|
+
}
|
|
14806
|
+
const target = leaf.property;
|
|
14807
|
+
if (target.type !== "string") {
|
|
14808
|
+
ctx.addIssue({
|
|
14809
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14810
|
+
path: [...at, "path"],
|
|
14811
|
+
message: `"${condition.path}" is a ${target.type} setting; a condition compares string values.`
|
|
14812
|
+
});
|
|
14813
|
+
continue;
|
|
14814
|
+
}
|
|
14815
|
+
if (isSecretProperty(target)) {
|
|
14816
|
+
ctx.addIssue({
|
|
14817
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14818
|
+
path: [...at, "path"],
|
|
14819
|
+
message: `"${condition.path}" is a secret. The platform never reads a secret's value, so a condition on one could never be evaluated.`
|
|
14820
|
+
});
|
|
14821
|
+
continue;
|
|
14822
|
+
}
|
|
14823
|
+
const single = withoutCrossFieldRules({
|
|
14824
|
+
...definition.configuration,
|
|
14825
|
+
properties: { [leaf.name]: target },
|
|
14826
|
+
required: [leaf.name]
|
|
14827
|
+
});
|
|
14828
|
+
const outside = condition.anyOf.filter((value) => !validateSubmittedConfiguration(single, { [leaf.name]: value }).ok);
|
|
14829
|
+
if (outside.length > 0) {
|
|
14830
|
+
ctx.addIssue({
|
|
14831
|
+
code: external_exports.ZodIssueCode.custom,
|
|
14832
|
+
path: [...at, "anyOf"],
|
|
14833
|
+
message: `"${condition.path}" never accepts ${outside.map((v) => `"${v}"`).join(", ")}, so this condition could never make the secret required.`
|
|
14834
|
+
});
|
|
14835
|
+
}
|
|
14836
|
+
}
|
|
14622
14837
|
const provides = definition.serviceInterfaces?.provides ?? [];
|
|
14623
14838
|
const provideKeys = provides.map((provide) => provide.key);
|
|
14624
14839
|
const duplicateProvideKeys = provideKeys.filter((key, i) => provideKeys.indexOf(key) !== i);
|
|
@@ -16589,9 +16804,16 @@ var MANIFEST_ACTIONS = [
|
|
|
16589
16804
|
/** The key names a resource and the document matches it. A pipeline that runs
|
|
16590
16805
|
* on every push and changes nothing reports this, and it is SUCCESS. */
|
|
16591
16806
|
"NO_CHANGE",
|
|
16592
|
-
/**
|
|
16807
|
+
/** `adopt` points at a resource that was DELETED. Its own verb, because both
|
|
16593
16808
|
* alternatives are wrong: `NO_CHANGE` would call a missing resource fine, and
|
|
16594
|
-
* `CREATE`
|
|
16809
|
+
* `CREATE` would silently ignore the `adopt` the caller asked for.
|
|
16810
|
+
*
|
|
16811
|
+
* IT USED TO COVER A SECOND, COMMONER CASE: a manifest whose `metadata.key`
|
|
16812
|
+
* named a deleted resource, back when a key was claimed for ever. A completed
|
|
16813
|
+
* delete now releases the key, so that manifest is planned as a `CREATE` and
|
|
16814
|
+
* the verb narrowed to the one case a caller reaches by naming a `pri_…` by
|
|
16815
|
+
* hand. Kept in the vocabulary rather than removed: it is still produced, and
|
|
16816
|
+
* a Control Plane that still answers it must still be understood. */
|
|
16595
16817
|
"GONE",
|
|
16596
16818
|
/** The document pins a different release. Done through `:upgrade`, which has
|
|
16597
16819
|
* the Definition's own capability check, its own version resolution and its
|
|
@@ -17002,7 +17224,7 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
17002
17224
|
if (!planned.ok) return printError(planned.error, json);
|
|
17003
17225
|
if (verb === "plan") {
|
|
17004
17226
|
printPlan(planned.data, json);
|
|
17005
|
-
if (isUnconverged(planned.data)) {
|
|
17227
|
+
if (isUnconverged(planned.data) && planned.data.action === "NO_CHANGE") {
|
|
17006
17228
|
if (!json) {
|
|
17007
17229
|
process.stderr.write(
|
|
17008
17230
|
`${planned.data.manifestKey ?? "This resource"} is ${planned.data.health?.status ?? "not serving"}. Its desired state is applied; it is not running. \`vfac get operation <id>\` says why.
|
|
@@ -17011,6 +17233,12 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
17011
17233
|
}
|
|
17012
17234
|
return 1;
|
|
17013
17235
|
}
|
|
17236
|
+
if (isUnconverged(planned.data) && !json) {
|
|
17237
|
+
process.stderr.write(
|
|
17238
|
+
`${planned.data.manifestKey ?? "This resource"} is ${planned.data.health?.status ?? "not serving"}. This plan changes it.
|
|
17239
|
+
`
|
|
17240
|
+
);
|
|
17241
|
+
}
|
|
17014
17242
|
return 0;
|
|
17015
17243
|
}
|
|
17016
17244
|
const parsedAction = ManifestActionSchema.safeParse(planned.data.action);
|
|
@@ -17310,23 +17538,20 @@ async function runDoctor(parsed) {
|
|
|
17310
17538
|
const compatible = compatibility(cliVersion(), probe.minimumCliVersion);
|
|
17311
17539
|
checks.push(compatible);
|
|
17312
17540
|
if (!compatible.ok) return report2(checks, json);
|
|
17313
|
-
const
|
|
17314
|
-
if (!
|
|
17315
|
-
checks.push({ name: "
|
|
17541
|
+
const session = await ensureSession({ endpointFlag: parsed.flags["endpoint"] });
|
|
17542
|
+
if (!session.ok) {
|
|
17543
|
+
checks.push({ name: "Sign-in", ok: false, detail: session.message });
|
|
17316
17544
|
return report2(checks, json);
|
|
17317
17545
|
}
|
|
17318
17546
|
checks.push({
|
|
17319
|
-
name: "
|
|
17547
|
+
name: "Sign-in",
|
|
17320
17548
|
ok: true,
|
|
17321
|
-
detail:
|
|
17549
|
+
detail: session.ready.via === "stored" ? "a stored sign-in is still valid, so no credential was spent" : session.ready.gate ? `authenticated as "${session.ready.gate.name}" (${session.ready.gate.id})` : "authenticated"
|
|
17322
17550
|
});
|
|
17323
|
-
if (!sessionUsable(stored.session, endpoint, /* @__PURE__ */ new Date())) {
|
|
17324
|
-
writeStored({ ...stored, session: signedIn.session });
|
|
17325
|
-
}
|
|
17326
17551
|
const who = await call({
|
|
17327
17552
|
endpoint,
|
|
17328
17553
|
path: "/api/v1/whoami",
|
|
17329
|
-
token:
|
|
17554
|
+
token: session.ready.token
|
|
17330
17555
|
});
|
|
17331
17556
|
if (!who.ok) {
|
|
17332
17557
|
checks.push({
|
|
@@ -18219,8 +18444,9 @@ Run \`vfac get product-instance pri_\u2026\` to see which ones this credential m
|
|
|
18219
18444
|
if (command === "delete" && !parsed.booleans.has("yes")) {
|
|
18220
18445
|
process.stderr.write(
|
|
18221
18446
|
`delete removes ${instanceId} and everything it is serving. This cannot be undone.
|
|
18222
|
-
If it was deployed from a manifest, its \`metadata.key\`
|
|
18223
|
-
|
|
18447
|
+
If it was deployed from a manifest, its \`metadata.key\` is held until this delete
|
|
18448
|
+
FINISHES, then released. Applying the same file afterwards creates a new, empty
|
|
18449
|
+
resource under that key. It does not bring this one back.
|
|
18224
18450
|
Re-run with --yes to confirm: vfac lifecycle delete ${instanceId} --yes
|
|
18225
18451
|
Or suspend it instead, which stops it and keeps it: \`vfac lifecycle suspend\`.
|
|
18226
18452
|
`
|
|
@@ -18379,7 +18605,10 @@ Authentication
|
|
|
18379
18605
|
|
|
18380
18606
|
Exit codes
|
|
18381
18607
|
0 done \u2014 including "no change", which is what a pipeline that changes nothing
|
|
18382
|
-
should report
|
|
18608
|
+
should report, AND including a plan that refuses. plan answers what would
|
|
18609
|
+
happen; "it would be refused" is an answer, so it exits 0. Read the action,
|
|
18610
|
+
not the code: vfac plan -f resource.yaml --output json, then branch on
|
|
18611
|
+
.action
|
|
18383
18612
|
1 refused, the request was wrong, or the resource matches your manifest and is
|
|
18384
18613
|
NOT serving. "Nothing needed applying" is not the same as "it works", and a
|
|
18385
18614
|
NO_CHANGE that could be a false green is the case this one exists for
|