@varde-flyt/vfac 0.2.0 → 0.3.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 +41 -2
  2. package/dist/vfac.mjs +88 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -61,14 +61,42 @@ vfac get product-instance <productInstanceId>
61
61
  ```
62
62
 
63
63
  Secrets are supplied per apply and never belong in the manifest — a manifest is a
64
- file you commit.
64
+ file you commit. `vfac plan` lists the ones an apply will still need.
65
+
66
+ ## Reading a setting from another resource
67
+
68
+ A setting may take its value from an output another resource publishes, instead
69
+ of a value you write:
70
+
71
+ ```yaml
72
+ configuration:
73
+ someSetting:
74
+ valueFrom:
75
+ resourceOutput:
76
+ resourceKey: the-other-resource
77
+ output: <one the other Product publishes>
78
+ ```
79
+
80
+ `resourceKey` is that resource's `metadata.key`, in the same Project, and it has
81
+ to be running and publishing the output before a manifest naming it is accepted —
82
+ the value is checked against the setting it fills at that moment. Reference
83
+ the value; never copy it — a copy is one the platform cannot see the origin of,
84
+ so nothing redeploys this resource when the other one changes and nothing stops
85
+ the other one being deleted out from under it.
86
+
87
+ An output the Product declares as a credential may only fill a credential
88
+ setting, and is never COPIED: the platform points the consuming service at the
89
+ producer's own stored secret and resolves that reference when it deploys the
90
+ service, exactly as it resolves the service's own credentials. So there is no
91
+ second copy to rotate, and no value for you to read — `vfac get product-instance`
92
+ shows one as `[secret]`. `vfac guide` is authoritative.
65
93
 
66
94
  ## Running it again
67
95
 
68
96
  **Send no secret.** A pipeline re-applies the same manifest on every push:
69
97
 
70
98
  ```sh
