@varde-flyt/vfac 0.6.0 → 0.7.1
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 +29 -5
- package/dist/vfac.mjs +145 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,11 +26,19 @@ vfac guide
|
|
|
26
26
|
vfac whoami
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`doctor` checks the endpoint,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
declares the oldest client it supports;
|
|
33
|
-
before sending your credential anywhere.
|
|
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, your
|
|
31
|
+
credential and what that credential reaches, in that order, and reports the first
|
|
32
|
+
thing that is actually wrong. A platform declares the oldest client it supports;
|
|
33
|
+
below that, `doctor` fails and says so before sending your credential anywhere.
|
|
34
|
+
|
|
35
|
+
The selected Project and the Projects your credential reaches are two different
|
|
36
|
+
lines, and reading the second as the first is the mistake worth naming: a command
|
|
37
|
+
with no `--project` uses the SELECTED one, and having access to a Project is not
|
|
38
|
+
the same as being in it. `doctor` also says so when it has just started a fresh
|
|
39
|
+
config because the old one could not be read — the endpoint and Project you had
|
|
40
|
+
set are gone in that case, and the unreadable file is kept beside it rather than
|
|
41
|
+
discarded.
|
|
34
42
|
|
|
35
43
|
`doctor` is also the one command that contacts a host other than your own
|
|
36
44
|
platform: it asks `registry.npmjs.org` whether a newer `vfac` has been published.
|
|
@@ -73,6 +81,22 @@ vfac get product-instance <productInstanceId>
|
|
|
73
81
|
Secrets are supplied per apply and never belong in the manifest — a manifest is a
|
|
74
82
|
file you commit. `vfac plan` lists the ones an apply will still need.
|
|
75
83
|
|
|
84
|
+
### `metadata.key` is the name you choose
|
|
85
|
+
|
|
86
|
+
`manifest init` writes one into the file, derived from `--name`. It is what the
|
|
87
|
+
platform recognises the resource by on every later apply, and it is the one
|
|
88
|
+
identifier in a manifest that is yours: a resource key is 3–63 characters of
|
|
89
|
+
lowercase letters, digits and hyphens, starting with a letter and ending with a
|
|
90
|
+
letter or digit.
|
|
91
|
+
|
|
92
|
+
**A key is claimed for good, and a delete does not release it.** The record of a
|
|
93
|
+
deleted resource keeps its key, so a manifest that named it can never apply again
|
|
94
|
+
— `vfac plan` answers `GONE` — and a replacement needs a different key. That is
|
|
95
|
+
worth knowing before the delete rather than after it, because after it there is
|
|
96
|
+
nothing to undo; `vfac lifecycle delete` says the same thing at the prompt. If
|
|
97
|
+
what you want is to stop the service and keep the option of bringing it back,
|
|
98
|
+
`vfac lifecycle suspend` is the reversible one.
|
|
99
|
+
|
|
76
100
|
## Reading a setting from another resource
|
|
77
101
|
|
|
78
102
|
A setting may take its value from an output another resource publishes, instead
|
package/dist/vfac.mjs
CHANGED
|
@@ -7424,22 +7424,46 @@ function parseArgs(argv) {
|
|
|
7424
7424
|
}
|
|
7425
7425
|
|
|
7426
7426
|
// src/config.ts
|
|
7427
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7427
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7428
7428
|
import { homedir } from "node:os";
|
|
7429
7429
|
import { dirname, join } from "node:path";
|
|
7430
|
+
var salvaged = null;
|
|
7431
|
+
function salvagedConfigPath() {
|
|
7432
|
+
return salvaged;
|
|
7433
|
+
}
|
|
7430
7434
|
function configPath() {
|
|
7431
7435
|
const base = process.env["VFAC_CONFIG_HOME"] ?? join(homedir(), ".config", "vfac");
|
|
7432
7436
|
return join(base, "config.json");
|
|
7433
7437
|
}
|
|
7434
7438
|
function readStored(path = configPath()) {
|
|
7439
|
+
let raw;
|
|
7435
7440
|
try {
|
|
7436
|
-
|
|
7437
|
-
|
|
7441
|
+
raw = readFileSync(path, "utf8");
|
|
7442
|
+
} catch {
|
|
7443
|
+
return {};
|
|
7444
|
+
}
|
|
7445
|
+
try {
|
|
7446
|
+
const parsed = JSON.parse(raw);
|
|
7447
|
+
if (typeof parsed !== "object" || parsed === null) throw new Error("not an object");
|
|
7438
7448
|
return parsed;
|
|
7439
7449
|
} catch {
|
|
7450
|
+
salvage(path);
|
|
7440
7451
|
return {};
|
|
7441
7452
|
}
|
|
7442
7453
|
}
|
|
7454
|
+
function salvage(path) {
|
|
7455
|
+
const kept = `${path}.corrupt`;
|
|
7456
|
+
try {
|
|
7457
|
+
readFileSync(kept, "utf8");
|
|
7458
|
+
return;
|
|
7459
|
+
} catch {
|
|
7460
|
+
}
|
|
7461
|
+
try {
|
|
7462
|
+
renameSync(path, kept);
|
|
7463
|
+
salvaged = kept;
|
|
7464
|
+
} catch {
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7443
7467
|
function writeStored(next, path = configPath()) {
|
|
7444
7468
|
const directory = dirname(path);
|
|
7445
7469
|
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
@@ -12875,6 +12899,11 @@ function matches(pattern, value) {
|
|
|
12875
12899
|
var RESOURCE_API_VERSION = "resources.vardeflyt.no/v1";
|
|
12876
12900
|
var PRODUCT_INSTANCE_KIND = "ProductInstance";
|
|
12877
12901
|
|
|
12902
|
+
// ../product-configuration/dist/resource-key.js
|
|
12903
|
+
var RESOURCE_KEY_PATTERN = /^[a-z][a-z0-9-]{1,61}[a-z0-9]$/;
|
|
12904
|
+
var RESOURCE_KEY_RULE = "A resource key is 3\u201363 characters of lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit.";
|
|
12905
|
+
var ResourceKeySchema = external_exports.string().regex(RESOURCE_KEY_PATTERN, RESOURCE_KEY_RULE).describe(RESOURCE_KEY_RULE);
|
|
12906
|
+
|
|
12878
12907
|
// ../product-configuration/dist/product-instance-example.js
|
|
12879
12908
|
function resolveExampleChoice(options, selected, declaredDefault) {
|
|
12880
12909
|
if (selected !== void 0)
|
|
@@ -12937,7 +12966,7 @@ function productInstanceExample(input) {
|
|
|
12937
12966
|
}
|
|
12938
12967
|
function exampleResourceKey(name) {
|
|
12939
12968
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "").slice(0, 63);
|
|
12940
|
-
return
|
|
12969
|
+
return RESOURCE_KEY_PATTERN.test(slug) ? slug : "";
|
|
12941
12970
|
}
|
|
12942
12971
|
|
|
12943
12972
|
// ../platform-schemas/dist/index.js
|
|
@@ -13403,6 +13432,16 @@ var ProductOutputSchema = external_exports.object({
|
|
|
13403
13432
|
*/
|
|
13404
13433
|
secret: external_exports.boolean().optional()
|
|
13405
13434
|
}).strict();
|
|
13435
|
+
var ProductConditionSchema = external_exports.object({
|
|
13436
|
+
key: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, "Condition keys are camelCase.").describe("Stable key. A running instance reports its state under this key."),
|
|
13437
|
+
displayName: external_exports.string().min(2).max(60),
|
|
13438
|
+
/**
|
|
13439
|
+
* What the customer must understand, and — when it is ACTION_REQUIRED —
|
|
13440
|
+
* enough to act on. The platform never writes this sentence: it does not
|
|
13441
|
+
* know what the condition is about.
|
|
13442
|
+
*/
|
|
13443
|
+
description: external_exports.string().min(10).max(300)
|
|
13444
|
+
}).strict();
|
|
13406
13445
|
var ProductDependencySchema = external_exports.object({
|
|
13407
13446
|
/**
|
|
13408
13447
|
* OPTIONAL ONLY WHEN `interface` IS PRESENT, enforced below.
|
|
@@ -13631,6 +13670,16 @@ var ProductDefinitionObjectSchema = external_exports.object({
|
|
|
13631
13670
|
// --- Results, relationships, operations -------------------------------
|
|
13632
13671
|
outputs: external_exports.array(ProductOutputSchema).min(1).max(MAX_PRODUCT_OUTPUTS),
|
|
13633
13672
|
dependencies: external_exports.array(ProductDependencySchema).max(8),
|
|
13673
|
+
/**
|
|
13674
|
+
* Operational conditions a running instance of this Product may report.
|
|
13675
|
+
*
|
|
13676
|
+
* `.optional()`, NEVER `.default([])`. A default materialises the key,
|
|
13677
|
+
* `contractDigest` digests the parsed object, and a republish of unchanged
|
|
13678
|
+
* bytes would then be refused as `RELEASED_CONTRACT_MUTATED`. Absent means
|
|
13679
|
+
* this Product reports none, which is what every Definition published
|
|
13680
|
+
* before this field already means.
|
|
13681
|
+
*/
|
|
13682
|
+
conditions: external_exports.array(ProductConditionSchema).min(1).max(8).optional(),
|
|
13634
13683
|
/**
|
|
13635
13684
|
* The service interfaces this Product provides to other Products.
|
|
13636
13685
|
*
|
|
@@ -13697,6 +13746,15 @@ var ProductDefinitionSchema = ProductDefinitionObjectSchema.superRefine((definit
|
|
|
13697
13746
|
});
|
|
13698
13747
|
}
|
|
13699
13748
|
});
|
|
13749
|
+
const conditionKeys = (definition.conditions ?? []).map((condition) => condition.key);
|
|
13750
|
+
const duplicateConditions = conditionKeys.filter((key, i) => conditionKeys.indexOf(key) !== i);
|
|
13751
|
+
if (duplicateConditions.length > 0) {
|
|
13752
|
+
ctx.addIssue({
|
|
13753
|
+
code: external_exports.ZodIssueCode.custom,
|
|
13754
|
+
path: ["conditions"],
|
|
13755
|
+
message: `Duplicate condition keys: ${[...new Set(duplicateConditions)].join(", ")}.`
|
|
13756
|
+
});
|
|
13757
|
+
}
|
|
13700
13758
|
const dependencyIds = definition.dependencies.map((dependency) => dependency.productId).filter((id) => id !== void 0);
|
|
13701
13759
|
const duplicateDependencies = dependencyIds.filter((id, i) => dependencyIds.indexOf(id) !== i);
|
|
13702
13760
|
if (duplicateDependencies.length > 0) {
|
|
@@ -14880,6 +14938,33 @@ var DeploymentBlueprintObjectSchema = external_exports.object({
|
|
|
14880
14938
|
serviceExports: external_exports.array(ServiceExportSchema).max(MAX_SERVICE_EXPORTS).optional(),
|
|
14881
14939
|
/** Service interfaces this Product consumes. Absent means none. */
|
|
14882
14940
|
serviceBindings: external_exports.array(ServiceBindingSchema).max(MAX_SERVICE_BINDINGS).optional(),
|
|
14941
|
+
/**
|
|
14942
|
+
* Which of your own components may report an operational condition.
|
|
14943
|
+
*
|
|
14944
|
+
* NOT DECORATION, and the same policy `dynamicResources[].consumers` is. The
|
|
14945
|
+
* Control Plane mints the condition-reporting credential only for a named
|
|
14946
|
+
* component, so a Blueprint that names none has declared conditions in its
|
|
14947
|
+
* Definition that nothing can reach. Delivery is the enforcement.
|
|
14948
|
+
*
|
|
14949
|
+
* `.optional()`, NEVER `.default([])` — `contractDigest` digests the parsed
|
|
14950
|
+
* object, and a default would materialise the key on every Blueprint
|
|
14951
|
+
* published before this field existed.
|
|
14952
|
+
*
|
|
14953
|
+
* EXACTLY ONE, and that is a property of what a condition IS rather than a
|
|
14954
|
+
* limit somebody will want raised. A condition is keyed per INSTANCE, not
|
|
14955
|
+
* per component: two components reporting `sso` write the same row and
|
|
14956
|
+
* decide it between them by whichever clock is ahead, which is a race with
|
|
14957
|
+
* a customer-visible answer. And "is this Product usable" is a statement
|
|
14958
|
+
* about the instance — CLAUDE.md's "One Product Instance is ONE runtime
|
|
14959
|
+
* unit" — so a second voice is a second opinion, not more coverage.
|
|
14960
|
+
*
|
|
14961
|
+
* It also keeps the platform's withdrawal honest. The watermark that stops
|
|
14962
|
+
* a superseded runtime's answer being served is per instance, because what
|
|
14963
|
+
* replaced the container is an instance-level event; with two reporters,
|
|
14964
|
+
* redeploying one would withdraw the other's still-correct answer and it
|
|
14965
|
+
* would never speak again.
|
|
14966
|
+
*/
|
|
14967
|
+
conditionReporters: external_exports.array(ComponentIdSchema).min(1).max(1).optional(),
|
|
14883
14968
|
secrets: external_exports.array(SecretWiringSchema).max(20),
|
|
14884
14969
|
dataStores: external_exports.array(DataStoreSchema).max(8),
|
|
14885
14970
|
egress: external_exports.array(EgressRuleSchema).max(24),
|
|
@@ -15645,17 +15730,15 @@ function isTerminalAction(action) {
|
|
|
15645
15730
|
}
|
|
15646
15731
|
var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
|
|
15647
15732
|
|
|
15648
|
-
// ../product-contracts/dist/resource-key.js
|
|
15649
|
-
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.");
|
|
15650
|
-
|
|
15651
15733
|
// ../product-contracts/dist/resource-output-reference.js
|
|
15734
|
+
var OUTPUT_KEY_RULE = "Output keys are camelCase.";
|
|
15652
15735
|
var ResourceOutputReferenceSchema = external_exports.object({
|
|
15653
15736
|
valueFrom: external_exports.object({
|
|
15654
15737
|
resourceOutput: external_exports.object({
|
|
15655
15738
|
/** The producing Resource's `metadata.key`, in the same Project. */
|
|
15656
15739
|
resourceKey: ResourceKeySchema,
|
|
15657
15740
|
/** The key of an output that Resource's Product declares. */
|
|
15658
|
-
output: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/,
|
|
15741
|
+
output: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, OUTPUT_KEY_RULE)
|
|
15659
15742
|
}).strict()
|
|
15660
15743
|
}).strict()
|
|
15661
15744
|
}).strict();
|
|
@@ -16271,6 +16354,16 @@ async function runDoctor(parsed) {
|
|
|
16271
16354
|
const json = parsed.flags["output"] === "json";
|
|
16272
16355
|
const checks = [{ name: "CLI", ok: true, detail: `vfac ${cliVersion()}` }];
|
|
16273
16356
|
const stored = readStored();
|
|
16357
|
+
const salvaged2 = salvagedConfigPath();
|
|
16358
|
+
if (salvaged2 !== null) {
|
|
16359
|
+
checks.push({
|
|
16360
|
+
name: "Config",
|
|
16361
|
+
ok: true,
|
|
16362
|
+
note: true,
|
|
16363
|
+
detail: `the previous config could not be read and was kept at ${salvaged2}. What this command reports below is a fresh one \u2014 the endpoint and Project it held are gone until you set them again. Delete the kept file once you have; it may hold a stale session.`,
|
|
16364
|
+
facts: { salvagedConfigPath: salvaged2 }
|
|
16365
|
+
});
|
|
16366
|
+
}
|
|
16274
16367
|
const resolved = resolveEndpoint(parsed.flags["endpoint"], stored);
|
|
16275
16368
|
if (!resolved) {
|
|
16276
16369
|
checks.push({
|
|
@@ -16289,7 +16382,16 @@ async function runDoctor(parsed) {
|
|
|
16289
16382
|
checks.push({
|
|
16290
16383
|
name: "Endpoint",
|
|
16291
16384
|
ok: true,
|
|
16292
|
-
detail: `${endpoint} (from ${resolved.source})
|
|
16385
|
+
detail: `${endpoint} (from ${resolved.source})`,
|
|
16386
|
+
facts: { endpoint, source: resolved.source }
|
|
16387
|
+
});
|
|
16388
|
+
const selected = stored.context?.projectId ?? null;
|
|
16389
|
+
checks.push({
|
|
16390
|
+
name: "Project",
|
|
16391
|
+
ok: true,
|
|
16392
|
+
note: selected === null,
|
|
16393
|
+
detail: selected === null ? "none selected. Commands need `--project prj_\u2026`, or run `vfac context set --project prj_\u2026` once. This is not a fault \u2014 it is what a pipeline that names the Project per command looks like \u2014 but it is NOT the same thing as which Projects your credential reaches." : `${selected} (selected in this context; which Projects the credential reaches is a separate question)`,
|
|
16394
|
+
facts: { selectedProjectId: selected }
|
|
16293
16395
|
});
|
|
16294
16396
|
const probe = await probeMachineApi({ endpoint });
|
|
16295
16397
|
if (!probe.ok) {
|
|
@@ -16335,7 +16437,8 @@ async function runDoctor(parsed) {
|
|
|
16335
16437
|
access.tenantWide ? {
|
|
16336
16438
|
name: "Access",
|
|
16337
16439
|
ok: true,
|
|
16338
|
-
detail: `the whole organization \u2014 ${access.tenantPermissions.join(", ")}
|
|
16440
|
+
detail: `the whole organization \u2014 ${access.tenantPermissions.join(", ")}`,
|
|
16441
|
+
facts: { scope: "organization", grantedProjectIds: null }
|
|
16339
16442
|
} : projects.length === 0 ? {
|
|
16340
16443
|
// A FAILURE, NOT A NOTE. This credential authenticates and can do
|
|
16341
16444
|
// nothing at all — and the check said so in words while `report`
|
|
@@ -16365,6 +16468,14 @@ async function runDoctor(parsed) {
|
|
|
16365
16468
|
// believes it can deploy and is then refused reports a platform
|
|
16366
16469
|
// defect, which is precisely the outcome the guide now tells it not
|
|
16367
16470
|
// to reach for. Scope and role are two questions; this answers both.
|
|
16471
|
+
facts: {
|
|
16472
|
+
scope: "projects",
|
|
16473
|
+
// THE GRANT, and the name says which of the two questions it
|
|
16474
|
+
// answers. A reader gating on `doctor --output json` had no way
|
|
16475
|
+
// to tell this list from the selected Project; now the field
|
|
16476
|
+
// names are the difference.
|
|
16477
|
+
grantedProjectIds: projects.join(",")
|
|
16478
|
+
},
|
|
16368
16479
|
detail: `${projects.join(", ")} \u2014 and nothing organization-wide. ` + (mutating.length > 0 ? "You can deploy in those Projects, read their catalogue, and list them. " : "Read-only there: catalogue, manifests, resources and operations. An apply or a lifecycle command is refused, and that is what was granted rather than a fault. ") + "Anything ACROSS the organization \u2014 a Project you hold nothing in, or its members \u2014 is refused."
|
|
16369
16480
|
}
|
|
16370
16481
|
);
|
|
@@ -16624,6 +16735,27 @@ async function runGet(parsed) {
|
|
|
16624
16735
|
const declaredNames = declared.map((entry) => entry["key"]).filter((key2) => typeof key2 === "string");
|
|
16625
16736
|
const names = [.../* @__PURE__ */ new Set([...Object.keys(outputs), ...declaredNames])].sort();
|
|
16626
16737
|
const isSecret2 = (name) => declared.find((entry) => entry["key"] === name)?.["secret"] === true;
|
|
16738
|
+
const conditions = Array.isArray(resource["conditions"]) ? resource["conditions"] : [];
|
|
16739
|
+
if (conditions.length > 0) {
|
|
16740
|
+
lines.push("", " Operational");
|
|
16741
|
+
const width = Math.max(
|
|
16742
|
+
...conditions.map((one) => String(one["displayName"] ?? one["key"] ?? "").length)
|
|
16743
|
+
);
|
|
16744
|
+
for (const one of conditions) {
|
|
16745
|
+
const label = String(one["displayName"] ?? one["key"] ?? "");
|
|
16746
|
+
const state = String(one["state"] ?? "UNKNOWN");
|
|
16747
|
+
const reason = one["reason"];
|
|
16748
|
+
lines.push(
|
|
16749
|
+
` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}`
|
|
16750
|
+
);
|
|
16751
|
+
const description = one["description"];
|
|
16752
|
+
if (typeof description === "string") {
|
|
16753
|
+
for (const line of wrapOutput(description, 68)) {
|
|
16754
|
+
lines.push(` ${" ".repeat(width)} ${line}`);
|
|
16755
|
+
}
|
|
16756
|
+
}
|
|
16757
|
+
}
|
|
16758
|
+
}
|
|
16627
16759
|
if (names.length > 0) {
|
|
16628
16760
|
lines.push("", " Outputs");
|
|
16629
16761
|
const width = Math.max(...names.map((name) => name.length));
|
|
@@ -17052,9 +17184,7 @@ async function runManifestInit(parsed) {
|
|
|
17052
17184
|
const notes = [];
|
|
17053
17185
|
const key = parsed.flags["key"] ?? exampleResourceKey(name);
|
|
17054
17186
|
if (!key) {
|
|
17055
|
-
notes.push(
|
|
17056
|
-
"metadata.key \u2014 a stable identity, 3 to 63 characters, lower case. Required before apply"
|
|
17057
|
-
);
|
|
17187
|
+
notes.push(`metadata.key \u2014 a stable identity, required before apply. ${RESOURCE_KEY_RULE}`);
|
|
17058
17188
|
}
|
|
17059
17189
|
const chooseable = (product.dependencies ?? []).filter(
|
|
17060
17190
|
(dependency) => dependency.interface?.cardinality === "tenant-multiple"
|
|
@@ -17183,6 +17313,8 @@ Run \`vfac get product-instance pri_\u2026\` to see which ones this credential m
|
|
|
17183
17313
|
if (command === "delete" && !parsed.booleans.has("yes")) {
|
|
17184
17314
|
process.stderr.write(
|
|
17185
17315
|
`delete removes ${instanceId} and everything it is serving. This cannot be undone.
|
|
17316
|
+
If it was deployed from a manifest, its \`metadata.key\` stays claimed: a replacement
|
|
17317
|
+
needs a different key, and re-applying the same file will answer GONE.
|
|
17186
17318
|
Re-run with --yes to confirm: vfac lifecycle delete ${instanceId} --yes
|
|
17187
17319
|
Or suspend it instead, which stops it and keeps it: \`vfac lifecycle suspend\`.
|
|
17188
17320
|
`
|