@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.
Files changed (3) hide show
  1. package/README.md +35 -0
  2. package/dist/vfac.mjs +137 -66
  3. 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: 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."),
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: 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.").optional()
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 isSecret = (name) => declared.find((entry) => entry["key"] === name)?.["secret"] === true;
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)} ${isSecret(name) ? "[secret]" : String(outputs[name] ?? "")}`
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"] ?? keyFrom(name);
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.interface?.id ?? "a service connection"} \u2014 optional here, but this Product may require it depending on your settings. A manifest cannot supply one yet`
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 properties = product.configuration?.properties ?? {};
16667
- const required = product.configuration?.required ?? [];
16668
- const configuration = {};
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 document = {
16687
- apiVersion: "resources.vardeflyt.no/v1",
16688
- kind: "ProductInstance",
16689
- // The key is spread rather than set, so an underivable one is ABSENT from
16690
- // the document instead of present and empty.
16691
- metadata: { name, ...key ? { key } : {} },
16692
- spec: {
16693
- productId: product.productId,
16694
- // PINNED, ALWAYS. The contract requires a version, and a document that
16695
- // tracked "newest" would redeploy a customer's resource because somebody
16696
- // else published.
16697
- productVersion: product.version,
16698
- region,
16699
- profile,
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varde-flyt/vfac",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Deploy and manage Varde Flyt Products from a manifest.",
5
5
  "repository": {
6
6
  "type": "git",