@varde-flyt/vfac 0.4.0 → 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 +43 -2
- package/dist/vfac.mjs +387 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,8 +26,18 @@ vfac guide
|
|
|
26
26
|
vfac whoami
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`doctor` checks the endpoint, the API,
|
|
30
|
-
|
|
29
|
+
`doctor` checks the endpoint, the API, whether this client is new enough for the
|
|
30
|
+
platform it is pointed at, your credential and what that credential reaches, in
|
|
31
|
+
that order, and reports the first thing that is actually wrong. A platform
|
|
32
|
+
declares the oldest client it supports; below that, `doctor` fails and says so
|
|
33
|
+
before sending your credential anywhere.
|
|
34
|
+
|
|
35
|
+
`doctor` is also the one command that contacts a host other than your own
|
|
36
|
+
platform: it asks `registry.npmjs.org` whether a newer `vfac` has been published.
|
|
37
|
+
That request is anonymous — no credential, no session, no endpoint, nothing
|
|
38
|
+
identifying you — and it can never change the exit code, so an unreachable or
|
|
39
|
+
blocked registry is reported and otherwise ignored. Set `VFAC_NO_UPDATE_CHECK` to
|
|
40
|
+
any value to skip it entirely; nothing else in this tool makes that request.
|
|
31
41
|
|
|
32
42
|
`vfac guide` is the documentation. It is served by the platform you are pointed
|
|
33
43
|
at rather than bundled here, so it always describes the deployment in front of
|
|
@@ -91,6 +101,37 @@ service, exactly as it resolves the service's own credentials. So there is no
|
|
|
91
101
|
second copy to rotate, and no value for you to read — `vfac get product-instance`
|
|
92
102
|
shows one as `[secret]`. `vfac guide` is authoritative.
|
|
93
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
|
+
|
|
94
135
|
## Connecting to another resource
|
|
95
136
|
|
|
96
137
|
A Product may declare a **service dependency** — a connection to another service
|
package/dist/vfac.mjs
CHANGED
|
@@ -7670,7 +7670,9 @@ async function probeMachineApi(args) {
|
|
|
7670
7670
|
}
|
|
7671
7671
|
const authentication = payload["authentication"];
|
|
7672
7672
|
const contextGate = typeof authentication === "object" && authentication !== null && typeof authentication["contextGate"] === "string" ? authentication["contextGate"] : null;
|
|
7673
|
-
|
|
7673
|
+
const cli = payload["cli"];
|
|
7674
|
+
const minimumCliVersion = typeof cli === "object" && cli !== null && typeof cli["minimumVersion"] === "string" ? cli["minimumVersion"] : null;
|
|
7675
|
+
return { ok: true, service, apiVersion, contextGate, minimumCliVersion };
|
|
7674
7676
|
}
|
|
7675
7677
|
|
|
7676
7678
|
// src/manifest.ts
|
|
@@ -7878,14 +7880,26 @@ function renderOperation(operationId, data) {
|
|
|
7878
7880
|
lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
|
|
7879
7881
|
const summary = failure["summary"];
|
|
7880
7882
|
if (typeof summary === "string") lines.push(` ${summary}`);
|
|
7881
|
-
const detail = failure["detail"];
|
|
7882
|
-
if (typeof detail === "string" && detail !== "") lines.push(` ${detail}`);
|
|
7883
7883
|
const cause = failure["cause"];
|
|
7884
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
|
+
}
|
|
7885
7898
|
const retryable = failure["retryable"];
|
|
7886
7899
|
if (typeof retryable === "boolean") {
|
|
7900
|
+
const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
|
|
7887
7901
|
lines.push(
|
|
7888
|
-
retryable ?
|
|
7902
|
+
retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}.`
|
|
7889
7903
|
);
|
|
7890
7904
|
}
|
|
7891
7905
|
}
|
|
@@ -12259,6 +12273,27 @@ var CustomerConfigurationObjectSchema = external_exports.object({
|
|
|
12259
12273
|
type: external_exports.literal("object"),
|
|
12260
12274
|
properties: external_exports.record(PropertyNameSchema, ConfigurationPropertySchema).describe("Customer-editable settings, keyed by camelCase property name."),
|
|
12261
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."),
|
|
12262
12297
|
/**
|
|
12263
12298
|
* Literal `false`, not merely defaulted: docs/CONVENTIONS.md §8 requires
|
|
12264
12299
|
* contracts to reject unknown fields, and an unknown key accepted here
|
|
@@ -12453,7 +12488,129 @@ var CustomerConfigurationSchema = CustomerConfigurationObjectSchema.superRefine(
|
|
|
12453
12488
|
message: `Duplicate required entries: ${[...new Set(duplicates)].join(", ")}.`
|
|
12454
12489
|
});
|
|
12455
12490
|
}
|
|
12491
|
+
checkDependentRequired(config, ctx);
|
|
12456
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
|
+
}
|
|
12457
12614
|
function enumerableValues(property) {
|
|
12458
12615
|
const out = [];
|
|
12459
12616
|
if (property.type === "string") {
|
|
@@ -12488,8 +12645,9 @@ function isPlainObject(value) {
|
|
|
12488
12645
|
function isSecretProperty(property) {
|
|
12489
12646
|
return property.type === "string" && property["x-secret"] === true;
|
|
12490
12647
|
}
|
|
12491
|
-
function validateSubmittedConfiguration(schema, submitted) {
|
|
12648
|
+
function validateSubmittedConfiguration(schema, submitted, presence = {}) {
|
|
12492
12649
|
const issues = [];
|
|
12650
|
+
const supplied = /* @__PURE__ */ new Set();
|
|
12493
12651
|
if (submitted !== void 0 && submitted !== null && !isPlainObject(submitted)) {
|
|
12494
12652
|
return {
|
|
12495
12653
|
ok: false,
|
|
@@ -12520,6 +12678,8 @@ function validateSubmittedConfiguration(schema, submitted) {
|
|
|
12520
12678
|
const blank = typeof input[name] === "string" && input[name].trim().length === 0;
|
|
12521
12679
|
const present = Object.hasOwn(input, name) && input[name] !== void 0 && !blank;
|
|
12522
12680
|
const secret = isSecretProperty(property);
|
|
12681
|
+
if (present)
|
|
12682
|
+
supplied.add(name);
|
|
12523
12683
|
if (!present) {
|
|
12524
12684
|
const fallback = secret ? void 0 : defaultOf(property);
|
|
12525
12685
|
if (fallback !== void 0) {
|
|
@@ -12544,10 +12704,48 @@ function validateSubmittedConfiguration(schema, submitted) {
|
|
|
12544
12704
|
}
|
|
12545
12705
|
configuration[name] = checked;
|
|
12546
12706
|
}
|
|
12707
|
+
issues.push(...crossFieldIssues(schema, supplied, presence));
|
|
12547
12708
|
if (issues.length > 0)
|
|
12548
12709
|
return { ok: false, issues, partial: configuration };
|
|
12549
12710
|
return { ok: true, configuration, secrets };
|
|
12550
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
|
+
}
|
|
12551
12749
|
function defaultOf(property) {
|
|
12552
12750
|
switch (property.type) {
|
|
12553
12751
|
case "string":
|
|
@@ -12678,6 +12876,15 @@ var RESOURCE_API_VERSION = "resources.vardeflyt.no/v1";
|
|
|
12678
12876
|
var PRODUCT_INSTANCE_KIND = "ProductInstance";
|
|
12679
12877
|
|
|
12680
12878
|
// ../product-configuration/dist/product-instance-example.js
|
|
12879
|
+
function resolveExampleChoice(options, selected, declaredDefault) {
|
|
12880
|
+
if (selected !== void 0)
|
|
12881
|
+
return options.includes(selected) ? { value: selected, issue: null } : { value: null, issue: "invalid" };
|
|
12882
|
+
if (declaredDefault !== void 0 && options.includes(declaredDefault))
|
|
12883
|
+
return { value: declaredDefault, issue: null };
|
|
12884
|
+
if (options.length === 1)
|
|
12885
|
+
return { value: options[0], issue: null };
|
|
12886
|
+
return { value: null, issue: options.length === 0 ? "unavailable" : "required" };
|
|
12887
|
+
}
|
|
12681
12888
|
function isSecret(declaration) {
|
|
12682
12889
|
return declaration.writeOnly === true || declaration["x-secret"] === true;
|
|
12683
12890
|
}
|
|
@@ -12687,6 +12894,14 @@ function seedConfiguration(configuration) {
|
|
|
12687
12894
|
const values = {};
|
|
12688
12895
|
const open = [];
|
|
12689
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));
|
|
12690
12905
|
for (const [name, declaration] of Object.entries(properties)) {
|
|
12691
12906
|
if (isSecret(declaration)) {
|
|
12692
12907
|
if (required.has(name))
|
|
@@ -12702,7 +12917,7 @@ function seedConfiguration(configuration) {
|
|
|
12702
12917
|
open.push({ name, type: declaration.type ?? null });
|
|
12703
12918
|
}
|
|
12704
12919
|
}
|
|
12705
|
-
return { values, open, requiredSecrets };
|
|
12920
|
+
return { values, open, requiredSecrets, groups };
|
|
12706
12921
|
}
|
|
12707
12922
|
function productInstanceExample(input) {
|
|
12708
12923
|
return {
|
|
@@ -13573,11 +13788,11 @@ var ProductDefinitionSchema = ProductDefinitionObjectSchema.superRefine((definit
|
|
|
13573
13788
|
});
|
|
13574
13789
|
return;
|
|
13575
13790
|
}
|
|
13576
|
-
const single = {
|
|
13791
|
+
const single = withoutCrossFieldRules({
|
|
13577
13792
|
...definition.configuration,
|
|
13578
13793
|
properties: { [condition.property]: property },
|
|
13579
13794
|
required: [condition.property]
|
|
13580
|
-
};
|
|
13795
|
+
});
|
|
13581
13796
|
const outside = condition.anyOf.filter((value) => !validateSubmittedConfiguration(single, { [condition.property]: value }).ok);
|
|
13582
13797
|
if (outside.length > 0) {
|
|
13583
13798
|
ctx.addIssue({
|
|
@@ -15736,9 +15951,9 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
15736
15951
|
process.stderr.write("Which manifest? Use -f <file>.\n");
|
|
15737
15952
|
return 2;
|
|
15738
15953
|
}
|
|
15739
|
-
const
|
|
15740
|
-
if (!
|
|
15741
|
-
process.stderr.write(`${
|
|
15954
|
+
const manifest2 = readManifest(file);
|
|
15955
|
+
if (!manifest2.ok) {
|
|
15956
|
+
process.stderr.write(`${manifest2.message}
|
|
15742
15957
|
`);
|
|
15743
15958
|
return 2;
|
|
15744
15959
|
}
|
|
@@ -15764,7 +15979,7 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
15764
15979
|
const json = parsed.flags["output"] === "json";
|
|
15765
15980
|
const path = `/api/v1/projects/${encodeURIComponent(projectId)}/manifest`;
|
|
15766
15981
|
const envelope = {
|
|
15767
|
-
template:
|
|
15982
|
+
template: manifest2.document,
|
|
15768
15983
|
...Object.keys(secrets.secrets).length > 0 ? { secrets: secrets.secrets } : {},
|
|
15769
15984
|
...parsed.flags["adopt"] ? { adopt: parsed.flags["adopt"] } : {}
|
|
15770
15985
|
};
|
|
@@ -15776,7 +15991,7 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
15776
15991
|
token: session.ready.token,
|
|
15777
15992
|
// The DOCUMENT, not the envelope: `validate` asks whether a file is well
|
|
15778
15993
|
// formed, which is a question about the file and not about a request.
|
|
15779
|
-
body: { verb: "validate", payload:
|
|
15994
|
+
body: { verb: "validate", payload: manifest2.document }
|
|
15780
15995
|
});
|
|
15781
15996
|
if (!result.ok) return printError(result.error, json);
|
|
15782
15997
|
process.stdout.write(
|
|
@@ -15849,7 +16064,7 @@ Read the operation that failed: \`vfac get operation <id>\`, or \`vfac get produ
|
|
|
15849
16064
|
body: { verb: "apply", action, payload: envelope }
|
|
15850
16065
|
});
|
|
15851
16066
|
if (!applied.ok) return printError(applied.error, json);
|
|
15852
|
-
const key = manifestKeyOf(
|
|
16067
|
+
const key = manifestKeyOf(manifest2.document) ?? planned.data.manifestKey ?? "(unnamed)";
|
|
15853
16068
|
const rawOperationId = applied.data["operationId"];
|
|
15854
16069
|
const operationId = typeof rawOperationId === "string" && rawOperationId !== "" ? rawOperationId : null;
|
|
15855
16070
|
if (!parsed.booleans.has("wait")) {
|
|
@@ -15993,17 +16208,63 @@ Project ${projectId ?? "(none)"}
|
|
|
15993
16208
|
|
|
15994
16209
|
// src/version.ts
|
|
15995
16210
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
15996
|
-
function
|
|
16211
|
+
function manifest() {
|
|
15997
16212
|
try {
|
|
15998
|
-
const
|
|
16213
|
+
const parsed = JSON.parse(
|
|
15999
16214
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
16000
16215
|
);
|
|
16001
|
-
|
|
16002
|
-
return typeof version === "string" ? version : "unknown";
|
|
16216
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
16003
16217
|
} catch {
|
|
16004
|
-
return
|
|
16218
|
+
return null;
|
|
16005
16219
|
}
|
|
16006
16220
|
}
|
|
16221
|
+
function cliVersion() {
|
|
16222
|
+
const version = manifest()?.["version"];
|
|
16223
|
+
return typeof version === "string" ? version : "unknown";
|
|
16224
|
+
}
|
|
16225
|
+
function cliPackageName() {
|
|
16226
|
+
const name = manifest()?.["name"];
|
|
16227
|
+
return typeof name === "string" ? name : null;
|
|
16228
|
+
}
|
|
16229
|
+
function cliInstallCommand() {
|
|
16230
|
+
const name = cliPackageName();
|
|
16231
|
+
return name === null ? null : `npm install -g ${name}`;
|
|
16232
|
+
}
|
|
16233
|
+
|
|
16234
|
+
// src/registry.ts
|
|
16235
|
+
var UPDATE_CHECK_TIMEOUT_MS = 3e3;
|
|
16236
|
+
var REGISTRY_ORIGIN = "https://registry.npmjs.org";
|
|
16237
|
+
var UPDATE_CHECK_OPT_OUT = "VFAC_NO_UPDATE_CHECK";
|
|
16238
|
+
async function latestPublishedVersion(args) {
|
|
16239
|
+
const env = args?.env ?? process.env;
|
|
16240
|
+
if (env[UPDATE_CHECK_OPT_OUT]) return { ok: false };
|
|
16241
|
+
const name = cliPackageName();
|
|
16242
|
+
if (name === null) return { ok: false };
|
|
16243
|
+
const doFetch = args?.fetchImpl ?? fetch;
|
|
16244
|
+
let response;
|
|
16245
|
+
try {
|
|
16246
|
+
response = await doFetch(`${REGISTRY_ORIGIN}/-/package/${encodeURIComponent(name)}/dist-tags`, {
|
|
16247
|
+
method: "GET",
|
|
16248
|
+
redirect: "manual",
|
|
16249
|
+
signal: signal()
|
|
16250
|
+
});
|
|
16251
|
+
} catch {
|
|
16252
|
+
return { ok: false };
|
|
16253
|
+
}
|
|
16254
|
+
if (!response.ok) return { ok: false };
|
|
16255
|
+
let payload;
|
|
16256
|
+
try {
|
|
16257
|
+
payload = await response.json();
|
|
16258
|
+
} catch {
|
|
16259
|
+
return { ok: false };
|
|
16260
|
+
}
|
|
16261
|
+
if (typeof payload !== "object" || payload === null) return { ok: false };
|
|
16262
|
+
const latest = payload["latest"];
|
|
16263
|
+
return typeof latest === "string" && latest !== "" ? { ok: true, latest } : { ok: false };
|
|
16264
|
+
}
|
|
16265
|
+
function signal() {
|
|
16266
|
+
return typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) : void 0;
|
|
16267
|
+
}
|
|
16007
16268
|
|
|
16008
16269
|
// src/commands/doctor.ts
|
|
16009
16270
|
async function runDoctor(parsed) {
|
|
@@ -16036,6 +16297,9 @@ async function runDoctor(parsed) {
|
|
|
16036
16297
|
return report(checks, json);
|
|
16037
16298
|
}
|
|
16038
16299
|
checks.push({ name: "Machine API", ok: true, detail: probe.apiVersion });
|
|
16300
|
+
const compatible = compatibility(cliVersion(), probe.minimumCliVersion);
|
|
16301
|
+
checks.push(compatible);
|
|
16302
|
+
if (!compatible.ok) return report(checks, json);
|
|
16039
16303
|
const signedIn = await signIn({ endpoint });
|
|
16040
16304
|
if (!signedIn.ok) {
|
|
16041
16305
|
checks.push({ name: "Context Gate", ok: false, detail: signedIn.message });
|
|
@@ -16110,8 +16374,87 @@ async function runDoctor(parsed) {
|
|
|
16110
16374
|
note: true,
|
|
16111
16375
|
detail: "run `vfac guide` for the platform's own instructions, written for a machine"
|
|
16112
16376
|
});
|
|
16377
|
+
checks.push(await updateAvailable(cliVersion()));
|
|
16113
16378
|
return report(checks, json);
|
|
16114
16379
|
}
|
|
16380
|
+
function compatibility(local, declared) {
|
|
16381
|
+
const name = "Compatibility";
|
|
16382
|
+
const facts = { version: local, minimumVersion: declared };
|
|
16383
|
+
if (declared === null) {
|
|
16384
|
+
return {
|
|
16385
|
+
name,
|
|
16386
|
+
ok: true,
|
|
16387
|
+
detail: "this Tenant Portal names no minimum version, so there is nothing to check against.",
|
|
16388
|
+
facts
|
|
16389
|
+
};
|
|
16390
|
+
}
|
|
16391
|
+
if (!PLAIN_SEMVER_REGEX.test(declared)) {
|
|
16392
|
+
return {
|
|
16393
|
+
name,
|
|
16394
|
+
ok: true,
|
|
16395
|
+
note: true,
|
|
16396
|
+
detail: `this Tenant Portal asks for "${declared}", which is not a version this client can read. Treated as no requirement rather than as a refusal.`,
|
|
16397
|
+
facts
|
|
16398
|
+
};
|
|
16399
|
+
}
|
|
16400
|
+
if (!PLAIN_SEMVER_REGEX.test(local)) {
|
|
16401
|
+
return {
|
|
16402
|
+
name,
|
|
16403
|
+
ok: true,
|
|
16404
|
+
note: true,
|
|
16405
|
+
detail: `this build does not report its own version, so it cannot be checked against the vfac ${declared} this Tenant Portal requires.`,
|
|
16406
|
+
facts
|
|
16407
|
+
};
|
|
16408
|
+
}
|
|
16409
|
+
if (compareSemver(local, declared) >= 0) {
|
|
16410
|
+
return { name, ok: true, detail: `OK \u2014 requires vfac ${declared} or newer`, facts };
|
|
16411
|
+
}
|
|
16412
|
+
const install = cliInstallCommand();
|
|
16413
|
+
return {
|
|
16414
|
+
name,
|
|
16415
|
+
ok: false,
|
|
16416
|
+
detail: `UPGRADE REQUIRED. This Tenant Portal supports vfac ${declared} and newer; this is ${local}. ` + (local.includes("-") ? `A prerelease sorts below the release it is named for, so ${local} does not satisfy ${declared}. ` : "") + (install === null ? "" : `Run: ${install}`),
|
|
16417
|
+
facts
|
|
16418
|
+
};
|
|
16419
|
+
}
|
|
16420
|
+
async function updateAvailable(local) {
|
|
16421
|
+
const name = "Update";
|
|
16422
|
+
const result = await latestPublishedVersion();
|
|
16423
|
+
if (!result.ok) {
|
|
16424
|
+
return {
|
|
16425
|
+
name,
|
|
16426
|
+
ok: true,
|
|
16427
|
+
detail: "could not check for updates",
|
|
16428
|
+
facts: { version: local, latest: null }
|
|
16429
|
+
};
|
|
16430
|
+
}
|
|
16431
|
+
const facts = { version: local, latest: result.latest };
|
|
16432
|
+
if (!PLAIN_SEMVER_REGEX.test(local) || !PLAIN_SEMVER_REGEX.test(result.latest)) {
|
|
16433
|
+
return {
|
|
16434
|
+
name,
|
|
16435
|
+
ok: true,
|
|
16436
|
+
detail: `the latest published release is vfac ${result.latest}`,
|
|
16437
|
+
facts
|
|
16438
|
+
};
|
|
16439
|
+
}
|
|
16440
|
+
const order = compareSemver(local, result.latest);
|
|
16441
|
+
if (order > 0) {
|
|
16442
|
+
return {
|
|
16443
|
+
name,
|
|
16444
|
+
ok: true,
|
|
16445
|
+
detail: `vfac ${local} is ahead of the latest release (${result.latest})`,
|
|
16446
|
+
facts
|
|
16447
|
+
};
|
|
16448
|
+
}
|
|
16449
|
+
if (order === 0) return { name, ok: true, detail: `vfac ${local} is the latest release`, facts };
|
|
16450
|
+
return {
|
|
16451
|
+
name,
|
|
16452
|
+
ok: true,
|
|
16453
|
+
note: true,
|
|
16454
|
+
detail: `vfac ${result.latest} is available (running ${local})`,
|
|
16455
|
+
facts
|
|
16456
|
+
};
|
|
16457
|
+
}
|
|
16115
16458
|
function report(checks, json) {
|
|
16116
16459
|
const failed = checks.filter((check) => !check.ok);
|
|
16117
16460
|
if (json) {
|
|
@@ -16724,42 +17067,27 @@ async function runManifestInit(parsed) {
|
|
|
16724
17067
|
}
|
|
16725
17068
|
const regions = product.supportedRegions ?? [];
|
|
16726
17069
|
const chosenRegion = parsed.flags["region"];
|
|
16727
|
-
|
|
16728
|
-
if (
|
|
16729
|
-
if (!regions.includes(chosenRegion)) {
|
|
16730
|
-
return refuse(
|
|
16731
|
-
`${product.productId} ${product.version} does not offer the region "${chosenRegion}".${regions.length > 0 ? ` It offers: ${regions.join(", ")}.` : ""}`
|
|
16732
|
-
);
|
|
16733
|
-
}
|
|
16734
|
-
region = chosenRegion;
|
|
16735
|
-
} else if (regions.length === 1) {
|
|
16736
|
-
region = regions[0];
|
|
16737
|
-
} else {
|
|
17070
|
+
const regionChoice = resolveExampleChoice(regions, chosenRegion);
|
|
17071
|
+
if (regionChoice.value === null) {
|
|
16738
17072
|
return refuse(
|
|
16739
|
-
regions.length
|
|
17073
|
+
regionChoice.issue === "invalid" ? `${product.productId} ${product.version} does not offer the region "${chosenRegion}".${regions.length > 0 ? ` It offers: ${regions.join(", ")}.` : ""}` : regionChoice.issue === "unavailable" ? `${product.productId} ${product.version} declares no region. Ask the publisher.` : `This Product offers several regions. Pass --region <${regions.join("|")}>.`
|
|
16740
17074
|
);
|
|
16741
17075
|
}
|
|
17076
|
+
const region = regionChoice.value;
|
|
16742
17077
|
const profiles = product.profiles ?? [];
|
|
16743
17078
|
const offered = profiles.map((entry) => entry.id);
|
|
16744
17079
|
const chosenProfile = parsed.flags["profile"];
|
|
16745
|
-
const
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
);
|
|
16752
|
-
}
|
|
16753
|
-
profile = chosenProfile;
|
|
16754
|
-
} else if (declaredDefault) {
|
|
16755
|
-
profile = declaredDefault.id;
|
|
16756
|
-
} else if (offered.length === 1) {
|
|
16757
|
-
profile = offered[0];
|
|
16758
|
-
} else {
|
|
17080
|
+
const profileChoice = resolveExampleChoice(
|
|
17081
|
+
offered,
|
|
17082
|
+
chosenProfile,
|
|
17083
|
+
profiles.find((entry) => entry.default === true)?.id
|
|
17084
|
+
);
|
|
17085
|
+
if (profileChoice.value === null) {
|
|
16759
17086
|
return refuse(
|
|
16760
|
-
offered.length
|
|
17087
|
+
profileChoice.issue === "invalid" ? `${product.productId} ${product.version} does not offer the service profile "${chosenProfile}".${offered.length > 0 ? ` It offers: ${offered.join(", ")}.` : ""}` : profileChoice.issue === "unavailable" ? `${product.productId} ${product.version} declares no service profile. Ask the publisher.` : `This Product offers several service profiles. Pass --profile <${offered.join("|")}>.`
|
|
16761
17088
|
);
|
|
16762
17089
|
}
|
|
17090
|
+
const profile = profileChoice.value;
|
|
16763
17091
|
const seed = seedConfiguration(product.configuration);
|
|
16764
17092
|
for (const property of seed.requiredSecrets) {
|
|
16765
17093
|
notes.push(`--secret ${property}=env:VAR \u2014 required, and never written to this file`);
|
|
@@ -16769,6 +17097,13 @@ async function runManifestInit(parsed) {
|
|
|
16769
17097
|
`spec.configuration.${setting.name} \u2014 required${setting.type ? ` (${setting.type})` : ""}`
|
|
16770
17098
|
);
|
|
16771
17099
|
}
|
|
17100
|
+
for (const group of seed.groups) {
|
|
17101
|
+
const settings = group.settings.map((name2) => `spec.configuration.${name2}`);
|
|
17102
|
+
const secrets = group.secrets.map((name2) => `--secret ${name2}=env:VAR`);
|
|
17103
|
+
notes.push(
|
|
17104
|
+
`${[...settings, ...secrets].join(" + ")} \u2014 all of these together, or none of them`
|
|
17105
|
+
);
|
|
17106
|
+
}
|
|
16772
17107
|
const document = productInstanceExample({
|
|
16773
17108
|
name,
|
|
16774
17109
|
...key ? { key } : {},
|
|
@@ -16979,9 +17314,12 @@ Options
|
|
|
16979
17314
|
--output json, for a pipeline that reads the answer
|
|
16980
17315
|
|
|
16981
17316
|
Diagnosing
|
|
16982
|
-
vfac doctor checks the endpoint, the machine API,
|
|
16983
|
-
that
|
|
16984
|
-
|
|
17317
|
+
vfac doctor checks the endpoint, the machine API, whether this client is new
|
|
17318
|
+
enough for that platform, the credential and what that credential
|
|
17319
|
+
can reach \u2014 in that order, so the first thing that is actually
|
|
17320
|
+
wrong is the thing it reports. It ends by asking npm whether a
|
|
17321
|
+
newer vfac has been published, which is a note and never a
|
|
17322
|
+
failure. Set VFAC_NO_UPDATE_CHECK to skip that one request
|
|
16985
17323
|
vfac whoami what this credential is, and which Project scopes it has been
|
|
16986
17324
|
GRANTED. A credential scoped to one Project deploys fine and
|
|
16987
17325
|
cannot make organization-wide reads \u2014 this is where that is said.
|