@varde-flyt/vfac 0.13.0 → 0.14.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 +17 -3
- package/dist/vfac.mjs +556 -318
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,8 +80,18 @@ vfac get operation <operationId>
|
|
|
80
80
|
vfac get product-instance <productInstanceId>
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
Lost the operation id — a terminal that closed, a CI log nobody kept? The
|
|
84
|
+
operations of a resource are listed newest first, one page at a time; the
|
|
85
|
+
`--cursor` for older ones is printed, and is `nextCursor` in JSON:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
vfac get operations --resource <productInstanceId>
|
|
89
|
+
```
|
|
90
|
+
|
|
83
91
|
Secrets are supplied per apply and never belong in the manifest — a manifest is a
|
|
84
|
-
file you commit.
|
|
92
|
+
file you commit. A value for a setting the Product declares secret is refused
|
|
93
|
+
at every door by the platform, and by `vfac` before the file is sent when the
|
|
94
|
+
file pins the release the catalog currently offers. **Plan the run that sets one with the same `--secret` inputs the
|
|
85
95
|
apply will carry**: `plan` takes the flag exactly as `apply` does, and without it
|
|
86
96
|
the plan describes a different request.
|
|
87
97
|
|
|
@@ -182,7 +192,9 @@ spec:
|
|
|
182
192
|
|
|
183
193
|
`vfac manifest init` names the exact property for every dependency you have to
|
|
184
194
|
choose, and `vfac plan` prints the connections an apply would establish before
|
|
185
|
-
you approve it.
|
|
195
|
+
you approve it. A resource in this Project that cannot provide the connection is
|
|
196
|
+
refused on `spec.dependencyBindings.<dependency-key>`, saying why; one named in
|
|
197
|
+
another Project answers only that it could not be used. The dependency key comes from the Product, not from you —
|
|
186
198
|
`vfac product show <productId>` lists every dependency with its key, its
|
|
187
199
|
capability, and whether a setting is what turns it on.
|
|
188
200
|
|
|
@@ -214,6 +226,7 @@ vfac product show <productId> --output json
|
|
|
214
226
|
vfac plan -f resource.yaml --output json
|
|
215
227
|
vfac get product-instance <productInstanceId> --output json
|
|
216
228
|
vfac get product-instances --output json
|
|
229
|
+
vfac get operations --resource <productInstanceId> --output json
|
|
217
230
|
```
|
|
218
231
|
|
|
219
232
|
**Decide from the action, not from the exit code.** A plan that REFUSES still
|
|
@@ -235,8 +248,9 @@ The envelopes are stable at the top level:
|
|
|
235
248
|
|
|
236
249
|
| Command | Answers with |
|
|
237
250
|
| --- | --- |
|
|
238
|
-
| `vfac get product-instance <id> --output json` | `resource` and `lifecycle` — `status`, `outputs` and `conditions` are under `resource` |
|
|
251
|
+
| `vfac get product-instance <id> --output json` | `resource` and `lifecycle` — `status`, `outputs` and `conditions` are under `resource`; each condition carries `state` and `freshness` (`FRESH`, or `STALE` when not re-confirmed recently), and `lifecycle.refused[]` carries a `reason` |
|
|
239
252
|
| `vfac get product-instances --output json` | `productInstances` and `nextCursor` |
|
|
253
|
+
| `vfac get operations --resource <productInstanceId> --output json` | `operations` and `nextCursor` — one page, newest first, and `nextCursor` reads older ones; each line is `operationId`, `type`, `status`, `phase` and its timestamps |
|
|
240
254
|
|
|
241
255
|
`vfac guide` is authoritative for what is inside them.
|
|
242
256
|
|
package/dist/vfac.mjs
CHANGED
|
@@ -7378,6 +7378,8 @@ var VALUED = /* @__PURE__ */ new Set([
|
|
|
7378
7378
|
// an organization and part of a catalogue, silently, once either passed 25.
|
|
7379
7379
|
"cursor",
|
|
7380
7380
|
"page-size",
|
|
7381
|
+
// `get operations --resource pri_…`: the history of one resource.
|
|
7382
|
+
"resource",
|
|
7381
7383
|
"product",
|
|
7382
7384
|
"name",
|
|
7383
7385
|
"key",
|
|
@@ -7700,283 +7702,7 @@ async function probeMachineApi(args) {
|
|
|
7700
7702
|
}
|
|
7701
7703
|
|
|
7702
7704
|
// src/manifest.ts
|
|
7703
|
-
var import_yaml = __toESM(require_dist(), 1);
|
|
7704
7705
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
7705
|
-
function readManifest(path) {
|
|
7706
|
-
let text;
|
|
7707
|
-
try {
|
|
7708
|
-
text = readFileSync2(path, "utf8");
|
|
7709
|
-
} catch {
|
|
7710
|
-
return { ok: false, message: `Could not read ${path}.` };
|
|
7711
|
-
}
|
|
7712
|
-
let parsed;
|
|
7713
|
-
try {
|
|
7714
|
-
parsed = (0, import_yaml.parse)(text);
|
|
7715
|
-
} catch (error) {
|
|
7716
|
-
return {
|
|
7717
|
-
ok: false,
|
|
7718
|
-
message: `${path} is not valid YAML or JSON: ${error instanceof Error ? error.message : "unknown error"}`
|
|
7719
|
-
};
|
|
7720
|
-
}
|
|
7721
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
7722
|
-
return { ok: false, message: `${path} does not contain a manifest document.` };
|
|
7723
|
-
}
|
|
7724
|
-
return { ok: true, document: parsed };
|
|
7725
|
-
}
|
|
7726
|
-
function resolveSecrets(pairs, environment = process.env) {
|
|
7727
|
-
const secrets = {};
|
|
7728
|
-
for (const pair of pairs) {
|
|
7729
|
-
const separator = pair.indexOf("=");
|
|
7730
|
-
if (separator <= 0) {
|
|
7731
|
-
return { ok: false, message: `--secret expects name=env:VARIABLE, got "${pair}".` };
|
|
7732
|
-
}
|
|
7733
|
-
const name = pair.slice(0, separator);
|
|
7734
|
-
const source = pair.slice(separator + 1);
|
|
7735
|
-
if (!source.startsWith("env:")) {
|
|
7736
|
-
return {
|
|
7737
|
-
ok: false,
|
|
7738
|
-
message: `--secret ${name} must read from the environment: --secret ${name}=env:VARIABLE. A value on the command line is in your shell history and in the process table.`
|
|
7739
|
-
};
|
|
7740
|
-
}
|
|
7741
|
-
const variable = source.slice("env:".length);
|
|
7742
|
-
if (variable.length === 0) {
|
|
7743
|
-
return { ok: false, message: `--secret ${name}=env: names no variable.` };
|
|
7744
|
-
}
|
|
7745
|
-
const value = environment[variable];
|
|
7746
|
-
if (value === void 0 || value === "") {
|
|
7747
|
-
return { ok: false, message: `${variable} is not set, so ${name} has no value.` };
|
|
7748
|
-
}
|
|
7749
|
-
secrets[name] = value;
|
|
7750
|
-
}
|
|
7751
|
-
return { ok: true, secrets };
|
|
7752
|
-
}
|
|
7753
|
-
function manifestKeyOf(document) {
|
|
7754
|
-
const metadata = document["metadata"];
|
|
7755
|
-
if (typeof metadata !== "object" || metadata === null) return null;
|
|
7756
|
-
const key = metadata["key"];
|
|
7757
|
-
return typeof key === "string" ? key : null;
|
|
7758
|
-
}
|
|
7759
|
-
|
|
7760
|
-
// src/endpoint.ts
|
|
7761
|
-
function isLoopback(hostname) {
|
|
7762
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
7763
|
-
}
|
|
7764
|
-
function validateEndpoint(raw, source) {
|
|
7765
|
-
let url;
|
|
7766
|
-
try {
|
|
7767
|
-
url = new URL(raw);
|
|
7768
|
-
} catch {
|
|
7769
|
-
return { ok: false, message: `${source} is not a URL: "${raw}".` };
|
|
7770
|
-
}
|
|
7771
|
-
if (url.protocol === "https:") return { ok: true, endpoint: url.origin };
|
|
7772
|
-
if (url.protocol === "http:" && isLoopback(url.hostname)) {
|
|
7773
|
-
return { ok: true, endpoint: url.origin };
|
|
7774
|
-
}
|
|
7775
|
-
return {
|
|
7776
|
-
ok: false,
|
|
7777
|
-
message: `${source} must be https \u2014 a credential is sent to it, and "${url.protocol}//" would send it in the clear. Plain http is accepted for localhost only.`
|
|
7778
|
-
};
|
|
7779
|
-
}
|
|
7780
|
-
|
|
7781
|
-
// src/session.ts
|
|
7782
|
-
async function ensureSession(args) {
|
|
7783
|
-
const stored = readStored();
|
|
7784
|
-
const resolved = resolveEndpoint(args.endpointFlag, stored);
|
|
7785
|
-
if (!resolved) {
|
|
7786
|
-
return {
|
|
7787
|
-
ok: false,
|
|
7788
|
-
message: "No endpoint. Run `vfac context set --endpoint https://your-organization.cloud.vardeflyt.no`, or set VFAC_ENDPOINT."
|
|
7789
|
-
};
|
|
7790
|
-
}
|
|
7791
|
-
const checked = validateEndpoint(resolved.value, resolved.source);
|
|
7792
|
-
if (!checked.ok) return { ok: false, message: checked.message };
|
|
7793
|
-
const endpoint = checked.endpoint;
|
|
7794
|
-
const credential = credentialFromEnvironment();
|
|
7795
|
-
if (args.forceExchange !== true && sessionUsable(stored.session, endpoint, args.now ?? /* @__PURE__ */ new Date(), credential?.keyId)) {
|
|
7796
|
-
return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
|
|
7797
|
-
}
|
|
7798
|
-
const signedIn = await signIn({
|
|
7799
|
-
endpoint,
|
|
7800
|
-
...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
|
|
7801
|
-
});
|
|
7802
|
-
if (!signedIn.ok) return { ok: false, message: signedIn.message };
|
|
7803
|
-
const next = { ...stored, session: signedIn.session };
|
|
7804
|
-
writeStored(next);
|
|
7805
|
-
return {
|
|
7806
|
-
ok: true,
|
|
7807
|
-
ready: {
|
|
7808
|
-
endpoint,
|
|
7809
|
-
token: signedIn.session.token,
|
|
7810
|
-
stored: next,
|
|
7811
|
-
via: "exchanged",
|
|
7812
|
-
...signedIn.gate ? { gate: signedIn.gate } : {}
|
|
7813
|
-
}
|
|
7814
|
-
};
|
|
7815
|
-
}
|
|
7816
|
-
function resolveProject(flag, stored) {
|
|
7817
|
-
return flag ?? stored.context?.projectId ?? null;
|
|
7818
|
-
}
|
|
7819
|
-
|
|
7820
|
-
// src/operations.ts
|
|
7821
|
-
var OPERATION_STATUSES = [
|
|
7822
|
-
"PENDING",
|
|
7823
|
-
"RUNNING",
|
|
7824
|
-
"SUCCEEDED",
|
|
7825
|
-
"FAILED",
|
|
7826
|
-
"CANCELLED"
|
|
7827
|
-
];
|
|
7828
|
-
var TERMINAL_STATUSES = ["SUCCEEDED", "FAILED", "CANCELLED"];
|
|
7829
|
-
var POLL_INTERVAL_MS = 5e3;
|
|
7830
|
-
var MAX_WAIT_MS = 60 * 60 * 1e3;
|
|
7831
|
-
function withProject(path, projectId) {
|
|
7832
|
-
return projectId === null ? path : `${path}?project=${encodeURIComponent(projectId)}`;
|
|
7833
|
-
}
|
|
7834
|
-
function operationPath(operationId, projectId) {
|
|
7835
|
-
return withProject(`/api/v1/operations/${encodeURIComponent(operationId)}`, projectId);
|
|
7836
|
-
}
|
|
7837
|
-
function classify(data) {
|
|
7838
|
-
const status = data["status"];
|
|
7839
|
-
if (typeof status !== "string") return "unknown";
|
|
7840
|
-
if (TERMINAL_STATUSES.includes(status)) return "terminal";
|
|
7841
|
-
if (OPERATION_STATUSES.includes(status)) return "working";
|
|
7842
|
-
return "unknown";
|
|
7843
|
-
}
|
|
7844
|
-
async function readOperation(args) {
|
|
7845
|
-
const result = await call({
|
|
7846
|
-
endpoint: args.endpoint,
|
|
7847
|
-
path: operationPath(args.operationId, args.projectId),
|
|
7848
|
-
token: args.token,
|
|
7849
|
-
...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
|
|
7850
|
-
});
|
|
7851
|
-
return result.ok ? { ok: true, data: result.data } : { ok: false, error: result.error };
|
|
7852
|
-
}
|
|
7853
|
-
async function waitForOperation(args) {
|
|
7854
|
-
const deps = args.deps ?? {};
|
|
7855
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
7856
|
-
const now = deps.now ?? (() => Date.now());
|
|
7857
|
-
const deadline = now() + MAX_WAIT_MS;
|
|
7858
|
-
let token = args.token;
|
|
7859
|
-
let refreshed = false;
|
|
7860
|
-
let last = null;
|
|
7861
|
-
for (; ; ) {
|
|
7862
|
-
const read = await readOperation({
|
|
7863
|
-
endpoint: args.endpoint,
|
|
7864
|
-
token,
|
|
7865
|
-
operationId: args.operationId,
|
|
7866
|
-
projectId: args.projectId,
|
|
7867
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
7868
|
-
});
|
|
7869
|
-
if (!read.ok) {
|
|
7870
|
-
if (read.error.status === 401 && !refreshed && deps.refreshToken) {
|
|
7871
|
-
refreshed = true;
|
|
7872
|
-
const next = await deps.refreshToken();
|
|
7873
|
-
if (next !== null) {
|
|
7874
|
-
token = next;
|
|
7875
|
-
continue;
|
|
7876
|
-
}
|
|
7877
|
-
}
|
|
7878
|
-
return { kind: "read-failed", error: read.error };
|
|
7879
|
-
}
|
|
7880
|
-
last = read.data;
|
|
7881
|
-
const verdict = classify(read.data);
|
|
7882
|
-
if (verdict === "terminal") {
|
|
7883
|
-
return { kind: "settled", status: String(read.data["status"]), data: read.data };
|
|
7884
|
-
}
|
|
7885
|
-
if (verdict === "unknown") {
|
|
7886
|
-
return { kind: "unknown-status", status: String(read.data["status"]), data: read.data };
|
|
7887
|
-
}
|
|
7888
|
-
args.onProgress?.(read.data);
|
|
7889
|
-
if (now() + POLL_INTERVAL_MS >= deadline) return { kind: "timed-out", data: last };
|
|
7890
|
-
await sleep(POLL_INTERVAL_MS);
|
|
7891
|
-
}
|
|
7892
|
-
}
|
|
7893
|
-
function renderOperation(operationId, data) {
|
|
7894
|
-
const status = String(data["status"] ?? "UNKNOWN");
|
|
7895
|
-
const phase = data["phase"];
|
|
7896
|
-
const lines = [
|
|
7897
|
-
typeof phase === "string" && phase !== "" ? `${operationId} ${status} (${phase})` : `${operationId} ${status}`
|
|
7898
|
-
];
|
|
7899
|
-
const type = data["type"];
|
|
7900
|
-
if (typeof type === "string") lines[0] += ` ${type}`;
|
|
7901
|
-
const steps = Array.isArray(data["steps"]) ? data["steps"] : [];
|
|
7902
|
-
if (steps.length > 0) {
|
|
7903
|
-
lines.push("");
|
|
7904
|
-
for (const step of steps) {
|
|
7905
|
-
const state = String(step["state"] ?? "");
|
|
7906
|
-
lines.push(` ${markerFor(state)} ${String(step["label"] ?? step["key"] ?? "")}`);
|
|
7907
|
-
}
|
|
7908
|
-
}
|
|
7909
|
-
const failure = asRecord(data["failure"]);
|
|
7910
|
-
if (failure) {
|
|
7911
|
-
lines.push("");
|
|
7912
|
-
const stage = failure["stage"];
|
|
7913
|
-
lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
|
|
7914
|
-
const summary = failure["summary"];
|
|
7915
|
-
if (typeof summary === "string") lines.push(` ${summary}`);
|
|
7916
|
-
const cause = failure["cause"];
|
|
7917
|
-
if (typeof cause === "string") lines.push(` cause: ${cause}`);
|
|
7918
|
-
const services = failure["services"];
|
|
7919
|
-
if (Array.isArray(services)) {
|
|
7920
|
-
for (const entry of services) {
|
|
7921
|
-
const one = asRecord(entry);
|
|
7922
|
-
if (one === null) continue;
|
|
7923
|
-
const dependency = one["dependency"];
|
|
7924
|
-
if (typeof dependency !== "string" || dependency === "") continue;
|
|
7925
|
-
const code = one["code"];
|
|
7926
|
-
lines.push(
|
|
7927
|
-
typeof code === "string" && code !== "" ? ` ${dependency} \u2014 the service reported: ${code}` : ` ${dependency} \u2014 the service reported nothing usable`
|
|
7928
|
-
);
|
|
7929
|
-
}
|
|
7930
|
-
}
|
|
7931
|
-
const product = failure["product"];
|
|
7932
|
-
const reportedSettings = [];
|
|
7933
|
-
if (Array.isArray(product)) {
|
|
7934
|
-
for (const entry of product) {
|
|
7935
|
-
const one = asRecord(entry);
|
|
7936
|
-
if (one === null) continue;
|
|
7937
|
-
const component = one["component"];
|
|
7938
|
-
const code = one["code"];
|
|
7939
|
-
if (typeof component !== "string" || typeof code !== "string") continue;
|
|
7940
|
-
if (component === "" || code === "") continue;
|
|
7941
|
-
const setting = one["setting"];
|
|
7942
|
-
if (typeof setting === "string" && setting !== "") {
|
|
7943
|
-
reportedSettings.push(setting);
|
|
7944
|
-
lines.push(` ${component} refused to start: ${code} (setting: ${setting})`);
|
|
7945
|
-
} else {
|
|
7946
|
-
lines.push(` ${component} refused to start: ${code}`);
|
|
7947
|
-
}
|
|
7948
|
-
}
|
|
7949
|
-
}
|
|
7950
|
-
const retryable = failure["retryable"];
|
|
7951
|
-
if (typeof retryable === "boolean") {
|
|
7952
|
-
const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
|
|
7953
|
-
const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
|
|
7954
|
-
const cause2 = failure["cause"];
|
|
7955
|
-
const named = reportedSettings[0];
|
|
7956
|
-
lines.push(
|
|
7957
|
-
retryable ? ` Another attempt can succeed: ${again}.` : named !== void 0 ? ` Correct \`${named}\` in your manifest, then \`vfac apply\` it${data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? ` and ${again}` : ""}. Repeating it unchanged fails the same way.` : cause2 === "unknown" ? ` The platform could not classify this, so there is nothing here to correct. Read the whole record with \`vfac get operation ${operationId} --output json\`, and quote that id if you ask for help. Repeating it unchanged fails the same way.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
|
|
7958
|
-
);
|
|
7959
|
-
}
|
|
7960
|
-
}
|
|
7961
|
-
return lines.join("\n");
|
|
7962
|
-
}
|
|
7963
|
-
function markerFor(state) {
|
|
7964
|
-
switch (state) {
|
|
7965
|
-
case "completed":
|
|
7966
|
-
return "\u2713";
|
|
7967
|
-
case "failed":
|
|
7968
|
-
return "\u2717";
|
|
7969
|
-
case "active":
|
|
7970
|
-
return "\u203A";
|
|
7971
|
-
case "pending":
|
|
7972
|
-
return "\xB7";
|
|
7973
|
-
default:
|
|
7974
|
-
return "-";
|
|
7975
|
-
}
|
|
7976
|
-
}
|
|
7977
|
-
function asRecord(value) {
|
|
7978
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
7979
|
-
}
|
|
7980
7706
|
|
|
7981
7707
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
7982
7708
|
var external_exports = {};
|
|
@@ -13790,6 +13516,9 @@ function withoutPropertyRules(properties) {
|
|
|
13790
13516
|
}
|
|
13791
13517
|
return out;
|
|
13792
13518
|
}
|
|
13519
|
+
function declaredSecretNames(schema) {
|
|
13520
|
+
return Object.entries(schema.properties).filter(([, property]) => isSecretProperty(property)).map(([name]) => name).sort();
|
|
13521
|
+
}
|
|
13793
13522
|
function defaultOf(property) {
|
|
13794
13523
|
switch (property.type) {
|
|
13795
13524
|
case "string":
|
|
@@ -17289,7 +17018,23 @@ function isTerminalAction(action) {
|
|
|
17289
17018
|
var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
|
|
17290
17019
|
|
|
17291
17020
|
// ../product-contracts/dist/contract-issue.js
|
|
17021
|
+
var PATH_SEGMENT = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
17022
|
+
var UNPRINTABLE_SEGMENT = "<unprintable>";
|
|
17292
17023
|
var MAX_SEGMENTS = MAX_CONFIGURATION_PATH_SEGMENTS + 2;
|
|
17024
|
+
function printableContractPath(path) {
|
|
17025
|
+
const printable = path.slice(0, MAX_SEGMENTS).map((segment) => typeof segment === "number" || PATH_SEGMENT.test(segment) ? String(segment) : UNPRINTABLE_SEGMENT);
|
|
17026
|
+
return printable.join(".") || "(document)";
|
|
17027
|
+
}
|
|
17028
|
+
function manifestConfigurationField(field) {
|
|
17029
|
+
if (field === "")
|
|
17030
|
+
return "spec.configuration";
|
|
17031
|
+
const parsed = parseConfigurationPath(field);
|
|
17032
|
+
return printableContractPath([
|
|
17033
|
+
"spec",
|
|
17034
|
+
"configuration",
|
|
17035
|
+
...parsed === null ? [field] : pathSegments(parsed)
|
|
17036
|
+
]);
|
|
17037
|
+
}
|
|
17293
17038
|
|
|
17294
17039
|
// ../product-contracts/dist/resource-output-reference.js
|
|
17295
17040
|
var OUTPUT_KEY_RULE = "Output keys are camelCase.";
|
|
@@ -17303,6 +17048,9 @@ var ResourceOutputReferenceSchema = external_exports.object({
|
|
|
17303
17048
|
}).strict()
|
|
17304
17049
|
}).strict()
|
|
17305
17050
|
}).strict();
|
|
17051
|
+
function looksLikeReference(value) {
|
|
17052
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "valueFrom" in value;
|
|
17053
|
+
}
|
|
17306
17054
|
|
|
17307
17055
|
// ../product-contracts/dist/dependency-binding.js
|
|
17308
17056
|
var DependencyBindingSelectorSchema = external_exports.object({
|
|
@@ -17438,6 +17186,23 @@ var ProductInstanceTemplateSchema = external_exports.object({
|
|
|
17438
17186
|
dependencyBindings: DependencyBindingsSchema.optional()
|
|
17439
17187
|
}).strict()
|
|
17440
17188
|
}).strict();
|
|
17189
|
+
function secretValuesInDocument(schema, configuration) {
|
|
17190
|
+
if (!isPlainRecord(configuration) || !isPlainRecord(schema.properties))
|
|
17191
|
+
return [];
|
|
17192
|
+
const declarations = Object.fromEntries(Object.entries(schema.properties).filter(([, property]) => isPlainRecord(property)));
|
|
17193
|
+
return declaredSecretNames({
|
|
17194
|
+
properties: declarations
|
|
17195
|
+
}).filter((name) => Object.hasOwn(configuration, name) && !looksLikeReference(configuration[name]));
|
|
17196
|
+
}
|
|
17197
|
+
function secretValueIssue(name) {
|
|
17198
|
+
return {
|
|
17199
|
+
field: manifestConfigurationField(name),
|
|
17200
|
+
message: `"${name}" is secret input and must never appear in a template. Supply it separately when applying, so it goes to the secret manager rather than into a document.`
|
|
17201
|
+
};
|
|
17202
|
+
}
|
|
17203
|
+
function isPlainRecord(value) {
|
|
17204
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17205
|
+
}
|
|
17441
17206
|
|
|
17442
17207
|
// ../product-contracts/dist/product-submission.schema.js
|
|
17443
17208
|
var PRODUCT_SUBMISSION_KIND = "ProductSubmission";
|
|
@@ -17506,36 +17271,369 @@ function checkProductSubmission(submission, ctx) {
|
|
|
17506
17271
|
});
|
|
17507
17272
|
}
|
|
17508
17273
|
}
|
|
17509
|
-
var PUBLISH_RULE_ANNEX = {
|
|
17510
|
-
allOf: [
|
|
17511
|
-
{
|
|
17512
|
-
$comment: 'displayName reads "<Product name> \u2014 <Short description>": an em dash (U+2014) with one space on each side. A hyphen and an en dash are different characters and are refused.',
|
|
17513
|
-
properties: {
|
|
17514
|
-
definition: { properties: { displayName: { pattern: PRODUCT_DISPLAY_NAME_PATTERN } } }
|
|
17274
|
+
var PUBLISH_RULE_ANNEX = {
|
|
17275
|
+
allOf: [
|
|
17276
|
+
{
|
|
17277
|
+
$comment: 'displayName reads "<Product name> \u2014 <Short description>": an em dash (U+2014) with one space on each side. A hyphen and an en dash are different characters and are refused.',
|
|
17278
|
+
properties: {
|
|
17279
|
+
definition: { properties: { displayName: { pattern: PRODUCT_DISPLAY_NAME_PATTERN } } }
|
|
17280
|
+
}
|
|
17281
|
+
},
|
|
17282
|
+
{
|
|
17283
|
+
// THE SAME ASYMMETRY, one field along. `portfolio` is optional on a
|
|
17284
|
+
// Definition because frozen releases carry none; a Submission is by
|
|
17285
|
+
// construction a new release, so it must say where the Product belongs.
|
|
17286
|
+
// The `enum` is the LIVE list — narrower than the Definition's, which
|
|
17287
|
+
// still admits the retired ids a stored document may name.
|
|
17288
|
+
$comment: "A new release must declare portfolio.category, the catalog section a customer finds this Product under. Retired section ids still parse in a published Definition but may not be published again.",
|
|
17289
|
+
properties: {
|
|
17290
|
+
definition: {
|
|
17291
|
+
required: ["portfolio"],
|
|
17292
|
+
properties: {
|
|
17293
|
+
portfolio: {
|
|
17294
|
+
required: ["category"],
|
|
17295
|
+
properties: { category: { enum: [...PORTFOLIO_CATEGORY_IDS] } }
|
|
17296
|
+
}
|
|
17297
|
+
}
|
|
17298
|
+
}
|
|
17299
|
+
}
|
|
17300
|
+
}
|
|
17301
|
+
]
|
|
17302
|
+
};
|
|
17303
|
+
withJsonSchemaAnnex(ProductSubmissionSchema, PUBLISH_RULE_ANNEX);
|
|
17304
|
+
|
|
17305
|
+
// src/manifest.ts
|
|
17306
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
17307
|
+
function readManifest(path) {
|
|
17308
|
+
let text;
|
|
17309
|
+
try {
|
|
17310
|
+
text = readFileSync2(path, "utf8");
|
|
17311
|
+
} catch {
|
|
17312
|
+
return { ok: false, message: `Could not read ${path}.` };
|
|
17313
|
+
}
|
|
17314
|
+
let parsed;
|
|
17315
|
+
try {
|
|
17316
|
+
parsed = (0, import_yaml.parse)(text, { prettyErrors: false });
|
|
17317
|
+
} catch (error) {
|
|
17318
|
+
return {
|
|
17319
|
+
ok: false,
|
|
17320
|
+
message: `${path} is not valid YAML or JSON${whereItFailed(text, error)}.`
|
|
17321
|
+
};
|
|
17322
|
+
}
|
|
17323
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
17324
|
+
return { ok: false, message: `${path} does not contain a manifest document.` };
|
|
17325
|
+
}
|
|
17326
|
+
return { ok: true, document: parsed };
|
|
17327
|
+
}
|
|
17328
|
+
function resolveSecrets(pairs, environment = process.env) {
|
|
17329
|
+
const secrets = {};
|
|
17330
|
+
for (const [index, pair] of pairs.entries()) {
|
|
17331
|
+
const label = pairs.length === 1 ? "--secret" : `--secret #${index + 1}`;
|
|
17332
|
+
const separator = pair.indexOf("=");
|
|
17333
|
+
if (separator <= 0) {
|
|
17334
|
+
return {
|
|
17335
|
+
ok: false,
|
|
17336
|
+
message: `${label} expects name=env:VARIABLE, and there is no name before "=".`
|
|
17337
|
+
};
|
|
17338
|
+
}
|
|
17339
|
+
const name = pair.slice(0, separator);
|
|
17340
|
+
const source = pair.slice(separator + 1);
|
|
17341
|
+
if (!source.startsWith("env:")) {
|
|
17342
|
+
return {
|
|
17343
|
+
ok: false,
|
|
17344
|
+
message: `${label} must read from the environment: --secret <name>=env:VARIABLE. A value on the command line is in your shell history and in the process table.`
|
|
17345
|
+
};
|
|
17346
|
+
}
|
|
17347
|
+
const variable = source.slice("env:".length);
|
|
17348
|
+
if (variable.length === 0) {
|
|
17349
|
+
return { ok: false, message: `${label} names no variable after "env:".` };
|
|
17350
|
+
}
|
|
17351
|
+
const value = environment[variable];
|
|
17352
|
+
if (value === void 0 || value === "") {
|
|
17353
|
+
return {
|
|
17354
|
+
ok: false,
|
|
17355
|
+
message: `${label} reads an environment variable that is not set, or is empty.`
|
|
17356
|
+
};
|
|
17357
|
+
}
|
|
17358
|
+
secrets[name] = value;
|
|
17359
|
+
}
|
|
17360
|
+
return { ok: true, secrets };
|
|
17361
|
+
}
|
|
17362
|
+
function manifestKeyOf(document) {
|
|
17363
|
+
const metadata = document["metadata"];
|
|
17364
|
+
if (typeof metadata !== "object" || metadata === null) return null;
|
|
17365
|
+
const key = metadata["key"];
|
|
17366
|
+
return typeof key === "string" ? key : null;
|
|
17367
|
+
}
|
|
17368
|
+
function whereItFailed(text, error) {
|
|
17369
|
+
if (typeof error !== "object" || error === null) return "";
|
|
17370
|
+
const code = error.code;
|
|
17371
|
+
const pos = error.pos;
|
|
17372
|
+
const kind = typeof code === "string" && /^[A-Z_]+$/.test(code) ? code : null;
|
|
17373
|
+
const offset = Array.isArray(pos) && typeof pos[0] === "number" ? pos[0] : null;
|
|
17374
|
+
if (offset === null) return kind ? ` (${kind})` : "";
|
|
17375
|
+
const before = text.slice(0, offset);
|
|
17376
|
+
const line = before.split("\n").length;
|
|
17377
|
+
const column = offset - before.lastIndexOf("\n");
|
|
17378
|
+
return ` (${kind ?? "error"} at line ${line}, column ${column})`;
|
|
17379
|
+
}
|
|
17380
|
+
function pinnedRelease(document) {
|
|
17381
|
+
const spec = document["spec"];
|
|
17382
|
+
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) return null;
|
|
17383
|
+
const { productId, productVersion } = spec;
|
|
17384
|
+
if (typeof productId !== "string" || productId === "") return null;
|
|
17385
|
+
if (typeof productVersion !== "string" || productVersion === "") return null;
|
|
17386
|
+
return { productId, productVersion };
|
|
17387
|
+
}
|
|
17388
|
+
function carriesLiteralSettings(document) {
|
|
17389
|
+
const configuration = configurationOf(document);
|
|
17390
|
+
return configuration !== null && Object.values(configuration).some((value) => !looksLikeReference(value));
|
|
17391
|
+
}
|
|
17392
|
+
function pastedSecretSettings(document, view) {
|
|
17393
|
+
const pinned = pinnedRelease(document);
|
|
17394
|
+
if (!pinned || typeof view !== "object" || view === null) return [];
|
|
17395
|
+
const { productId, version, configuration } = view;
|
|
17396
|
+
if (productId !== pinned.productId || version !== pinned.productVersion) return [];
|
|
17397
|
+
if (typeof configuration !== "object" || configuration === null) return [];
|
|
17398
|
+
try {
|
|
17399
|
+
return secretValuesInDocument(
|
|
17400
|
+
configuration,
|
|
17401
|
+
configurationOf(document)
|
|
17402
|
+
);
|
|
17403
|
+
} catch {
|
|
17404
|
+
return [];
|
|
17405
|
+
}
|
|
17406
|
+
}
|
|
17407
|
+
function configurationOf(document) {
|
|
17408
|
+
const spec = document["spec"];
|
|
17409
|
+
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) return null;
|
|
17410
|
+
const configuration = spec["configuration"];
|
|
17411
|
+
if (typeof configuration !== "object" || configuration === null || Array.isArray(configuration)) {
|
|
17412
|
+
return null;
|
|
17413
|
+
}
|
|
17414
|
+
return configuration;
|
|
17415
|
+
}
|
|
17416
|
+
|
|
17417
|
+
// src/endpoint.ts
|
|
17418
|
+
function isLoopback(hostname) {
|
|
17419
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
17420
|
+
}
|
|
17421
|
+
function validateEndpoint(raw, source) {
|
|
17422
|
+
let url;
|
|
17423
|
+
try {
|
|
17424
|
+
url = new URL(raw);
|
|
17425
|
+
} catch {
|
|
17426
|
+
return { ok: false, message: `${source} is not a URL: "${raw}".` };
|
|
17427
|
+
}
|
|
17428
|
+
if (url.protocol === "https:") return { ok: true, endpoint: url.origin };
|
|
17429
|
+
if (url.protocol === "http:" && isLoopback(url.hostname)) {
|
|
17430
|
+
return { ok: true, endpoint: url.origin };
|
|
17431
|
+
}
|
|
17432
|
+
return {
|
|
17433
|
+
ok: false,
|
|
17434
|
+
message: `${source} must be https \u2014 a credential is sent to it, and "${url.protocol}//" would send it in the clear. Plain http is accepted for localhost only.`
|
|
17435
|
+
};
|
|
17436
|
+
}
|
|
17437
|
+
|
|
17438
|
+
// src/session.ts
|
|
17439
|
+
async function ensureSession(args) {
|
|
17440
|
+
const stored = readStored();
|
|
17441
|
+
const resolved = resolveEndpoint(args.endpointFlag, stored);
|
|
17442
|
+
if (!resolved) {
|
|
17443
|
+
return {
|
|
17444
|
+
ok: false,
|
|
17445
|
+
message: "No endpoint. Run `vfac context set --endpoint https://your-organization.cloud.vardeflyt.no`, or set VFAC_ENDPOINT."
|
|
17446
|
+
};
|
|
17447
|
+
}
|
|
17448
|
+
const checked = validateEndpoint(resolved.value, resolved.source);
|
|
17449
|
+
if (!checked.ok) return { ok: false, message: checked.message };
|
|
17450
|
+
const endpoint = checked.endpoint;
|
|
17451
|
+
const credential = credentialFromEnvironment();
|
|
17452
|
+
if (args.forceExchange !== true && sessionUsable(stored.session, endpoint, args.now ?? /* @__PURE__ */ new Date(), credential?.keyId)) {
|
|
17453
|
+
return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
|
|
17454
|
+
}
|
|
17455
|
+
const signedIn = await signIn({
|
|
17456
|
+
endpoint,
|
|
17457
|
+
...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
|
|
17458
|
+
});
|
|
17459
|
+
if (!signedIn.ok) return { ok: false, message: signedIn.message };
|
|
17460
|
+
const next = { ...stored, session: signedIn.session };
|
|
17461
|
+
writeStored(next);
|
|
17462
|
+
return {
|
|
17463
|
+
ok: true,
|
|
17464
|
+
ready: {
|
|
17465
|
+
endpoint,
|
|
17466
|
+
token: signedIn.session.token,
|
|
17467
|
+
stored: next,
|
|
17468
|
+
via: "exchanged",
|
|
17469
|
+
...signedIn.gate ? { gate: signedIn.gate } : {}
|
|
17470
|
+
}
|
|
17471
|
+
};
|
|
17472
|
+
}
|
|
17473
|
+
function resolveProject(flag, stored) {
|
|
17474
|
+
return flag ?? stored.context?.projectId ?? null;
|
|
17475
|
+
}
|
|
17476
|
+
|
|
17477
|
+
// src/operations.ts
|
|
17478
|
+
var OPERATION_STATUSES = [
|
|
17479
|
+
"PENDING",
|
|
17480
|
+
"RUNNING",
|
|
17481
|
+
"SUCCEEDED",
|
|
17482
|
+
"FAILED",
|
|
17483
|
+
"CANCELLED"
|
|
17484
|
+
];
|
|
17485
|
+
var TERMINAL_STATUSES = ["SUCCEEDED", "FAILED", "CANCELLED"];
|
|
17486
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
17487
|
+
var MAX_WAIT_MS = 60 * 60 * 1e3;
|
|
17488
|
+
function withProject(path, projectId) {
|
|
17489
|
+
return projectId === null ? path : `${path}?project=${encodeURIComponent(projectId)}`;
|
|
17490
|
+
}
|
|
17491
|
+
function operationPath(operationId, projectId) {
|
|
17492
|
+
return withProject(`/api/v1/operations/${encodeURIComponent(operationId)}`, projectId);
|
|
17493
|
+
}
|
|
17494
|
+
function classify(data) {
|
|
17495
|
+
const status = data["status"];
|
|
17496
|
+
if (typeof status !== "string") return "unknown";
|
|
17497
|
+
if (TERMINAL_STATUSES.includes(status)) return "terminal";
|
|
17498
|
+
if (OPERATION_STATUSES.includes(status)) return "working";
|
|
17499
|
+
return "unknown";
|
|
17500
|
+
}
|
|
17501
|
+
async function readOperation(args) {
|
|
17502
|
+
const result = await call({
|
|
17503
|
+
endpoint: args.endpoint,
|
|
17504
|
+
path: operationPath(args.operationId, args.projectId),
|
|
17505
|
+
token: args.token,
|
|
17506
|
+
...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
|
|
17507
|
+
});
|
|
17508
|
+
return result.ok ? { ok: true, data: result.data } : { ok: false, error: result.error };
|
|
17509
|
+
}
|
|
17510
|
+
async function waitForOperation(args) {
|
|
17511
|
+
const deps = args.deps ?? {};
|
|
17512
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
17513
|
+
const now = deps.now ?? (() => Date.now());
|
|
17514
|
+
const deadline = now() + MAX_WAIT_MS;
|
|
17515
|
+
let token = args.token;
|
|
17516
|
+
let refreshed = false;
|
|
17517
|
+
let last = null;
|
|
17518
|
+
for (; ; ) {
|
|
17519
|
+
const read = await readOperation({
|
|
17520
|
+
endpoint: args.endpoint,
|
|
17521
|
+
token,
|
|
17522
|
+
operationId: args.operationId,
|
|
17523
|
+
projectId: args.projectId,
|
|
17524
|
+
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
17525
|
+
});
|
|
17526
|
+
if (!read.ok) {
|
|
17527
|
+
if (read.error.status === 401 && !refreshed && deps.refreshToken) {
|
|
17528
|
+
refreshed = true;
|
|
17529
|
+
const next = await deps.refreshToken();
|
|
17530
|
+
if (next !== null) {
|
|
17531
|
+
token = next;
|
|
17532
|
+
continue;
|
|
17533
|
+
}
|
|
17534
|
+
}
|
|
17535
|
+
return { kind: "read-failed", error: read.error };
|
|
17536
|
+
}
|
|
17537
|
+
last = read.data;
|
|
17538
|
+
const verdict = classify(read.data);
|
|
17539
|
+
if (verdict === "terminal") {
|
|
17540
|
+
return { kind: "settled", status: String(read.data["status"]), data: read.data };
|
|
17541
|
+
}
|
|
17542
|
+
if (verdict === "unknown") {
|
|
17543
|
+
return { kind: "unknown-status", status: String(read.data["status"]), data: read.data };
|
|
17544
|
+
}
|
|
17545
|
+
args.onProgress?.(read.data);
|
|
17546
|
+
if (now() + POLL_INTERVAL_MS >= deadline) return { kind: "timed-out", data: last };
|
|
17547
|
+
await sleep(POLL_INTERVAL_MS);
|
|
17548
|
+
}
|
|
17549
|
+
}
|
|
17550
|
+
function renderOperation(operationId, data) {
|
|
17551
|
+
const status = String(data["status"] ?? "UNKNOWN");
|
|
17552
|
+
const phase = data["phase"];
|
|
17553
|
+
const lines = [
|
|
17554
|
+
typeof phase === "string" && phase !== "" ? `${operationId} ${status} (${phase})` : `${operationId} ${status}`
|
|
17555
|
+
];
|
|
17556
|
+
const type = data["type"];
|
|
17557
|
+
if (typeof type === "string") lines[0] += ` ${type}`;
|
|
17558
|
+
const steps = Array.isArray(data["steps"]) ? data["steps"] : [];
|
|
17559
|
+
if (steps.length > 0) {
|
|
17560
|
+
lines.push("");
|
|
17561
|
+
for (const step of steps) {
|
|
17562
|
+
const state = String(step["state"] ?? "");
|
|
17563
|
+
lines.push(` ${markerFor(state)} ${String(step["label"] ?? step["key"] ?? "")}`);
|
|
17564
|
+
}
|
|
17565
|
+
}
|
|
17566
|
+
const failure = asRecord(data["failure"]);
|
|
17567
|
+
if (failure) {
|
|
17568
|
+
lines.push("");
|
|
17569
|
+
const stage = failure["stage"];
|
|
17570
|
+
lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
|
|
17571
|
+
const summary = failure["summary"];
|
|
17572
|
+
if (typeof summary === "string") lines.push(` ${summary}`);
|
|
17573
|
+
const cause = failure["cause"];
|
|
17574
|
+
if (typeof cause === "string") lines.push(` cause: ${cause}`);
|
|
17575
|
+
const services = failure["services"];
|
|
17576
|
+
if (Array.isArray(services)) {
|
|
17577
|
+
for (const entry of services) {
|
|
17578
|
+
const one = asRecord(entry);
|
|
17579
|
+
if (one === null) continue;
|
|
17580
|
+
const dependency = one["dependency"];
|
|
17581
|
+
if (typeof dependency !== "string" || dependency === "") continue;
|
|
17582
|
+
const code = one["code"];
|
|
17583
|
+
lines.push(
|
|
17584
|
+
typeof code === "string" && code !== "" ? ` ${dependency} \u2014 the service reported: ${code}` : ` ${dependency} \u2014 the service reported nothing usable`
|
|
17585
|
+
);
|
|
17515
17586
|
}
|
|
17516
|
-
}
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
17520
|
-
|
|
17521
|
-
|
|
17522
|
-
|
|
17523
|
-
|
|
17524
|
-
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
}
|
|
17587
|
+
}
|
|
17588
|
+
const product = failure["product"];
|
|
17589
|
+
const reportedSettings = [];
|
|
17590
|
+
if (Array.isArray(product)) {
|
|
17591
|
+
for (const entry of product) {
|
|
17592
|
+
const one = asRecord(entry);
|
|
17593
|
+
if (one === null) continue;
|
|
17594
|
+
const component = one["component"];
|
|
17595
|
+
const code = one["code"];
|
|
17596
|
+
if (typeof component !== "string" || typeof code !== "string") continue;
|
|
17597
|
+
if (component === "" || code === "") continue;
|
|
17598
|
+
const setting = one["setting"];
|
|
17599
|
+
if (typeof setting === "string" && setting !== "") {
|
|
17600
|
+
reportedSettings.push(setting);
|
|
17601
|
+
lines.push(` ${component} refused to start: ${code} (setting: ${setting})`);
|
|
17602
|
+
} else {
|
|
17603
|
+
lines.push(` ${component} refused to start: ${code}`);
|
|
17533
17604
|
}
|
|
17534
17605
|
}
|
|
17535
17606
|
}
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
|
|
17607
|
+
const retryable = failure["retryable"];
|
|
17608
|
+
if (typeof retryable === "boolean") {
|
|
17609
|
+
const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
|
|
17610
|
+
const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
|
|
17611
|
+
const cause2 = failure["cause"];
|
|
17612
|
+
const named = reportedSettings[0];
|
|
17613
|
+
lines.push(
|
|
17614
|
+
retryable ? ` Another attempt can succeed: ${again}.` : named !== void 0 ? ` Correct \`${named}\` in your manifest, then \`vfac apply\` it${data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? ` and ${again}` : ""}. Repeating it unchanged fails the same way.` : cause2 === "unknown" ? ` The platform could not classify this, so there is nothing here to correct. Read the whole record with \`vfac get operation ${operationId} --output json\`, and quote that id if you ask for help. Repeating it unchanged fails the same way.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
|
|
17615
|
+
);
|
|
17616
|
+
}
|
|
17617
|
+
}
|
|
17618
|
+
return lines.join("\n");
|
|
17619
|
+
}
|
|
17620
|
+
function markerFor(state) {
|
|
17621
|
+
switch (state) {
|
|
17622
|
+
case "completed":
|
|
17623
|
+
return "\u2713";
|
|
17624
|
+
case "failed":
|
|
17625
|
+
return "\u2717";
|
|
17626
|
+
case "active":
|
|
17627
|
+
return "\u203A";
|
|
17628
|
+
case "pending":
|
|
17629
|
+
return "\xB7";
|
|
17630
|
+
default:
|
|
17631
|
+
return "-";
|
|
17632
|
+
}
|
|
17633
|
+
}
|
|
17634
|
+
function asRecord(value) {
|
|
17635
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
17636
|
+
}
|
|
17539
17637
|
|
|
17540
17638
|
// src/render.ts
|
|
17541
17639
|
function isUnconverged(plan) {
|
|
@@ -17649,6 +17747,12 @@ async function runApply(verb, parsed, deps = {}) {
|
|
|
17649
17747
|
return 1;
|
|
17650
17748
|
}
|
|
17651
17749
|
const json = parsed.flags["output"] === "json";
|
|
17750
|
+
const pasted = await pastedSecretsIn(manifest2.document, {
|
|
17751
|
+
endpoint: session.ready.endpoint,
|
|
17752
|
+
token: session.ready.token,
|
|
17753
|
+
projectId
|
|
17754
|
+
});
|
|
17755
|
+
if (pasted.length > 0) return refusePastedSecrets(file, pasted, json);
|
|
17652
17756
|
const path = `/api/v1/projects/${encodeURIComponent(projectId)}/manifest`;
|
|
17653
17757
|
const envelope = {
|
|
17654
17758
|
template: manifest2.document,
|
|
@@ -17842,6 +17946,39 @@ Read the operation that failed: \`vfac get operation <id>\`, or \`vfac get produ
|
|
|
17842
17946
|
}
|
|
17843
17947
|
}
|
|
17844
17948
|
}
|
|
17949
|
+
async function pastedSecretsIn(document, target) {
|
|
17950
|
+
const pinned = pinnedRelease(document);
|
|
17951
|
+
if (!pinned || !carriesLiteralSettings(document)) return [];
|
|
17952
|
+
const view = await call({
|
|
17953
|
+
endpoint: target.endpoint,
|
|
17954
|
+
path: `/api/v1/projects/${encodeURIComponent(target.projectId)}/products/${encodeURIComponent(pinned.productId)}`,
|
|
17955
|
+
token: target.token
|
|
17956
|
+
});
|
|
17957
|
+
return view.ok ? pastedSecretSettings(document, view.data) : [];
|
|
17958
|
+
}
|
|
17959
|
+
function refusePastedSecrets(file, names, json) {
|
|
17960
|
+
const fields = names.map(secretValueIssue);
|
|
17961
|
+
if (json) {
|
|
17962
|
+
return printError(
|
|
17963
|
+
{
|
|
17964
|
+
status: 400,
|
|
17965
|
+
code: "REQUEST_INVALID",
|
|
17966
|
+
message: `${file} contains a secret value, so it was not sent. Nothing was changed.`,
|
|
17967
|
+
fields
|
|
17968
|
+
},
|
|
17969
|
+
true
|
|
17970
|
+
);
|
|
17971
|
+
}
|
|
17972
|
+
for (const field of fields) {
|
|
17973
|
+
process.stderr.write(`${field.field} contains a secret value.
|
|
17974
|
+
`);
|
|
17975
|
+
}
|
|
17976
|
+
process.stderr.write(
|
|
17977
|
+
"\nSecrets must not be stored in a manifest, so this file was not sent and nothing was changed.\nRemove the value and supply it with:\n" + names.map((name) => ` --secret ${name}=env:VARIABLE
|
|
17978
|
+
`).join("") + "\nIf the value was a real credential and this file has been committed or shared,\nrotate it.\n"
|
|
17979
|
+
);
|
|
17980
|
+
return 1;
|
|
17981
|
+
}
|
|
17845
17982
|
|
|
17846
17983
|
// src/commands/context.ts
|
|
17847
17984
|
function runContext(parsed) {
|
|
@@ -18025,21 +18162,32 @@ async function runDoctor(parsed) {
|
|
|
18025
18162
|
const mutating = access.projects.filter(
|
|
18026
18163
|
(project) => project.permissions.some((permission) => !permission.endsWith(".read"))
|
|
18027
18164
|
);
|
|
18165
|
+
const organizationWide = access.tenantPermissions.includes("project.read");
|
|
18166
|
+
const alsoOrganizationWide = access.tenantPermissions.length > 0 ? `Organization-wide it holds ${access.tenantPermissions.join(", ")}, which reaches no Project. ` : "";
|
|
18167
|
+
const perProject = projects.length === 0 ? "" : `. Granted per Project: ${projects.join(", ")}` + (mutating.length > 0 ? ` \u2014 you can deploy in ${mutating.map((project) => project.projectId).join(", ")}` : " \u2014 read-only there");
|
|
18028
18168
|
checks.push(
|
|
18029
|
-
|
|
18169
|
+
organizationWide ? {
|
|
18030
18170
|
name: "Access",
|
|
18031
18171
|
ok: true,
|
|
18032
|
-
detail: `the whole organization \u2014 ${access.tenantPermissions.join(", ")}`,
|
|
18033
|
-
facts: {
|
|
18172
|
+
detail: `the whole organization \u2014 ${access.tenantPermissions.join(", ")}${perProject}`,
|
|
18173
|
+
facts: {
|
|
18174
|
+
scope: "organization",
|
|
18175
|
+
grantedProjectIds: projects.length > 0 ? projects.join(",") : null
|
|
18176
|
+
}
|
|
18034
18177
|
} : projects.length === 0 ? {
|
|
18035
18178
|
// A FAILURE, NOT A NOTE. This credential authenticates and can do
|
|
18036
18179
|
// nothing at all — and the check said so in words while `report`
|
|
18037
18180
|
// computed the exit code from `ok` alone, so `doctor` printed
|
|
18038
18181
|
// "nothing else will work" and exited 0. A pipeline reads the code,
|
|
18039
18182
|
// not the sentence.
|
|
18183
|
+
//
|
|
18184
|
+
// STILL A FAILURE WITH KNOWLEDGE READ AND NO PROJECT. That gate is a
|
|
18185
|
+
// legitimate one — it serves an MCP client — but every `vfac`
|
|
18186
|
+
// command that reads a Project is refused under it, and a pipeline
|
|
18187
|
+
// pointed at it has the wrong credential.
|
|
18040
18188
|
name: "Access",
|
|
18041
18189
|
ok: false,
|
|
18042
|
-
detail: "none. This credential holds no Project and nothing organization-wide, so every command below it will be refused. Ask an organization administrator for access."
|
|
18190
|
+
detail: access.tenantPermissions.length > 0 ? `no Project. ${alsoOrganizationWide}Every command below that reads a Project will be refused. A gate holding only that serves an MCP client, not vfac; ask an organization administrator for a Project role on the gate vfac uses.` : "none. This credential holds no Project and nothing organization-wide, so every command below it will be refused. Ask an organization administrator for access."
|
|
18043
18191
|
} : {
|
|
18044
18192
|
name: "Access",
|
|
18045
18193
|
ok: true,
|
|
@@ -18068,7 +18216,7 @@ async function runDoctor(parsed) {
|
|
|
18068
18216
|
// names are the difference.
|
|
18069
18217
|
grantedProjectIds: projects.join(",")
|
|
18070
18218
|
},
|
|
18071
|
-
detail: `${projects.join(", ")} \u2014 and nothing organization-wide.
|
|
18219
|
+
detail: `${projects.join(", ")}` + (access.tenantPermissions.length > 0 ? ". " : " \u2014 and nothing organization-wide. ") + alsoOrganizationWide + (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."
|
|
18072
18220
|
}
|
|
18073
18221
|
);
|
|
18074
18222
|
checks.push({
|
|
@@ -18223,9 +18371,9 @@ var MAX_PAGES = 50;
|
|
|
18223
18371
|
async function readAllPages(args) {
|
|
18224
18372
|
const items = [];
|
|
18225
18373
|
let cursor = args.cursor;
|
|
18226
|
-
const onePage = args.cursor !== void 0 || args.pageSize !== void 0;
|
|
18374
|
+
const onePage = args.onePage === true || args.cursor !== void 0 || args.pageSize !== void 0;
|
|
18227
18375
|
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
18228
|
-
const query = new URLSearchParams();
|
|
18376
|
+
const query = new URLSearchParams(args.query ?? {});
|
|
18229
18377
|
if (cursor !== void 0) query.set("cursor", cursor);
|
|
18230
18378
|
if (args.pageSize !== void 0) query.set("pageSize", args.pageSize);
|
|
18231
18379
|
const suffix = query.toString();
|
|
@@ -18260,7 +18408,7 @@ async function readAllPages(args) {
|
|
|
18260
18408
|
}
|
|
18261
18409
|
|
|
18262
18410
|
// src/commands/get.ts
|
|
18263
|
-
async function runGet(parsed) {
|
|
18411
|
+
async function runGet(parsed, deps = {}) {
|
|
18264
18412
|
const noun = parsed.command[1];
|
|
18265
18413
|
const json = parsed.flags["output"] === "json";
|
|
18266
18414
|
const session = await ensureSession({ endpointFlag: parsed.flags["endpoint"] });
|
|
@@ -18337,8 +18485,10 @@ async function runGet(parsed) {
|
|
|
18337
18485
|
const label = String(one["displayName"] ?? one["key"] ?? "");
|
|
18338
18486
|
const state = String(one["state"] ?? "UNKNOWN");
|
|
18339
18487
|
const reason = one["reason"];
|
|
18488
|
+
const observedAt = one["observedAt"];
|
|
18489
|
+
const stale = one["freshness"] === "STALE" && typeof observedAt === "string" ? ` [stale \u2014 last observed ${ageOf(observedAt, (deps.now ?? (() => /* @__PURE__ */ new Date()))())} ago]` : "";
|
|
18340
18490
|
lines.push(
|
|
18341
|
-
` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}`
|
|
18491
|
+
` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}${stale}`
|
|
18342
18492
|
);
|
|
18343
18493
|
const description = one["description"];
|
|
18344
18494
|
if (typeof description === "string") {
|
|
@@ -18375,6 +18525,10 @@ async function runGet(parsed) {
|
|
|
18375
18525
|
if (allowed.length > 0) {
|
|
18376
18526
|
lines.push("", ` You may: ${allowed.join(", ")} (vfac lifecycle <command> ${instanceId})`);
|
|
18377
18527
|
}
|
|
18528
|
+
const notNow = (lifecycle["refused"] ?? []).filter(
|
|
18529
|
+
(entry) => typeof entry["command"] === "string" && typeof entry["reason"] === "string" && entry["reason"] !== "permission_required"
|
|
18530
|
+
).map((entry) => `${String(entry["command"])} (${String(entry["reason"])})`);
|
|
18531
|
+
if (notNow.length > 0) lines.push(` Not now: ${notNow.join(", ")}`);
|
|
18378
18532
|
process.stdout.write(`${lines.join("\n")}
|
|
18379
18533
|
`);
|
|
18380
18534
|
return 0;
|
|
@@ -18421,7 +18575,75 @@ This is one page. Continue with --cursor ${result.nextCursor ?? ""}
|
|
|
18421
18575
|
}
|
|
18422
18576
|
return 0;
|
|
18423
18577
|
}
|
|
18424
|
-
|
|
18578
|
+
if (noun === "operations") {
|
|
18579
|
+
const projectId = resolveProject(parsed.flags["project"], session.ready.stored);
|
|
18580
|
+
if (!projectId) {
|
|
18581
|
+
process.stderr.write("No Project. Use --project prj_\u2026 or set one in your context.\n");
|
|
18582
|
+
return 2;
|
|
18583
|
+
}
|
|
18584
|
+
const resource = parsed.flags["resource"];
|
|
18585
|
+
const result = await readAllPages({
|
|
18586
|
+
endpoint: session.ready.endpoint,
|
|
18587
|
+
token: session.ready.token,
|
|
18588
|
+
path: `/api/v1/projects/${encodeURIComponent(projectId)}/operations`,
|
|
18589
|
+
key: "operations",
|
|
18590
|
+
cursor: parsed.flags["cursor"],
|
|
18591
|
+
pageSize: parsed.flags["page-size"],
|
|
18592
|
+
onePage: true,
|
|
18593
|
+
...resource === void 0 ? {} : { query: { productInstanceId: resource } }
|
|
18594
|
+
});
|
|
18595
|
+
if (!result.ok) {
|
|
18596
|
+
if (result.error.status === 404 && result.error.code === "NOT_FOUND") {
|
|
18597
|
+
process.stderr.write(
|
|
18598
|
+
"This Tenant Portal does not list operations yet \u2014 it is running an older build.\n"
|
|
18599
|
+
);
|
|
18600
|
+
return 1;
|
|
18601
|
+
}
|
|
18602
|
+
return printError(result.error, json);
|
|
18603
|
+
}
|
|
18604
|
+
if (json) {
|
|
18605
|
+
process.stdout.write(
|
|
18606
|
+
`${JSON.stringify({ operations: result.items, nextCursor: result.nextCursor })}
|
|
18607
|
+
`
|
|
18608
|
+
);
|
|
18609
|
+
return 0;
|
|
18610
|
+
}
|
|
18611
|
+
if (result.items.length === 0) {
|
|
18612
|
+
process.stdout.write(
|
|
18613
|
+
resource === void 0 ? "No operations in this project.\n" : "No operations for this resource in this project.\n"
|
|
18614
|
+
);
|
|
18615
|
+
return 0;
|
|
18616
|
+
}
|
|
18617
|
+
const rows = result.items.map((operation) => [
|
|
18618
|
+
operation.operationId ?? "(no id)",
|
|
18619
|
+
// `PRODUCT_INSTANCE_UPDATE` is every line's prefix and says nothing a
|
|
18620
|
+
// reader scanning the column needs. JSON keeps the platform's word.
|
|
18621
|
+
(operation.type ?? "").replace(/^PRODUCT_INSTANCE_/, ""),
|
|
18622
|
+
operation.status ?? "",
|
|
18623
|
+
operation.productInstanceId ?? "-",
|
|
18624
|
+
operation.createdAt ?? ""
|
|
18625
|
+
]);
|
|
18626
|
+
const widths = rows[0].map(
|
|
18627
|
+
(_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
|
|
18628
|
+
);
|
|
18629
|
+
for (const row of rows) {
|
|
18630
|
+
process.stdout.write(
|
|
18631
|
+
`${row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ")}
|
|
18632
|
+
`
|
|
18633
|
+
);
|
|
18634
|
+
}
|
|
18635
|
+
if (!result.complete) {
|
|
18636
|
+
process.stderr.write(
|
|
18637
|
+
`
|
|
18638
|
+
This is one page. Continue with --cursor ${result.nextCursor ?? ""}
|
|
18639
|
+
`
|
|
18640
|
+
);
|
|
18641
|
+
}
|
|
18642
|
+
return 0;
|
|
18643
|
+
}
|
|
18644
|
+
process.stderr.write(
|
|
18645
|
+
"Usage: vfac get product-instances | vfac get operation op_\u2026 | vfac get operations [--resource pri_\u2026]\n"
|
|
18646
|
+
);
|
|
18425
18647
|
return 2;
|
|
18426
18648
|
}
|
|
18427
18649
|
function wrapOutput(text, width) {
|
|
@@ -18438,6 +18660,13 @@ function wrapOutput(text, width) {
|
|
|
18438
18660
|
if (current !== "") out.push(current);
|
|
18439
18661
|
return out;
|
|
18440
18662
|
}
|
|
18663
|
+
function ageOf(iso, now) {
|
|
18664
|
+
const seconds = Math.max(0, Math.floor((now.getTime() - Date.parse(iso)) / 1e3));
|
|
18665
|
+
if (Number.isNaN(seconds)) return "an unknown time";
|
|
18666
|
+
if (seconds < 3600) return `${Math.max(1, Math.floor(seconds / 60))}m`;
|
|
18667
|
+
if (seconds < 72 * 3600) return `${Math.floor(seconds / 3600)}h`;
|
|
18668
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
18669
|
+
}
|
|
18441
18670
|
|
|
18442
18671
|
// src/commands/product.ts
|
|
18443
18672
|
async function runProduct(parsed) {
|
|
@@ -18998,10 +19227,13 @@ async function runWhoami(parsed) {
|
|
|
18998
19227
|
}
|
|
18999
19228
|
const { principal, access } = result.data;
|
|
19000
19229
|
const lines = [`${principal.name} ${principal.id} (${principal.type})`];
|
|
19001
|
-
if (access.
|
|
19230
|
+
if (access.tenantPermissions.length > 0) {
|
|
19002
19231
|
lines.push("", "Granted across the whole organization");
|
|
19003
19232
|
lines.push(` ${access.tenantPermissions.join(", ")}`);
|
|
19004
|
-
}
|
|
19233
|
+
}
|
|
19234
|
+
if (access.projects.length > 0 && access.tenantPermissions.length > 0) {
|
|
19235
|
+
lines.push("", "Granted in these Projects");
|
|
19236
|
+
} else if (!access.tenantWide) {
|
|
19005
19237
|
lines.push("", "Granted in these Projects only \u2014 nothing organization-wide.");
|
|
19006
19238
|
lines.push(" Listing these works. Reading ACROSS the organization does not.");
|
|
19007
19239
|
}
|
|
@@ -19009,7 +19241,7 @@ async function runWhoami(parsed) {
|
|
|
19009
19241
|
lines.push("", ` ${project.projectId}`);
|
|
19010
19242
|
lines.push(` ${project.permissions.join(", ")}`);
|
|
19011
19243
|
}
|
|
19012
|
-
if (
|
|
19244
|
+
if (access.projects.length === 0 && !access.tenantPermissions.includes("project.read")) {
|
|
19013
19245
|
lines.push("", " No Projects. Ask an organization administrator for access.");
|
|
19014
19246
|
}
|
|
19015
19247
|
lines.push(
|
|
@@ -19044,6 +19276,7 @@ var USAGE = `vfac \u2014 deploy Varde Flyt resources from a manifest
|
|
|
19044
19276
|
vfac get product-instances
|
|
19045
19277
|
vfac get product-instance pri_\u2026 what it is, and its outputs
|
|
19046
19278
|
vfac get operation op_\u2026
|
|
19279
|
+
vfac get operations [--resource pri_\u2026] newest first \u2014 find an operation id again
|
|
19047
19280
|
vfac lifecycle <suspend|resume|upgrade|reconcile|delete> pri_\u2026
|
|
19048
19281
|
delete is irreversible and needs --yes
|
|
19049
19282
|
vfac export product-instance pri_\u2026
|
|
@@ -19061,7 +19294,10 @@ Options
|
|
|
19061
19294
|
--secret inputs the apply will carry; without them the plan
|
|
19062
19295
|
answers about a different request. A value on the command line
|
|
19063
19296
|
is in your shell history and in the process table, so it is
|
|
19064
|
-
refused
|
|
19297
|
+
refused. A value for a secret setting written into the
|
|
19298
|
+
manifest itself is refused too: before the file is sent when it
|
|
19299
|
+
pins the release the catalog offers, by the platform otherwise
|
|
19300
|
+
--resource get operations: only that resource's history
|
|
19065
19301
|
--product which Product to write a starting manifest for
|
|
19066
19302
|
--name what a person will call the resource
|
|
19067
19303
|
--key its stable identity. Derived from --name when omitted
|
|
@@ -19089,8 +19325,10 @@ Reading the catalogue
|
|
|
19089
19325
|
Products are read UNDER a Project, because that is the scope a credential is
|
|
19090
19326
|
granted in. Pass --project, or store one with: vfac context set --project prj_\u2026
|
|
19091
19327
|
|
|
19092
|
-
project list and product
|
|
19093
|
-
cursor. Pass --cursor or --page-size only when you want
|
|
19328
|
+
project list, product list and get product-instances read the WHOLE list,
|
|
19329
|
+
following the server's cursor. Pass --cursor or --page-size only when you want
|
|
19330
|
+
a single page. get operations reads ONE page, newest first \u2014 a history only
|
|
19331
|
+
grows \u2014 and prints the --cursor that continues into older operations.
|
|
19094
19332
|
|
|
19095
19333
|
Authentication
|
|
19096
19334
|
As yourself: vfac login \u2014 approve the code in your browser. The terminal then
|