@varde-flyt/vfac 0.2.0-rc.1 → 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.
- package/README.md +41 -2
- package/dist/vfac.mjs +88 -5
- 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(
|
|
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
|
|
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(
|
|
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") {
|