@varde-flyt/vfac 0.3.2 → 0.5.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 +47 -2
- package/dist/vfac.mjs +234 -60
- 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,41 @@ 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
|
+
## Connecting to another resource
|
|
105
|
+
|
|
106
|
+
A Product may declare a **service dependency** — a connection to another service
|
|
107
|
+
that signs your people in, or that it looks a directory up in. When the platform
|
|
108
|
+
says the customer chooses which resource provides it, name that resource:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
spec:
|
|
112
|
+
dependencyBindings:
|
|
113
|
+
<dependency-key>:
|
|
114
|
+
resourceKey: the-providing-resource
|
|
115
|
+
projectId: prj_… # only if it is in another Project
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`vfac manifest init` names the exact property for every dependency you have to
|
|
119
|
+
choose, and `vfac plan` prints the connections an apply would establish before
|
|
120
|
+
you approve it. The dependency key comes from the Product, not from you —
|
|
121
|
+
`vfac product show <productId>` lists them.
|
|
122
|
+
|
|
123
|
+
This is not the same thing as the section above, and the difference is worth
|
|
124
|
+
holding on to:
|
|
125
|
+
|
|
126
|
+
| | |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `valueFrom.resourceOutput` | read a value another resource publishes |
|
|
129
|
+
| `dependencyBindings` | use that resource as the PROVIDER for a declared dependency |
|
|
130
|
+
|
|
131
|
+
A connection is established once. **A manifest can make one and cannot move
|
|
132
|
+
one**: pointing an existing connection at a different resource is refused rather
|
|
133
|
+
than performed, because a file you re-apply on every push must not be able to
|
|
134
|
+
silently move a running service onto a different identity provider. The
|
|
135
|
+
credential the connection delivers never appears in your manifest, in a plan, or
|
|
136
|
+
in any `vfac` output — the providing service issues it and the platform stores
|
|
137
|
+
it.
|
|
138
|
+
|
|
94
139
|
## Running it again
|
|
95
140
|
|
|
96
141
|
**Send no secret.** A pipeline re-applies the same manifest on every push:
|
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
|
|
@@ -12678,6 +12680,15 @@ var RESOURCE_API_VERSION = "resources.vardeflyt.no/v1";
|
|
|
12678
12680
|
var PRODUCT_INSTANCE_KIND = "ProductInstance";
|
|
12679
12681
|
|
|
12680
12682
|
// ../product-configuration/dist/product-instance-example.js
|
|
12683
|
+
function resolveExampleChoice(options, selected, declaredDefault) {
|
|
12684
|
+
if (selected !== void 0)
|
|
12685
|
+
return options.includes(selected) ? { value: selected, issue: null } : { value: null, issue: "invalid" };
|
|
12686
|
+
if (declaredDefault !== void 0 && options.includes(declaredDefault))
|
|
12687
|
+
return { value: declaredDefault, issue: null };
|
|
12688
|
+
if (options.length === 1)
|
|
12689
|
+
return { value: options[0], issue: null };
|
|
12690
|
+
return { value: null, issue: options.length === 0 ? "unavailable" : "required" };
|
|
12691
|
+
}
|
|
12681
12692
|
function isSecret(declaration) {
|
|
12682
12693
|
return declaration.writeOnly === true || declaration["x-secret"] === true;
|
|
12683
12694
|
}
|
|
@@ -15430,18 +15441,43 @@ function isTerminalAction(action) {
|
|
|
15430
15441
|
}
|
|
15431
15442
|
var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
|
|
15432
15443
|
|
|
15444
|
+
// ../product-contracts/dist/resource-key.js
|
|
15445
|
+
var ResourceKeySchema = external_exports.string().regex(/^[a-z][a-z0-9-]{1,61}[a-z0-9]$/, "A resource key is 3\u201363 characters of lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit.");
|
|
15446
|
+
|
|
15433
15447
|
// ../product-contracts/dist/resource-output-reference.js
|
|
15434
15448
|
var ResourceOutputReferenceSchema = external_exports.object({
|
|
15435
15449
|
valueFrom: external_exports.object({
|
|
15436
15450
|
resourceOutput: external_exports.object({
|
|
15437
15451
|
/** The producing Resource's `metadata.key`, in the same Project. */
|
|
15438
|
-
resourceKey:
|
|
15452
|
+
resourceKey: ResourceKeySchema,
|
|
15439
15453
|
/** The key of an output that Resource's Product declares. */
|
|
15440
15454
|
output: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, "Output keys are camelCase.")
|
|
15441
15455
|
}).strict()
|
|
15442
15456
|
}).strict()
|
|
15443
15457
|
}).strict();
|
|
15444
15458
|
|
|
15459
|
+
// ../product-contracts/dist/dependency-binding.js
|
|
15460
|
+
var DependencyBindingSelectorSchema = external_exports.object({
|
|
15461
|
+
/** The providing Resource's `metadata.key`. */
|
|
15462
|
+
resourceKey: ResourceKeySchema,
|
|
15463
|
+
/**
|
|
15464
|
+
* The Project that Resource lives in. Absent means the consumer's own.
|
|
15465
|
+
*
|
|
15466
|
+
* PRESENT BECAUSE A BINDING IS TENANT-SCOPED AND A PROJECT IS NOT A RUNTIME
|
|
15467
|
+
* BOUNDARY: a provider anywhere in the Tenant can satisfy a consumer
|
|
15468
|
+
* anywhere in it, and that has been true since bindings existed. What the
|
|
15469
|
+
* field does NOT do is grant anything — the Control Plane opens that
|
|
15470
|
+
* Project from the caller's own assertion, and a Project the caller holds
|
|
15471
|
+
* nothing in answers exactly as a Project that does not exist.
|
|
15472
|
+
*
|
|
15473
|
+
* A plain bounded string, exactly as `spec.projectId` is: an id that does
|
|
15474
|
+
* not name a readable Project is refused where the refusal can be phrased
|
|
15475
|
+
* without leaking, which is not here.
|
|
15476
|
+
*/
|
|
15477
|
+
projectId: external_exports.string().min(1).max(64).optional()
|
|
15478
|
+
}).strict();
|
|
15479
|
+
var DependencyBindingsSchema = external_exports.record(ServiceBindingKeySchema, DependencyBindingSelectorSchema);
|
|
15480
|
+
|
|
15445
15481
|
// ../product-contracts/dist/product-instance-template.schema.js
|
|
15446
15482
|
var ProductInstanceTemplateSchema = external_exports.object({
|
|
15447
15483
|
apiVersion: ResourceApiVersionSchema,
|
|
@@ -15502,7 +15538,7 @@ var ProductInstanceTemplateSchema = external_exports.object({
|
|
|
15502
15538
|
* Shaped like a DNS label: it becomes a database predicate and a log
|
|
15503
15539
|
* field, and neither wants free text.
|
|
15504
15540
|
*/
|
|
15505
|
-
key:
|
|
15541
|
+
key: ResourceKeySchema.optional()
|
|
15506
15542
|
}).strict(),
|
|
15507
15543
|
spec: external_exports.object({
|
|
15508
15544
|
productId: external_exports.string().min(1).max(64),
|
|
@@ -15531,7 +15567,27 @@ var ProductInstanceTemplateSchema = external_exports.object({
|
|
|
15531
15567
|
* configuration schema by `resolveTemplate`, which is where the real
|
|
15532
15568
|
* shape lives — it differs per Product, so it cannot be stated here.
|
|
15533
15569
|
*/
|
|
15534
|
-
configuration: external_exports.record(external_exports.unknown()).optional()
|
|
15570
|
+
configuration: external_exports.record(external_exports.unknown()).optional(),
|
|
15571
|
+
/**
|
|
15572
|
+
* Which Resource satisfies each declared service dependency.
|
|
15573
|
+
*
|
|
15574
|
+
* A RESOURCE RELATION, NOT A SETTING, which is why it is a sibling of
|
|
15575
|
+
* `configuration` rather than a key inside it. `configuration` is the
|
|
15576
|
+
* Product's own settings and is validated against the Definition's
|
|
15577
|
+
* schema; this names another Resource, and the create envelope made the
|
|
15578
|
+
* identical split for the identical reason.
|
|
15579
|
+
*
|
|
15580
|
+
* OPTIONAL, PERMANENTLY. Every manifest written before this field
|
|
15581
|
+
* existed omits it, and absence has to keep meaning "leave the
|
|
15582
|
+
* connections alone" — otherwise re-applying an unchanged file would
|
|
15583
|
+
* release a binding somebody made in the portal.
|
|
15584
|
+
*
|
|
15585
|
+
* The keys the platform will ACCEPT here are narrower than the shape:
|
|
15586
|
+
* only a dependency whose interface the registry calls
|
|
15587
|
+
* `tenant-multiple` is one a customer chooses. `resolveTemplate` says
|
|
15588
|
+
* so against the pinned Definition, so the refusal names the field.
|
|
15589
|
+
*/
|
|
15590
|
+
dependencyBindings: DependencyBindingsSchema.optional()
|
|
15535
15591
|
}).strict()
|
|
15536
15592
|
}).strict();
|
|
15537
15593
|
|
|
@@ -15637,6 +15693,21 @@ function printPlan(plan, json) {
|
|
|
15637
15693
|
` from ${reference.resourceKey}.${reference.output}${reference.secret ? " [secret]" : ""}`
|
|
15638
15694
|
);
|
|
15639
15695
|
}
|
|
15696
|
+
const bindings = plan.dependencyBindings ?? [];
|
|
15697
|
+
if (bindings.length > 0) {
|
|
15698
|
+
lines.push(" dependency bindings:");
|
|
15699
|
+
for (const binding of bindings) {
|
|
15700
|
+
lines.push(` ${binding.dependencyKey}`);
|
|
15701
|
+
lines.push(
|
|
15702
|
+
` resource: ${binding.resourceKey}${binding.projectId ? ` (in ${binding.projectId})` : ""}`
|
|
15703
|
+
);
|
|
15704
|
+
if (binding.interfaceId !== void 0) {
|
|
15705
|
+
lines.push(
|
|
15706
|
+
` interface: ${binding.interfaceId}${binding.interfaceVersion ? `/${binding.interfaceVersion}` : ""}`
|
|
15707
|
+
);
|
|
15708
|
+
}
|
|
15709
|
+
}
|
|
15710
|
+
}
|
|
15640
15711
|
const required = plan.requiredSecrets ?? [];
|
|
15641
15712
|
if (required.length > 0) {
|
|
15642
15713
|
lines.push(" required secrets:");
|
|
@@ -15676,9 +15747,9 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
15676
15747
|
process.stderr.write("Which manifest? Use -f <file>.\n");
|
|
15677
15748
|
return 2;
|
|
15678
15749
|
}
|
|
15679
|
-
const
|
|
15680
|
-
if (!
|
|
15681
|
-
process.stderr.write(`${
|
|
15750
|
+
const manifest2 = readManifest(file);
|
|
15751
|
+
if (!manifest2.ok) {
|
|
15752
|
+
process.stderr.write(`${manifest2.message}
|
|
15682
15753
|
`);
|
|
15683
15754
|
return 2;
|
|
15684
15755
|
}
|
|
@@ -15704,7 +15775,7 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
15704
15775
|
const json = parsed.flags["output"] === "json";
|
|
15705
15776
|
const path = `/api/v1/projects/${encodeURIComponent(projectId)}/manifest`;
|
|
15706
15777
|
const envelope = {
|
|
15707
|
-
template:
|
|
15778
|
+
template: manifest2.document,
|
|
15708
15779
|
...Object.keys(secrets.secrets).length > 0 ? { secrets: secrets.secrets } : {},
|
|
15709
15780
|
...parsed.flags["adopt"] ? { adopt: parsed.flags["adopt"] } : {}
|
|
15710
15781
|
};
|
|
@@ -15716,7 +15787,7 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
15716
15787
|
token: session.ready.token,
|
|
15717
15788
|
// The DOCUMENT, not the envelope: `validate` asks whether a file is well
|
|
15718
15789
|
// formed, which is a question about the file and not about a request.
|
|
15719
|
-
body: { verb: "validate", payload:
|
|
15790
|
+
body: { verb: "validate", payload: manifest2.document }
|
|
15720
15791
|
});
|
|
15721
15792
|
if (!result.ok) return printError(result.error, json);
|
|
15722
15793
|
process.stdout.write(
|
|
@@ -15789,7 +15860,7 @@ Read the operation that failed: \`vfac get operation <id>\`, or \`vfac get produ
|
|
|
15789
15860
|
body: { verb: "apply", action, payload: envelope }
|
|
15790
15861
|
});
|
|
15791
15862
|
if (!applied.ok) return printError(applied.error, json);
|
|
15792
|
-
const key = manifestKeyOf(
|
|
15863
|
+
const key = manifestKeyOf(manifest2.document) ?? planned.data.manifestKey ?? "(unnamed)";
|
|
15793
15864
|
const rawOperationId = applied.data["operationId"];
|
|
15794
15865
|
const operationId = typeof rawOperationId === "string" && rawOperationId !== "" ? rawOperationId : null;
|
|
15795
15866
|
if (!parsed.booleans.has("wait")) {
|
|
@@ -15933,17 +16004,63 @@ Project ${projectId ?? "(none)"}
|
|
|
15933
16004
|
|
|
15934
16005
|
// src/version.ts
|
|
15935
16006
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
15936
|
-
function
|
|
16007
|
+
function manifest() {
|
|
15937
16008
|
try {
|
|
15938
|
-
const
|
|
16009
|
+
const parsed = JSON.parse(
|
|
15939
16010
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
15940
16011
|
);
|
|
15941
|
-
|
|
15942
|
-
return typeof version === "string" ? version : "unknown";
|
|
16012
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
15943
16013
|
} catch {
|
|
15944
|
-
return
|
|
16014
|
+
return null;
|
|
15945
16015
|
}
|
|
15946
16016
|
}
|
|
16017
|
+
function cliVersion() {
|
|
16018
|
+
const version = manifest()?.["version"];
|
|
16019
|
+
return typeof version === "string" ? version : "unknown";
|
|
16020
|
+
}
|
|
16021
|
+
function cliPackageName() {
|
|
16022
|
+
const name = manifest()?.["name"];
|
|
16023
|
+
return typeof name === "string" ? name : null;
|
|
16024
|
+
}
|
|
16025
|
+
function cliInstallCommand() {
|
|
16026
|
+
const name = cliPackageName();
|
|
16027
|
+
return name === null ? null : `npm install -g ${name}`;
|
|
16028
|
+
}
|
|
16029
|
+
|
|
16030
|
+
// src/registry.ts
|
|
16031
|
+
var UPDATE_CHECK_TIMEOUT_MS = 3e3;
|
|
16032
|
+
var REGISTRY_ORIGIN = "https://registry.npmjs.org";
|
|
16033
|
+
var UPDATE_CHECK_OPT_OUT = "VFAC_NO_UPDATE_CHECK";
|
|
16034
|
+
async function latestPublishedVersion(args) {
|
|
16035
|
+
const env = args?.env ?? process.env;
|
|
16036
|
+
if (env[UPDATE_CHECK_OPT_OUT]) return { ok: false };
|
|
16037
|
+
const name = cliPackageName();
|
|
16038
|
+
if (name === null) return { ok: false };
|
|
16039
|
+
const doFetch = args?.fetchImpl ?? fetch;
|
|
16040
|
+
let response;
|
|
16041
|
+
try {
|
|
16042
|
+
response = await doFetch(`${REGISTRY_ORIGIN}/-/package/${encodeURIComponent(name)}/dist-tags`, {
|
|
16043
|
+
method: "GET",
|
|
16044
|
+
redirect: "manual",
|
|
16045
|
+
signal: signal()
|
|
16046
|
+
});
|
|
16047
|
+
} catch {
|
|
16048
|
+
return { ok: false };
|
|
16049
|
+
}
|
|
16050
|
+
if (!response.ok) return { ok: false };
|
|
16051
|
+
let payload;
|
|
16052
|
+
try {
|
|
16053
|
+
payload = await response.json();
|
|
16054
|
+
} catch {
|
|
16055
|
+
return { ok: false };
|
|
16056
|
+
}
|
|
16057
|
+
if (typeof payload !== "object" || payload === null) return { ok: false };
|
|
16058
|
+
const latest = payload["latest"];
|
|
16059
|
+
return typeof latest === "string" && latest !== "" ? { ok: true, latest } : { ok: false };
|
|
16060
|
+
}
|
|
16061
|
+
function signal() {
|
|
16062
|
+
return typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) : void 0;
|
|
16063
|
+
}
|
|
15947
16064
|
|
|
15948
16065
|
// src/commands/doctor.ts
|
|
15949
16066
|
async function runDoctor(parsed) {
|
|
@@ -15976,6 +16093,9 @@ async function runDoctor(parsed) {
|
|
|
15976
16093
|
return report(checks, json);
|
|
15977
16094
|
}
|
|
15978
16095
|
checks.push({ name: "Machine API", ok: true, detail: probe.apiVersion });
|
|
16096
|
+
const compatible = compatibility(cliVersion(), probe.minimumCliVersion);
|
|
16097
|
+
checks.push(compatible);
|
|
16098
|
+
if (!compatible.ok) return report(checks, json);
|
|
15979
16099
|
const signedIn = await signIn({ endpoint });
|
|
15980
16100
|
if (!signedIn.ok) {
|
|
15981
16101
|
checks.push({ name: "Context Gate", ok: false, detail: signedIn.message });
|
|
@@ -16050,8 +16170,87 @@ async function runDoctor(parsed) {
|
|
|
16050
16170
|
note: true,
|
|
16051
16171
|
detail: "run `vfac guide` for the platform's own instructions, written for a machine"
|
|
16052
16172
|
});
|
|
16173
|
+
checks.push(await updateAvailable(cliVersion()));
|
|
16053
16174
|
return report(checks, json);
|
|
16054
16175
|
}
|
|
16176
|
+
function compatibility(local, declared) {
|
|
16177
|
+
const name = "Compatibility";
|
|
16178
|
+
const facts = { version: local, minimumVersion: declared };
|
|
16179
|
+
if (declared === null) {
|
|
16180
|
+
return {
|
|
16181
|
+
name,
|
|
16182
|
+
ok: true,
|
|
16183
|
+
detail: "this Tenant Portal names no minimum version, so there is nothing to check against.",
|
|
16184
|
+
facts
|
|
16185
|
+
};
|
|
16186
|
+
}
|
|
16187
|
+
if (!PLAIN_SEMVER_REGEX.test(declared)) {
|
|
16188
|
+
return {
|
|
16189
|
+
name,
|
|
16190
|
+
ok: true,
|
|
16191
|
+
note: true,
|
|
16192
|
+
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.`,
|
|
16193
|
+
facts
|
|
16194
|
+
};
|
|
16195
|
+
}
|
|
16196
|
+
if (!PLAIN_SEMVER_REGEX.test(local)) {
|
|
16197
|
+
return {
|
|
16198
|
+
name,
|
|
16199
|
+
ok: true,
|
|
16200
|
+
note: true,
|
|
16201
|
+
detail: `this build does not report its own version, so it cannot be checked against the vfac ${declared} this Tenant Portal requires.`,
|
|
16202
|
+
facts
|
|
16203
|
+
};
|
|
16204
|
+
}
|
|
16205
|
+
if (compareSemver(local, declared) >= 0) {
|
|
16206
|
+
return { name, ok: true, detail: `OK \u2014 requires vfac ${declared} or newer`, facts };
|
|
16207
|
+
}
|
|
16208
|
+
const install = cliInstallCommand();
|
|
16209
|
+
return {
|
|
16210
|
+
name,
|
|
16211
|
+
ok: false,
|
|
16212
|
+
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}`),
|
|
16213
|
+
facts
|
|
16214
|
+
};
|
|
16215
|
+
}
|
|
16216
|
+
async function updateAvailable(local) {
|
|
16217
|
+
const name = "Update";
|
|
16218
|
+
const result = await latestPublishedVersion();
|
|
16219
|
+
if (!result.ok) {
|
|
16220
|
+
return {
|
|
16221
|
+
name,
|
|
16222
|
+
ok: true,
|
|
16223
|
+
detail: "could not check for updates",
|
|
16224
|
+
facts: { version: local, latest: null }
|
|
16225
|
+
};
|
|
16226
|
+
}
|
|
16227
|
+
const facts = { version: local, latest: result.latest };
|
|
16228
|
+
if (!PLAIN_SEMVER_REGEX.test(local) || !PLAIN_SEMVER_REGEX.test(result.latest)) {
|
|
16229
|
+
return {
|
|
16230
|
+
name,
|
|
16231
|
+
ok: true,
|
|
16232
|
+
detail: `the latest published release is vfac ${result.latest}`,
|
|
16233
|
+
facts
|
|
16234
|
+
};
|
|
16235
|
+
}
|
|
16236
|
+
const order = compareSemver(local, result.latest);
|
|
16237
|
+
if (order > 0) {
|
|
16238
|
+
return {
|
|
16239
|
+
name,
|
|
16240
|
+
ok: true,
|
|
16241
|
+
detail: `vfac ${local} is ahead of the latest release (${result.latest})`,
|
|
16242
|
+
facts
|
|
16243
|
+
};
|
|
16244
|
+
}
|
|
16245
|
+
if (order === 0) return { name, ok: true, detail: `vfac ${local} is the latest release`, facts };
|
|
16246
|
+
return {
|
|
16247
|
+
name,
|
|
16248
|
+
ok: true,
|
|
16249
|
+
note: true,
|
|
16250
|
+
detail: `vfac ${result.latest} is available (running ${local})`,
|
|
16251
|
+
facts
|
|
16252
|
+
};
|
|
16253
|
+
}
|
|
16055
16254
|
function report(checks, json) {
|
|
16056
16255
|
const failed = checks.filter((check) => !check.ok);
|
|
16057
16256
|
if (json) {
|
|
@@ -16656,63 +16855,35 @@ async function runManifestInit(parsed) {
|
|
|
16656
16855
|
const chooseable = (product.dependencies ?? []).filter(
|
|
16657
16856
|
(dependency) => dependency.interface?.cardinality === "tenant-multiple"
|
|
16658
16857
|
);
|
|
16659
|
-
const mustChoose = chooseable.filter((dependency) => dependency.required === true);
|
|
16660
|
-
if (mustChoose.length > 0) {
|
|
16661
|
-
process.stderr.write(
|
|
16662
|
-
`${product.productId} requires a customer-selected service dependency that
|
|
16663
|
-
Product Deployment as Code v1 cannot express yet. Deploy it from the portal.
|
|
16664
|
-
`
|
|
16665
|
-
);
|
|
16666
|
-
for (const dependency of mustChoose) {
|
|
16667
|
-
process.stderr.write(` ${dependency.interface?.id ?? "a service"}`);
|
|
16668
|
-
process.stderr.write(dependency.reason ? ` \u2014 ${dependency.reason}
|
|
16669
|
-
` : "\n");
|
|
16670
|
-
}
|
|
16671
|
-
return 1;
|
|
16672
|
-
}
|
|
16673
16858
|
for (const dependency of chooseable) {
|
|
16859
|
+
const property = dependency.key === void 0 ? "spec.dependencyBindings.<key>.resourceKey" : `spec.dependencyBindings.${dependency.key}.resourceKey`;
|
|
16674
16860
|
notes.push(
|
|
16675
|
-
`${dependency.
|
|
16861
|
+
`${property} \u2014 ${dependency.required === true ? "required" : "optional, unless your settings make it required"}${dependency.interface?.id ? `. Provides ${dependency.interface.id}` : ""}. Name an existing resource: vfac get product-instances`
|
|
16676
16862
|
);
|
|
16677
16863
|
}
|
|
16678
16864
|
const regions = product.supportedRegions ?? [];
|
|
16679
16865
|
const chosenRegion = parsed.flags["region"];
|
|
16680
|
-
|
|
16681
|
-
if (
|
|
16682
|
-
if (!regions.includes(chosenRegion)) {
|
|
16683
|
-
return refuse(
|
|
16684
|
-
`${product.productId} ${product.version} does not offer the region "${chosenRegion}".${regions.length > 0 ? ` It offers: ${regions.join(", ")}.` : ""}`
|
|
16685
|
-
);
|
|
16686
|
-
}
|
|
16687
|
-
region = chosenRegion;
|
|
16688
|
-
} else if (regions.length === 1) {
|
|
16689
|
-
region = regions[0];
|
|
16690
|
-
} else {
|
|
16866
|
+
const regionChoice = resolveExampleChoice(regions, chosenRegion);
|
|
16867
|
+
if (regionChoice.value === null) {
|
|
16691
16868
|
return refuse(
|
|
16692
|
-
regions.length
|
|
16869
|
+
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("|")}>.`
|
|
16693
16870
|
);
|
|
16694
16871
|
}
|
|
16872
|
+
const region = regionChoice.value;
|
|
16695
16873
|
const profiles = product.profiles ?? [];
|
|
16696
16874
|
const offered = profiles.map((entry) => entry.id);
|
|
16697
16875
|
const chosenProfile = parsed.flags["profile"];
|
|
16698
|
-
const
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
);
|
|
16705
|
-
}
|
|
16706
|
-
profile = chosenProfile;
|
|
16707
|
-
} else if (declaredDefault) {
|
|
16708
|
-
profile = declaredDefault.id;
|
|
16709
|
-
} else if (offered.length === 1) {
|
|
16710
|
-
profile = offered[0];
|
|
16711
|
-
} else {
|
|
16876
|
+
const profileChoice = resolveExampleChoice(
|
|
16877
|
+
offered,
|
|
16878
|
+
chosenProfile,
|
|
16879
|
+
profiles.find((entry) => entry.default === true)?.id
|
|
16880
|
+
);
|
|
16881
|
+
if (profileChoice.value === null) {
|
|
16712
16882
|
return refuse(
|
|
16713
|
-
offered.length
|
|
16883
|
+
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("|")}>.`
|
|
16714
16884
|
);
|
|
16715
16885
|
}
|
|
16886
|
+
const profile = profileChoice.value;
|
|
16716
16887
|
const seed = seedConfiguration(product.configuration);
|
|
16717
16888
|
for (const property of seed.requiredSecrets) {
|
|
16718
16889
|
notes.push(`--secret ${property}=env:VAR \u2014 required, and never written to this file`);
|
|
@@ -16932,9 +17103,12 @@ Options
|
|
|
16932
17103
|
--output json, for a pipeline that reads the answer
|
|
16933
17104
|
|
|
16934
17105
|
Diagnosing
|
|
16935
|
-
vfac doctor checks the endpoint, the machine API,
|
|
16936
|
-
that
|
|
16937
|
-
|
|
17106
|
+
vfac doctor checks the endpoint, the machine API, whether this client is new
|
|
17107
|
+
enough for that platform, the credential and what that credential
|
|
17108
|
+
can reach \u2014 in that order, so the first thing that is actually
|
|
17109
|
+
wrong is the thing it reports. It ends by asking npm whether a
|
|
17110
|
+
newer vfac has been published, which is a note and never a
|
|
17111
|
+
failure. Set VFAC_NO_UPDATE_CHECK to skip that one request
|
|
16938
17112
|
vfac whoami what this credential is, and which Project scopes it has been
|
|
16939
17113
|
GRANTED. A credential scoped to one Project deploys fine and
|
|
16940
17114
|
cannot make organization-wide reads \u2014 this is where that is said.
|