71
- vfac apply -f resource.yaml
99
+ vfac apply -f resource.yaml --wait
72
100
  ```
73
101
 
74
102
  With no secret to write, an apply that finds nothing changed answers `NO_CHANGE`.
@@ -76,6 +104,17 @@ That is the ordinary outcome for a pipeline, not a failure. It is not a health
76
104
  check either: `vfac` exits non-zero when a resource matches your manifest and is
77
105
  not actually serving.
78
106
 
107
+ **`--wait` is what makes a pipeline honest.** Deploys are asynchronous: without
108
+ it, `apply` exits 0 as soon as the platform has ACCEPTED the change, and the
109
+ deploy that fails a minute later fails behind a build that already went green.
110
+ With it, `apply` follows the operation and exits non-zero if it does not succeed.
111
+ It waits up to an hour; giving up is not a cancellation, and the operation keeps
112
+ running in the platform.
113
+
114
+ `vfac get operation` answers a different question and keeps its own contract: it
115
+ exits 0 whenever the read worked, even for an operation that failed, so a loop
116
+ can tell "the deploy failed" from "the read failed".
117
+
79
118
  Sending a secret is itself a change. The platform stores secrets write-only, so
80
119
  it has nothing to compare against and treats one it was handed as one to write —
81
120
  so this answers `UPDATE` and redeploys, even when the value is the value already
package/dist/vfac.mjs CHANGED
@@ -12729,6 +12729,10 @@ function reservedTenantSecretKeyViolation(secretKey) {
12729
12729
  }
12730
12730
  return null;
12731
12731
  }
12732
+ var SECRET_BINDING_SEPARATOR = "-b-";
12733
+ function secretKeyClaimsBinding(secretKey) {
12734
+ return new RegExp(`${SECRET_BINDING_SEPARATOR}[0-9a-z]{8}$`).test(secretKey);
12735
+ }
12732
12736
  var PLATFORM_REGISTRY_REF = "varde-product-registry";
12733
12737
  var MAX_IMAGE_REPOSITORY_LENGTH = 64;
12734
12738
  function imageRepository(productId, componentId) {
@@ -13117,11 +13121,22 @@ var ServiceProfileSchema = external_exports.object({
13117
13121
  /** Exactly one profile is the create-wizard default. */
13118
13122
  default: external_exports.boolean().optional()
13119
13123
  }).strict();
13124
+ var MAX_PRODUCT_OUTPUTS = 12;
13120
13125
  var ProductOutputSchema = external_exports.object({
13121
13126
  key: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, "Output keys are camelCase.").describe("Stable key. The Blueprint declares how this value is produced."),
13122
13127
  displayName: external_exports.string().min(2).max(60),
13123
13128
  description: external_exports.string().min(10).max(300),
13124
- type: external_exports.enum(["url", "identifier", "text"])
13129
+ type: external_exports.enum(["url", "identifier", "text"]),
13130
+ /**
13131
+ * Is this output a credential?
13132
+ *
13133
+ * `.optional()`, NEVER `.default()`. A default materialises the key,
13134
+ * `contractDigest` digests the PARSED object, and a republish of unchanged
13135
+ * bytes would then be refused as RELEASED_CONTRACT_MUTATED. Absent means
13136
+ * ordinary — the meaning every Definition published before this field
13137
+ * already has.
13138
+ */
13139
+ secret: external_exports.boolean().optional()
13125
13140
  }).strict();
13126
13141
  var ProductDependencySchema = external_exports.object({
13127
13142
  /**
@@ -13349,7 +13364,7 @@ var ProductDefinitionObjectSchema = external_exports.object({
13349
13364
  profiles: external_exports.array(ServiceProfileSchema).min(1).max(SERVICE_PROFILE_IDS.length),
13350
13365
  configuration: CustomerConfigurationSchema,
13351
13366
  // --- Results, relationships, operations -------------------------------
13352
- outputs: external_exports.array(ProductOutputSchema).min(1).max(12),
13367
+ outputs: external_exports.array(ProductOutputSchema).min(1).max(MAX_PRODUCT_OUTPUTS),
13353
13368
  dependencies: external_exports.array(ProductDependencySchema).max(8),
13354
13369
  /**
13355
13370
  * The service interfaces this Product provides to other Products.
@@ -14389,7 +14404,18 @@ var BlueprintOutputSchema = external_exports.object({
14389
14404
  pathSuffix: external_exports.string().regex(/^\/[A-Za-z0-9\-._~/]*$/).max(120).optional()
14390
14405
  }).strict(),
14391
14406
  external_exports.object({ kind: external_exports.literal("instance-identifier") }).strict(),
14392
- external_exports.object({ kind: external_exports.literal("literal"), value: external_exports.string().max(300) }).strict()
14407
+ external_exports.object({ kind: external_exports.literal("literal"), value: external_exports.string().max(300) }).strict(),
14408
+ external_exports.object({
14409
+ kind: external_exports.literal("secret"),
14410
+ /**
14411
+ * A key this Blueprint's own `secrets[]` declares. NOT a free-text
14412
+ * Secret Manager path: an output that could name any location would
14413
+ * be a way to read a secret the Product does not own, and the whole
14414
+ * point of resolving through the declaration is that the platform
14415
+ * composes the path from the instance's own identity.
14416
+ */
14417
+ secretKey: external_exports.string().regex(/^[a-z][a-z0-9-]{1,46}[a-z0-9]$/)
14418
+ }).strict()
14393
14419
  ])
14394
14420
  }).strict();
14395
14421
  var BlueprintLifecycleSchema = external_exports.object({
@@ -15177,6 +15203,15 @@ var DeploymentBlueprintSchema = DeploymentBlueprintObjectSchema.superRefine((blu
15177
15203
  });
15178
15204
  }
15179
15205
  });
15206
+ blueprint.secrets.forEach((secret, index) => {
15207
+ if (!secretKeyClaimsBinding(secret.key))
15208
+ return;
15209
+ ctx.addIssue({
15210
+ code: external_exports.ZodIssueCode.custom,
15211
+ path: ["secrets", index, "key"],
15212
+ message: `Secret key "${secret.key}" ends the way the platform marks a rotated secret. Choose a key that does not end in "-b-" followed by eight lowercase letters or digits.`
15213
+ });
15214
+ });
15180
15215
  const outputKeys = blueprint.outputs.map((output) => output.key);
15181
15216
  const duplicateOutputs = outputKeys.filter((key, i) => outputKeys.indexOf(key) !== i);
15182
15217
  if (duplicateOutputs.length > 0) {
@@ -15186,6 +15221,27 @@ var DeploymentBlueprintSchema = DeploymentBlueprintObjectSchema.superRefine((blu
15186
15221
  message: `Duplicate output keys: ${[...new Set(duplicateOutputs)].join(", ")}.`
15187
15222
  });
15188
15223
  }
15224
+ blueprint.outputs.forEach((output, index) => {
15225
+ const { source } = output;
15226
+ if (source.kind !== "secret")
15227
+ return;
15228
+ const wiring = blueprint.secrets.find((secret) => secret.key === source.secretKey);
15229
+ if (wiring === void 0) {
15230
+ ctx.addIssue({
15231
+ code: external_exports.ZodIssueCode.custom,
15232
+ path: ["outputs", index, "source", "secretKey"],
15233
+ message: `Output "${output.key}" names secret "${source.secretKey}", which this Blueprint does not declare.`
15234
+ });
15235
+ return;
15236
+ }
15237
+ if (wiring.scope !== "product-instance") {
15238
+ ctx.addIssue({
15239
+ code: external_exports.ZodIssueCode.custom,
15240
+ path: ["outputs", index, "source", "secretKey"],
15241
+ message: `Output "${output.key}" publishes secret "${wiring.key}", which is ${wiring.scope}-scoped. A published secret must belong to the instance that publishes it, so its scope must be product-instance.`
15242
+ });
15243
+ }
15244
+ });
15189
15245
  blueprint.outputs.forEach((output, index) => {
15190
15246
  if (output.source.kind !== "component-public-endpoint")
15191
15247
  return;
@@ -15324,6 +15380,18 @@ function isTerminalAction(action) {
15324
15380
  }
15325
15381
  var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
15326
15382
 
15383
+ // ../product-contracts/dist/resource-output-reference.js
15384
+ var ResourceOutputReferenceSchema = external_exports.object({
15385
+ valueFrom: external_exports.object({
15386
+ resourceOutput: external_exports.object({
15387
+ /** 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."),
15389
+ /** The key of an output that Resource's Product declares. */
15390
+ output: external_exports.string().regex(/^[a-z][a-zA-Z0-9]{1,47}$/, "Output keys are camelCase.")
15391
+ }).strict()
15392
+ }).strict()
15393
+ }).strict();
15394
+
15327
15395
  // ../product-contracts/dist/product-instance-template.schema.js
15328
15396
  var ProductInstanceTemplateSchema = external_exports.object({
15329
15397
  apiVersion: ResourceApiVersionSchema,
@@ -15513,6 +15581,17 @@ function printPlan(plan, json) {
15513
15581
  lines.push(` ~ ${change.field}`);
15514
15582
  lines.push(` ${change.from} \u2192 ${change.to}`);
15515
15583
  }
15584
+ for (const reference of plan.references ?? []) {
15585
+ lines.push(` ${reference.property}`);
15586
+ lines.push(
15587
+ ` from ${reference.resourceKey}.${reference.output}${reference.secret ? " [secret]" : ""}`
15588
+ );
15589
+ }
15590
+ const required = plan.requiredSecrets ?? [];
15591
+ if (required.length > 0) {
15592
+ lines.push(" required secrets:");
15593
+ for (const name of required) lines.push(` ${name}`);
15594
+ }
15516
15595
  if ((plan.secretsToWrite ?? []).length > 0) {
15517
15596
  lines.push(` secrets to set: ${plan.secretsToWrite.join(", ")}`);
15518
15597
  for (const line of secretWriteConsequence(plan)) lines.push(` ${line}`);
@@ -16078,12 +16157,16 @@ async function runGet(parsed) {
16078
16157
  if (typeof key === "string") lines.push(` key ${key}`);
16079
16158
  const outputs = resource["outputs"] ?? {};
16080
16159
  const declared = Array.isArray(resource["outputDeclarations"]) ? resource["outputDeclarations"] : [];
16081
- const names = Object.keys(outputs).sort();
16160
+ const declaredNames = declared.map((entry) => entry["key"]).filter((key2) => typeof key2 === "string");
16161
+ const names = [.../* @__PURE__ */ new Set([...Object.keys(outputs), ...declaredNames])].sort();
16162
+ const isSecret = (name) => declared.find((entry) => entry["key"] === name)?.["secret"] === true;
16082
16163
  if (names.length > 0) {
16083
16164
  lines.push("", " Outputs");
16084
16165
  const width = Math.max(...names.map((name) => name.length));
16085
16166
  for (const name of names) {
16086
- lines.push(` ${name.padEnd(width)} ${String(outputs[name] ?? "")}`);
16167
+ lines.push(
16168
+ ` ${name.padEnd(width)} ${isSecret(name) ? "[secret]" : String(outputs[name] ?? "")}`
16169
+ );
16087
16170
  const meaning = declared.find((entry) => entry["key"] === name);
16088
16171
  const description = meaning?.["description"];
16089
16172
  if (typeof description === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varde-flyt/vfac",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Deploy and manage Varde Flyt Products from a manifest.",
5
5
  "repository": {
6
6
  "type": "git",