@varde-flyt/vfac 0.3.1 → 0.4.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 +35 -0
- package/dist/vfac.mjs +137 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,6 +91,41 @@ service, exactly as it resolves the service's own credentials. So there is no
|
|
|
91
91
|
second copy to rotate, and no value for you to read — `vfac get product-instance`
|
|
92
92
|
shows one as `[secret]`. `vfac guide` is authoritative.
|
|
93
93
|
|
|
94
|
+
## Connecting to another resource
|
|
95
|
+
|
|
96
|
+
A Product may declare a **service dependency** — a connection to another service
|
|
97
|
+
that signs your people in, or that it looks a directory up in. When the platform
|
|
98
|
+
says the customer chooses which resource provides it, name that resource:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
spec:
|
|
102
|
+
dependencyBindings:
|
|
103
|
+
<dependency-key>:
|
|
104
|
+
resourceKey: the-providing-resource
|
|
105
|
+
projectId: prj_… # only if it is in another Project
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`vfac manifest init` names the exact property for every dependency you have to
|
|
109
|
+
choose, and `vfac plan` prints the connections an apply would establish before
|
|
110
|
+
you approve it. The dependency key comes from the Product, not from you —
|
|
111
|
+
`vfac product show <productId>` lists them.
|
|
112
|
+
|
|
113
|
+
This is not the same thing as the section above, and the difference is worth
|
|
114
|
+
holding on to:
|
|
115
|
+
|
|
116
|
+
| | |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `valueFrom.resourceOutput` | read a value another resource publishes |
|
|
119
|
+
| `dependencyBindings` | use that resource as the PROVIDER for a declared dependency |
|
|
120
|
+
|
|
121
|
+
A connection is established once. **A manifest can make one and cannot move
|
|
122
|
+
one**: pointing an existing connection at a different resource is refused rather
|
|
123
|
+
than performed, because a file you re-apply on every push must not be able to
|
|
124
|
+
silently move a running service onto a different identity provider. The
|
|
125
|
+
credential the connection delivers never appears in your manifest, in a plan, or
|
|
126
|
+
in any `vfac` output — the providing service issues it and the platform stores
|
|
127
|
+
it.
|
|
128
|
+
|
|
94
129
|
## Running it again
|
|
95
130
|
|
|
96
131
|
**Send no secret.** A pipeline re-applies the same manifest on every push:
|
package/dist/vfac.mjs
CHANGED
|
@@ -12673,6 +12673,58 @@ function matches(pattern, value) {
|
|
|
12673
12673
|
}
|
|
12674
12674
|
}
|
|
12675
12675
|
|
|
12676
|
+
// ../product-configuration/dist/resource-api-version.js
|
|
12677
|
+
var RESOURCE_API_VERSION = "resources.vardeflyt.no/v1";
|
|
12678
|
+
var PRODUCT_INSTANCE_KIND = "ProductInstance";
|
|
12679
|
+
|
|
12680
|
+
// ../product-configuration/dist/product-instance-example.js
|
|
12681
|
+
function isSecret(declaration) {
|
|
12682
|
+
return declaration.writeOnly === true || declaration["x-secret"] === true;
|
|
12683
|
+
}
|
|
12684
|
+
function seedConfiguration(configuration) {
|
|
12685
|
+
const properties = configuration?.properties ?? {};
|
|
12686
|
+
const required = new Set(configuration?.required ?? []);
|
|
12687
|
+
const values = {};
|
|
12688
|
+
const open = [];
|
|
12689
|
+
const requiredSecrets = [];
|
|
12690
|
+
for (const [name, declaration] of Object.entries(properties)) {
|
|
12691
|
+
if (isSecret(declaration)) {
|
|
12692
|
+
if (required.has(name))
|
|
12693
|
+
requiredSecrets.push(name);
|
|
12694
|
+
continue;
|
|
12695
|
+
}
|
|
12696
|
+
if (declaration.default !== void 0) {
|
|
12697
|
+
values[name] = declaration.default;
|
|
12698
|
+
continue;
|
|
12699
|
+
}
|
|
12700
|
+
if (required.has(name)) {
|
|
12701
|
+
values[name] = "";
|
|
12702
|
+
open.push({ name, type: declaration.type ?? null });
|
|
12703
|
+
}
|
|
12704
|
+
}
|
|
12705
|
+
return { values, open, requiredSecrets };
|
|
12706
|
+
}
|
|
12707
|
+
function productInstanceExample(input) {
|
|
12708
|
+
return {
|
|
12709
|
+
apiVersion: RESOURCE_API_VERSION,
|
|
12710
|
+
kind: PRODUCT_INSTANCE_KIND,
|
|
12711
|
+
// The key is spread rather than set, so an underivable one is ABSENT from
|
|
12712
|
+
// the document instead of present and empty.
|
|
12713
|
+
metadata: { name: input.name, ...input.key ? { key: input.key } : {} },
|
|
12714
|
+
spec: {
|
|
12715
|
+
productId: input.productId,
|
|
12716
|
+
productVersion: input.productVersion,
|
|
12717
|
+
region: input.region,
|
|
12718
|
+
profile: input.profile,
|
|
12719
|
+
configuration: input.configuration
|
|
12720
|
+
}
|
|
12721
|
+
};
|
|
12722
|
+
}
|
|
12723
|
+
function exampleResourceKey(name) {
|
|
12724
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "").slice(0, 63);
|
|
12725
|
+
return /^[a-z][a-z0-9-]{1,61}[a-z0-9]$/.test(slug) ? slug : "";
|
|
12726
|
+
}
|
|
12727
|
+
|
|
12676
12728
|
// ../platform-schemas/dist/index.js
|
|
12677
12729
|
var TENANT_KEY_REGEX = /^[a-z][a-z0-9-]{2,23}[a-z0-9]$/;
|
|
12678
12730
|
var RESERVED_TENANT_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -12763,10 +12815,8 @@ var OperationStatusSchema = external_exports.enum([
|
|
|
12763
12815
|
|
|
12764
12816
|
// ../product-contracts/dist/api-version.js
|
|
12765
12817
|
var PRODUCT_API_VERSION = "products.vardeflyt.no/v1";
|
|
12766
|
-
var RESOURCE_API_VERSION = "resources.vardeflyt.no/v1";
|
|
12767
12818
|
var PRODUCT_DEFINITION_KIND = "ProductDefinition";
|
|
12768
12819
|
var DEPLOYMENT_BLUEPRINT_KIND = "DeploymentBlueprint";
|
|
12769
|
-
var PRODUCT_INSTANCE_KIND = "ProductInstance";
|
|
12770
12820
|
var ProductApiVersionSchema = external_exports.literal(PRODUCT_API_VERSION, {
|
|
12771
12821
|
errorMap: () => ({
|
|
12772
12822
|
message: `apiVersion must be "${PRODUCT_API_VERSION}". An unrecognized contract format is refused rather than ignored.`
|
|
@@ -15380,18 +15430,43 @@ function isTerminalAction(action) {
|
|
|
15380
15430
|
}
|
|
15381
15431
|
var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
|
|
15382
15432
|
|
|
15433
|
+
// ../product-contracts/dist/resource-key.js
|
|
15434
|
+
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.");
|
|
15435
|
+
|
|
15383
15436
|
// ../product-contracts/dist/resource-output-reference.js
|
|
15384
15437
|
var ResourceOutputReferenceSchema = external_exports.object({
|
|
15385
15438
|
valueFrom: external_exports.object({
|
|
15386
15439
|
resourceOutput: external_exports.object({
|
|
15387
15440
|
/** The producing Resource's `metadata.key`, in the same Project. */
|
|
15388
|
-
resourceKey:
|
|
15441
|
+
resourceKey: ResourceKeySchema,
|
|
15389
15442
|
/** The key of an output that Resource's Product declares. */
|
|
15390
15443
|
output: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, "Output keys are camelCase.")
|
|
15391
15444
|
}).strict()
|
|
15392
15445
|
}).strict()
|
|
15393
15446
|
}).strict();
|
|
15394
15447
|
|
|
15448
|
+
// ../product-contracts/dist/dependency-binding.js
|
|
15449
|
+
var DependencyBindingSelectorSchema = external_exports.object({
|
|
15450
|
+
/** The providing Resource's `metadata.key`. */
|
|
15451
|
+
resourceKey: ResourceKeySchema,
|
|
15452
|
+
/**
|
|
15453
|
+
* The Project that Resource lives in. Absent means the consumer's own.
|
|
15454
|
+
*
|
|
15455
|
+
* PRESENT BECAUSE A BINDING IS TENANT-SCOPED AND A PROJECT IS NOT A RUNTIME
|
|
15456
|
+
* BOUNDARY: a provider anywhere in the Tenant can satisfy a consumer
|
|
15457
|
+
* anywhere in it, and that has been true since bindings existed. What the
|
|
15458
|
+
* field does NOT do is grant anything — the Control Plane opens that
|
|
15459
|
+
* Project from the caller's own assertion, and a Project the caller holds
|
|
15460
|
+
* nothing in answers exactly as a Project that does not exist.
|
|
15461
|
+
*
|
|
15462
|
+
* A plain bounded string, exactly as `spec.projectId` is: an id that does
|
|
15463
|
+
* not name a readable Project is refused where the refusal can be phrased
|
|
15464
|
+
* without leaking, which is not here.
|
|
15465
|
+
*/
|
|
15466
|
+
projectId: external_exports.string().min(1).max(64).optional()
|
|
15467
|
+
}).strict();
|
|
15468
|
+
var DependencyBindingsSchema = external_exports.record(ServiceBindingKeySchema, DependencyBindingSelectorSchema);
|
|
15469
|
+
|
|
15395
15470
|
// ../product-contracts/dist/product-instance-template.schema.js
|
|
15396
15471
|
var ProductInstanceTemplateSchema = external_exports.object({
|
|
15397
15472
|
apiVersion: ResourceApiVersionSchema,
|
|
@@ -15452,7 +15527,7 @@ var ProductInstanceTemplateSchema = external_exports.object({
|
|
|
15452
15527
|
* Shaped like a DNS label: it becomes a database predicate and a log
|
|
15453
15528
|
* field, and neither wants free text.
|
|
15454
15529
|
*/
|
|
15455
|
-
key:
|
|
15530
|
+
key: ResourceKeySchema.optional()
|
|
15456
15531
|
}).strict(),
|
|
15457
15532
|
spec: external_exports.object({
|
|
15458
15533
|
productId: external_exports.string().min(1).max(64),
|
|
@@ -15481,7 +15556,27 @@ var ProductInstanceTemplateSchema = external_exports.object({
|
|
|
15481
15556
|
* configuration schema by `resolveTemplate`, which is where the real
|
|
15482
15557
|
* shape lives — it differs per Product, so it cannot be stated here.
|
|
15483
15558
|
*/
|
|
15484
|
-
configuration: external_exports.record(external_exports.unknown()).optional()
|
|
15559
|
+
configuration: external_exports.record(external_exports.unknown()).optional(),
|
|
15560
|
+
/**
|
|
15561
|
+
* Which Resource satisfies each declared service dependency.
|
|
15562
|
+
*
|
|
15563
|
+
* A RESOURCE RELATION, NOT A SETTING, which is why it is a sibling of
|
|
15564
|
+
* `configuration` rather than a key inside it. `configuration` is the
|
|
15565
|
+
* Product's own settings and is validated against the Definition's
|
|
15566
|
+
* schema; this names another Resource, and the create envelope made the
|
|
15567
|
+
* identical split for the identical reason.
|
|
15568
|
+
*
|
|
15569
|
+
* OPTIONAL, PERMANENTLY. Every manifest written before this field
|
|
15570
|
+
* existed omits it, and absence has to keep meaning "leave the
|
|
15571
|
+
* connections alone" — otherwise re-applying an unchanged file would
|
|
15572
|
+
* release a binding somebody made in the portal.
|
|
15573
|
+
*
|
|
15574
|
+
* The keys the platform will ACCEPT here are narrower than the shape:
|
|
15575
|
+
* only a dependency whose interface the registry calls
|
|
15576
|
+
* `tenant-multiple` is one a customer chooses. `resolveTemplate` says
|
|
15577
|
+
* so against the pinned Definition, so the refusal names the field.
|
|
15578
|
+
*/
|
|
15579
|
+
dependencyBindings: DependencyBindingsSchema.optional()
|
|
15485
15580
|
}).strict()
|
|
15486
15581
|
}).strict();
|
|
15487
15582
|
|
|
@@ -15587,6 +15682,21 @@ function printPlan(plan, json) {
|
|
|
15587
15682
|
` from ${reference.resourceKey}.${reference.output}${reference.secret ? " [secret]" : ""}`
|
|
15588
15683
|
);
|
|
15589
15684
|
}
|
|
15685
|
+
const bindings = plan.dependencyBindings ?? [];
|
|
15686
|
+
if (bindings.length > 0) {
|
|
15687
|
+
lines.push(" dependency bindings:");
|
|
15688
|
+
for (const binding of bindings) {
|
|
15689
|
+
lines.push(` ${binding.dependencyKey}`);
|
|
15690
|
+
lines.push(
|
|
15691
|
+
` resource: ${binding.resourceKey}${binding.projectId ? ` (in ${binding.projectId})` : ""}`
|
|
15692
|
+
);
|
|
15693
|
+
if (binding.interfaceId !== void 0) {
|
|
15694
|
+
lines.push(
|
|
15695
|
+
` interface: ${binding.interfaceId}${binding.interfaceVersion ? `/${binding.interfaceVersion}` : ""}`
|
|
15696
|
+
);
|
|
15697
|
+
}
|
|
15698
|
+
}
|
|
15699
|
+
}
|
|
15590
15700
|
const required = plan.requiredSecrets ?? [];
|
|
15591
15701
|
if (required.length > 0) {
|
|
15592
15702
|
lines.push(" required secrets:");
|
|
@@ -16170,13 +16280,13 @@ async function runGet(parsed) {
|
|
|
16170
16280
|
const declared = Array.isArray(resource["outputDeclarations"]) ? resource["outputDeclarations"] : [];
|
|
16171
16281
|
const declaredNames = declared.map((entry) => entry["key"]).filter((key2) => typeof key2 === "string");
|
|
16172
16282
|
const names = [.../* @__PURE__ */ new Set([...Object.keys(outputs), ...declaredNames])].sort();
|
|
16173
|
-
const
|
|
16283
|
+
const isSecret2 = (name) => declared.find((entry) => entry["key"] === name)?.["secret"] === true;
|
|
16174
16284
|
if (names.length > 0) {
|
|
16175
16285
|
lines.push("", " Outputs");
|
|
16176
16286
|
const width = Math.max(...names.map((name) => name.length));
|
|
16177
16287
|
for (const name of names) {
|
|
16178
16288
|
lines.push(
|
|
16179
|
-
` ${name.padEnd(width)} ${
|
|
16289
|
+
` ${name.padEnd(width)} ${isSecret2(name) ? "[secret]" : String(outputs[name] ?? "")}`
|
|
16180
16290
|
);
|
|
16181
16291
|
const meaning = declared.find((entry) => entry["key"] === name);
|
|
16182
16292
|
const description = meaning?.["description"];
|
|
@@ -16597,7 +16707,7 @@ async function runManifestInit(parsed) {
|
|
|
16597
16707
|
if (!result.ok) return printError(result.error, parsed.flags["output"] === "json");
|
|
16598
16708
|
const product = result.data;
|
|
16599
16709
|
const notes = [];
|
|
16600
|
-
const key = parsed.flags["key"] ??
|
|
16710
|
+
const key = parsed.flags["key"] ?? exampleResourceKey(name);
|
|
16601
16711
|
if (!key) {
|
|
16602
16712
|
notes.push(
|
|
16603
16713
|
"metadata.key \u2014 a stable identity, 3 to 63 characters, lower case. Required before apply"
|
|
@@ -16606,23 +16716,10 @@ async function runManifestInit(parsed) {
|
|
|
16606
16716
|
const chooseable = (product.dependencies ?? []).filter(
|
|
16607
16717
|
(dependency) => dependency.interface?.cardinality === "tenant-multiple"
|
|
16608
16718
|
);
|
|
16609
|
-
const mustChoose = chooseable.filter((dependency) => dependency.required === true);
|
|
16610
|
-
if (mustChoose.length > 0) {
|
|
16611
|
-
process.stderr.write(
|
|
16612
|
-
`${product.productId} requires a customer-selected service dependency that
|
|
16613
|
-
Product Deployment as Code v1 cannot express yet. Deploy it from the portal.
|
|
16614
|
-
`
|
|
16615
|
-
);
|
|
16616
|
-
for (const dependency of mustChoose) {
|
|
16617
|
-
process.stderr.write(` ${dependency.interface?.id ?? "a service"}`);
|
|
16618
|
-
process.stderr.write(dependency.reason ? ` \u2014 ${dependency.reason}
|
|
16619
|
-
` : "\n");
|
|
16620
|
-
}
|
|
16621
|
-
return 1;
|
|
16622
|
-
}
|
|
16623
16719
|
for (const dependency of chooseable) {
|
|
16720
|
+
const property = dependency.key === void 0 ? "spec.dependencyBindings.<key>.resourceKey" : `spec.dependencyBindings.${dependency.key}.resourceKey`;
|
|
16624
16721
|
notes.push(
|
|
16625
|
-
`${dependency.
|
|
16722
|
+
`${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`
|
|
16626
16723
|
);
|
|
16627
16724
|
}
|
|
16628
16725
|
const regions = product.supportedRegions ?? [];
|
|
@@ -16663,43 +16760,24 @@ Product Deployment as Code v1 cannot express yet. Deploy it from the portal.
|
|
|
16663
16760
|
offered.length === 0 ? `${product.productId} ${product.version} declares no service profile. Ask the publisher.` : `This Product offers several service profiles. Pass --profile <${offered.join("|")}>.`
|
|
16664
16761
|
);
|
|
16665
16762
|
}
|
|
16666
|
-
const
|
|
16667
|
-
const
|
|
16668
|
-
|
|
16669
|
-
for (const [property, declaration] of Object.entries(properties)) {
|
|
16670
|
-
const secret = declaration.writeOnly === true || declaration["x-secret"] === true;
|
|
16671
|
-
if (secret) {
|
|
16672
|
-
if (required.includes(property)) {
|
|
16673
|
-
notes.push(`--secret ${property}=env:VAR \u2014 required, and never written to this file`);
|
|
16674
|
-
}
|
|
16675
|
-
continue;
|
|
16676
|
-
}
|
|
16677
|
-
if (declaration.default !== void 0) {
|
|
16678
|
-
configuration[property] = declaration.default;
|
|
16679
|
-
continue;
|
|
16680
|
-
}
|
|
16681
|
-
if (required.includes(property)) {
|
|
16682
|
-
configuration[property] = "";
|
|
16683
|
-
notes.push(`spec.configuration.${property} \u2014 required${typeOf(declaration)}`);
|
|
16684
|
-
}
|
|
16763
|
+
const seed = seedConfiguration(product.configuration);
|
|
16764
|
+
for (const property of seed.requiredSecrets) {
|
|
16765
|
+
notes.push(`--secret ${property}=env:VAR \u2014 required, and never written to this file`);
|
|
16685
16766
|
}
|
|
16686
|
-
const
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
configuration
|
|
16701
|
-
}
|
|
16702
|
-
};
|
|
16767
|
+
for (const setting of seed.open) {
|
|
16768
|
+
notes.push(
|
|
16769
|
+
`spec.configuration.${setting.name} \u2014 required${setting.type ? ` (${setting.type})` : ""}`
|
|
16770
|
+
);
|
|
16771
|
+
}
|
|
16772
|
+
const document = productInstanceExample({
|
|
16773
|
+
name,
|
|
16774
|
+
...key ? { key } : {},
|
|
16775
|
+
productId: product.productId,
|
|
16776
|
+
productVersion: product.version,
|
|
16777
|
+
region,
|
|
16778
|
+
profile,
|
|
16779
|
+
configuration: seed.values
|
|
16780
|
+
});
|
|
16703
16781
|
const checked = ProductInstanceTemplateSchema.safeParse(document);
|
|
16704
16782
|
if (!checked.success) {
|
|
16705
16783
|
const where = checked.error.issues.map((issue) => ` ${issue.path.join(".") || "(document)"}: ${issue.message}`).join("\n");
|
|
@@ -16723,13 +16801,6 @@ function refuse(message) {
|
|
|
16723
16801
|
`);
|
|
16724
16802
|
return 1;
|
|
16725
16803
|
}
|
|
16726
|
-
function typeOf(declaration) {
|
|
16727
|
-
return declaration.type ? ` (${declaration.type})` : "";
|
|
16728
|
-
}
|
|
16729
|
-
function keyFrom(name) {
|
|
16730
|
-
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "").slice(0, 63);
|
|
16731
|
-
return /^[a-z][a-z0-9-]{1,61}[a-z0-9]$/.test(slug) ? slug : "";
|
|
16732
|
-
}
|
|
16733
16804
|
|
|
16734
16805
|
// src/commands/guide.ts
|
|
16735
16806
|
async function runGuide(parsed) {
|