@monster-orange/den 0.0.0 → 0.2.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/CHANGELOG.md +204 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +68 -0
- package/docs/distributions.md +499 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +101 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +403 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +124 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +757 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/agent-actor.ts +77 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +688 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/status.ts +74 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +635 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +108 -0
- package/src/shared/environment-manager/log-tail.ts +213 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +335 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-container.ts +47 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1603 -0
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +326 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/merge-override.ts +75 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/placement.ts +321 -0
- package/src/shared/manifest/read-manifest.ts +278 -0
- package/src/shared/manifest/schema.ts +313 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +200 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +190 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/redact.ts +107 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +118 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/status.ts +99 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +701 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +148 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +81 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +570 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3742 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +117 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1319 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/mcp/api.ts +284 -0
- package/src/ui/mcp/guards.ts +141 -0
- package/src/ui/mcp/server.ts +147 -0
- package/src/ui/mcp/tools.ts +565 -0
- package/src/ui/server/audit.ts +71 -0
- package/src/ui/server/changelog-parser.ts +105 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3350 -0
- package/src/ui/server/log-history.ts +453 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +496 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +976 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +1373 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +5344 -0
- package/src/ui/server/public/logs.html +1496 -0
- package/src/ui/server/public/panel.js +787 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1103 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/readonly-routes.ts +200 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/ui-assets.ts +73 -0
- package/src/ui/server/workspace-routes.ts +2185 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-secrets readiness check.
|
|
3
|
+
*
|
|
4
|
+
* Repos scaffolded by stack that declare `config.local.sidecars[].secrets`
|
|
5
|
+
* ship a COMMITTED, generated `.stack-secrets.example.yml` listing every
|
|
6
|
+
* secret they need, and expect a gitignored `.stack-secrets.yml` beside it
|
|
7
|
+
* with real values. That example file is the whole contract — den reads it
|
|
8
|
+
* generically and needs no per-repo knowledge.
|
|
9
|
+
*
|
|
10
|
+
* Why this gate exists: a sidecar's generated `up.sh` validates only that each
|
|
11
|
+
* secret is PRESENT and NON-EMPTY, so the placeholder value (`replace-me`)
|
|
12
|
+
* sails through. Starting a stack that way is not a harmless no-op — a
|
|
13
|
+
* database container bakes the password it is first started with into its data
|
|
14
|
+
* volume at initdb time, and no later edit of the secrets file changes it. The
|
|
15
|
+
* result is a stack that looks healthy (liveness probes don't authenticate)
|
|
16
|
+
* while every consumer fails to connect. Refusing to start is the only cheap
|
|
17
|
+
* moment to prevent it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "fs";
|
|
21
|
+
import * as path from "path";
|
|
22
|
+
import * as yaml from "yaml";
|
|
23
|
+
import { getDistRoot } from "../paths.js";
|
|
24
|
+
import { getReposRoot } from "../den-config.js";
|
|
25
|
+
|
|
26
|
+
import { product } from "../product.js";
|
|
27
|
+
/** stack's generated placeholder, plus the `<angle-bracket>` fill-me-in convention. */
|
|
28
|
+
const PLACEHOLDER_RE = /^(replace-me|<[^>]*>)$/;
|
|
29
|
+
|
|
30
|
+
const EXAMPLE_FILE = ".stack-secrets.example.yml";
|
|
31
|
+
const SECRETS_FILE = ".stack-secrets.yml";
|
|
32
|
+
|
|
33
|
+
export interface SecretsCheckResult {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
/** Absolute path to the secrets file the engineer needs to fill in. */
|
|
36
|
+
secretsPath: string;
|
|
37
|
+
/** Absolute path to the committed example that declares the contract. */
|
|
38
|
+
examplePath: string;
|
|
39
|
+
/** True when `.stack-secrets.yml` doesn't exist at all. */
|
|
40
|
+
fileMissing: boolean;
|
|
41
|
+
/** Declared keys absent from the secrets file (names only — never values). */
|
|
42
|
+
missingKeys: string[];
|
|
43
|
+
/** Declared keys still holding a placeholder or empty value. */
|
|
44
|
+
placeholderKeys: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseFlatYaml(file: string): Record<string, unknown> | null {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = yaml.parse(fs.readFileSync(file, "utf-8")) as unknown;
|
|
50
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return parsed as Record<string, unknown>;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check a repo's local secrets against its declared contract.
|
|
61
|
+
*
|
|
62
|
+
* Returns `null` when the repo declares no contract (no example file) — i.e.
|
|
63
|
+
* most repos — so callers can treat "no contract" as "nothing to enforce".
|
|
64
|
+
*/
|
|
65
|
+
export function checkRepoSecrets(repoPath: string): SecretsCheckResult | null {
|
|
66
|
+
const examplePath = path.join(repoPath, EXAMPLE_FILE);
|
|
67
|
+
const secretsPath = path.join(repoPath, SECRETS_FILE);
|
|
68
|
+
if (!fs.existsSync(examplePath)) return null;
|
|
69
|
+
|
|
70
|
+
const declared = parseFlatYaml(examplePath);
|
|
71
|
+
if (!declared) return null; // unparseable contract — don't invent failures
|
|
72
|
+
const keys = Object.keys(declared);
|
|
73
|
+
if (keys.length === 0) return null;
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(secretsPath)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
secretsPath,
|
|
79
|
+
examplePath,
|
|
80
|
+
fileMissing: true,
|
|
81
|
+
missingKeys: keys,
|
|
82
|
+
placeholderKeys: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const actual = parseFlatYaml(secretsPath) ?? {};
|
|
87
|
+
const missingKeys: string[] = [];
|
|
88
|
+
const placeholderKeys: string[] = [];
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
if (!(key in actual)) {
|
|
91
|
+
missingKeys.push(key);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const value = actual[key];
|
|
95
|
+
const text = value == null ? "" : String(value).trim();
|
|
96
|
+
if (text === "" || PLACEHOLDER_RE.test(text)) placeholderKeys.push(key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
ok: missingKeys.length === 0 && placeholderKeys.length === 0,
|
|
101
|
+
secretsPath,
|
|
102
|
+
examplePath,
|
|
103
|
+
fileMissing: false,
|
|
104
|
+
missingKeys,
|
|
105
|
+
placeholderKeys,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Other checkouts of the same repo on this machine, in the order
|
|
111
|
+
* `readManifestAnyBranch` already established: other managed branch
|
|
112
|
+
* clones first, then the engineer's own sibling checkout.
|
|
113
|
+
*/
|
|
114
|
+
function otherCheckouts(repoName: string, excludePath: string): string[] {
|
|
115
|
+
const out: string[] = [];
|
|
116
|
+
const managedRoot = path.join(
|
|
117
|
+
getDistRoot(),
|
|
118
|
+
`${product().servicesDir}`,
|
|
119
|
+
repoName,
|
|
120
|
+
);
|
|
121
|
+
try {
|
|
122
|
+
for (const entry of fs.readdirSync(managedRoot, { withFileTypes: true })) {
|
|
123
|
+
if (entry.isDirectory()) out.push(path.join(managedRoot, entry.name));
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
/* no managed clones */
|
|
127
|
+
}
|
|
128
|
+
out.push(path.join(getReposRoot(), repoName));
|
|
129
|
+
const exclude = path.resolve(excludePath);
|
|
130
|
+
return out.filter((p) => path.resolve(p) !== exclude);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface SecretsSeedResult {
|
|
134
|
+
/** Keys filled in from the donor (names only — never values). */
|
|
135
|
+
filled: string[];
|
|
136
|
+
/** Checkout the values came from. */
|
|
137
|
+
donorPath: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Fill a freshly cloned repo's secrets file from ANOTHER checkout of the SAME
|
|
142
|
+
* repo on this machine.
|
|
143
|
+
*
|
|
144
|
+
* A managed clone starts with stack's placeholders, which blocks `make
|
|
145
|
+
* setup` and would poison containers if started — while the engineer's own
|
|
146
|
+
* sibling checkout of that same repo usually already holds every value. Copying
|
|
147
|
+
* them is exactly what they would do by hand, so do it for them and say so.
|
|
148
|
+
*
|
|
149
|
+
* Conservative by construction: only keys DECLARED by the contract, only when
|
|
150
|
+
* unset in the target, and only from donor values that are themselves real.
|
|
151
|
+
* Never overwrites an existing value.
|
|
152
|
+
*/
|
|
153
|
+
export function seedSecretsFromSibling(
|
|
154
|
+
repoName: string,
|
|
155
|
+
repoPath: string,
|
|
156
|
+
): SecretsSeedResult | null {
|
|
157
|
+
const check = checkRepoSecrets(repoPath);
|
|
158
|
+
if (!check || check.ok) return null;
|
|
159
|
+
|
|
160
|
+
const wanted = new Set([...check.missingKeys, ...check.placeholderKeys]);
|
|
161
|
+
for (const donorPath of otherCheckouts(repoName, repoPath)) {
|
|
162
|
+
const donorFile = path.join(donorPath, SECRETS_FILE);
|
|
163
|
+
if (!fs.existsSync(donorFile)) continue;
|
|
164
|
+
const donor = parseFlatYaml(donorFile);
|
|
165
|
+
if (!donor) continue;
|
|
166
|
+
|
|
167
|
+
const fill = new Map<string, string>();
|
|
168
|
+
for (const key of wanted) {
|
|
169
|
+
const raw = donor[key];
|
|
170
|
+
const text = raw == null ? "" : String(raw).trim();
|
|
171
|
+
if (text === "" || PLACEHOLDER_RE.test(text)) continue;
|
|
172
|
+
fill.set(key, text);
|
|
173
|
+
}
|
|
174
|
+
if (fill.size === 0) continue;
|
|
175
|
+
|
|
176
|
+
writeSecretValues(check.secretsPath, check.examplePath, fill);
|
|
177
|
+
return { filled: [...fill.keys()].sort(), donorPath };
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Write values into a flat `KEY: VALUE` secrets file, line-wise so the
|
|
184
|
+
* generated `# Used by:` comments and key order survive. Creates the file from
|
|
185
|
+
* the example when absent.
|
|
186
|
+
*/
|
|
187
|
+
function writeSecretValues(
|
|
188
|
+
secretsPath: string,
|
|
189
|
+
examplePath: string,
|
|
190
|
+
values: Map<string, string>,
|
|
191
|
+
): void {
|
|
192
|
+
const source = fs.existsSync(secretsPath) ? secretsPath : examplePath;
|
|
193
|
+
const lines = fs.readFileSync(source, "utf-8").split("\n");
|
|
194
|
+
const remaining = new Map(values);
|
|
195
|
+
|
|
196
|
+
const updated = lines.map((line) => {
|
|
197
|
+
const m = /^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*:\s*)(.*)$/.exec(line);
|
|
198
|
+
if (!m) return line;
|
|
199
|
+
const key = m[2];
|
|
200
|
+
if (!remaining.has(key)) return line;
|
|
201
|
+
const value = remaining.get(key)!;
|
|
202
|
+
remaining.delete(key);
|
|
203
|
+
return `${m[1]}${key}: ${quoteIfNeeded(value)}`;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Declared keys the file never listed (e.g. a hand-trimmed file).
|
|
207
|
+
for (const [key, value] of remaining) {
|
|
208
|
+
updated.push(`${key}: ${quoteIfNeeded(value)}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fs.writeFileSync(secretsPath, updated.join("\n"), { mode: 0o600 });
|
|
212
|
+
// `mode` above only applies when the file is CREATED — an existing file keeps
|
|
213
|
+
// its old permissions, so tighten explicitly. This file holds live credentials.
|
|
214
|
+
try {
|
|
215
|
+
fs.chmodSync(secretsPath, 0o600);
|
|
216
|
+
} catch {
|
|
217
|
+
/* best-effort — a read-only FS shouldn't fail the seed */
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Quote values the flat-file readers would otherwise mangle. `up.sh`'s awk
|
|
223
|
+
* strips a trailing ` #…` before unquoting, so a value containing " #" cannot
|
|
224
|
+
* round-trip through that parser at all — left as-is rather than silently
|
|
225
|
+
* corrupted, since real secrets don't contain it.
|
|
226
|
+
*/
|
|
227
|
+
function quoteIfNeeded(value: string): string {
|
|
228
|
+
if (!/[:#'"]|^\s|\s$/.test(value)) return value;
|
|
229
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Operator-facing explanation of a failed check. Key names only — never values.
|
|
234
|
+
* Leads with the absolute path because the file lives inside a managed
|
|
235
|
+
* clone, which is not somewhere an engineer would think to look.
|
|
236
|
+
*/
|
|
237
|
+
export function describeSecretsCheck(
|
|
238
|
+
repoName: string,
|
|
239
|
+
result: SecretsCheckResult,
|
|
240
|
+
): string[] {
|
|
241
|
+
const lines: string[] = [];
|
|
242
|
+
const unset = [...result.missingKeys, ...result.placeholderKeys];
|
|
243
|
+
lines.push(
|
|
244
|
+
result.fileMissing
|
|
245
|
+
? `${repoName}: local secrets file has not been created yet`
|
|
246
|
+
: `${repoName}: ${unset.length} local secret(s) still unset — ${unset.join(", ")}`,
|
|
247
|
+
);
|
|
248
|
+
lines.push(` file: ${result.secretsPath}`);
|
|
249
|
+
lines.push(` declared: ${result.examplePath}`);
|
|
250
|
+
lines.push(
|
|
251
|
+
` Starting now would initialize containers with placeholder credentials.`,
|
|
252
|
+
);
|
|
253
|
+
lines.push(
|
|
254
|
+
` A database volume keeps the password it is FIRST created with, so this`,
|
|
255
|
+
);
|
|
256
|
+
lines.push(
|
|
257
|
+
` is not self-correcting — fill the values in, then run Sync again.`,
|
|
258
|
+
);
|
|
259
|
+
return lines;
|
|
260
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The running container that holds a service's logs. A compose-stack service
|
|
3
|
+
* runs as `<stack>-<workspace>-<service>-1` (the stacks naming helpers); any
|
|
4
|
+
* other container-run service is found by opctl's naming convention.
|
|
5
|
+
*/
|
|
6
|
+
import { readManifest } from "../manifest/read-manifest.js";
|
|
7
|
+
import { composeServiceContainers } from "../stacks/manifest-commands.js";
|
|
8
|
+
import { resolveStackWorkspaces } from "../stacks/status.js";
|
|
9
|
+
import { findConventionalContainer, runningDockerNames } from "./opctl.js";
|
|
10
|
+
|
|
11
|
+
/** The stack's container for `subsystem` when one is running, else null. */
|
|
12
|
+
export function findStackServiceContainer(
|
|
13
|
+
repoName: string,
|
|
14
|
+
repoPath: string | undefined,
|
|
15
|
+
subsystem: string | undefined,
|
|
16
|
+
): string | null {
|
|
17
|
+
if (!repoPath || !subsystem) return null;
|
|
18
|
+
const manifest = readManifest(repoPath);
|
|
19
|
+
if (!manifest?.compose) return null;
|
|
20
|
+
let candidates: string[];
|
|
21
|
+
let running: string[];
|
|
22
|
+
try {
|
|
23
|
+
const { stackWorkspace } = resolveStackWorkspaces();
|
|
24
|
+
candidates = composeServiceContainers(
|
|
25
|
+
manifest,
|
|
26
|
+
subsystem,
|
|
27
|
+
repoName,
|
|
28
|
+
stackWorkspace,
|
|
29
|
+
);
|
|
30
|
+
running = runningDockerNames();
|
|
31
|
+
} catch {
|
|
32
|
+
return null; // bad workspace name, or docker unavailable
|
|
33
|
+
}
|
|
34
|
+
return candidates.find((c) => running.includes(c)) ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A service's running container: its compose stack's, else opctl's. */
|
|
38
|
+
export function findServiceContainer(
|
|
39
|
+
repoName: string,
|
|
40
|
+
repoPath?: string,
|
|
41
|
+
subsystem?: string,
|
|
42
|
+
): string | null {
|
|
43
|
+
return (
|
|
44
|
+
findStackServiceContainer(repoName, repoPath, subsystem) ??
|
|
45
|
+
findConventionalContainer(repoName, repoPath, subsystem)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared environment for service commands.
|
|
3
|
+
*
|
|
4
|
+
* Some repos read shared locations from environment variables (e.g. a
|
|
5
|
+
* shared-infra repo's secrets file, defaulting to a sibling path like
|
|
6
|
+
* `../infra/.secrets.yml`). A sibling default breaks when the consumer is a
|
|
7
|
+
* nested managed clone, because `../infra` no longer points at that repo.
|
|
8
|
+
*
|
|
9
|
+
* Rather than patch each repo, den injects the resolved values into every
|
|
10
|
+
* service command (`up`/`down`/`restart`/`setup`). The mapping comes from the
|
|
11
|
+
* org config's `service_env` (`KEY: "{repo:<name>}/<path>"`); den has no
|
|
12
|
+
* built-in entries.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readDenConfig } from "../den-config.js";
|
|
16
|
+
import { resolveRepoPath } from "./derivation/derive-graph.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Built-in shared env templates: none. Config (`service_env`) is merged over
|
|
20
|
+
* this; a key set to "" is skipped.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_SERVICE_ENV: Record<string, string> = {};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Expand `{repo:<name>}` placeholders to the absolute path den resolves
|
|
26
|
+
* `<name>` to (managed clone or dev sibling). Returns null when any placeholder
|
|
27
|
+
* can't be resolved, so the caller skips that variable entirely.
|
|
28
|
+
*/
|
|
29
|
+
export function expandServiceEnvTemplate(
|
|
30
|
+
template: string,
|
|
31
|
+
resolve: (repoName: string) => string | null = resolveRepoPath,
|
|
32
|
+
): string | null {
|
|
33
|
+
const tokens = [...template.matchAll(/\{repo:([^}]+)\}/g)];
|
|
34
|
+
let out = template;
|
|
35
|
+
for (const match of tokens) {
|
|
36
|
+
const repoPath = resolve(match[1]);
|
|
37
|
+
if (!repoPath) return null;
|
|
38
|
+
out = out.split(match[0]).join(repoPath);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Repos that SUPPLY shared values to other repos, derived from the `{repo:X}`
|
|
45
|
+
* tokens in the (config-merged) service_env templates.
|
|
46
|
+
*
|
|
47
|
+
* These are load-bearing for every consumer: if such a repo's own local config
|
|
48
|
+
* is unusable, starting the repos that read from it produces a fleet configured
|
|
49
|
+
* against placeholder values. Config-derived, so den needs no hardcoded
|
|
50
|
+
* knowledge of which repo happens to own the shared secrets.
|
|
51
|
+
*/
|
|
52
|
+
export function serviceEnvProviderRepos(
|
|
53
|
+
readConfig: () =>
|
|
54
|
+
| { service_env?: Record<string, string> }
|
|
55
|
+
| null
|
|
56
|
+
| undefined = readDenConfig,
|
|
57
|
+
): Set<string> {
|
|
58
|
+
const providers = new Set<string>();
|
|
59
|
+
try {
|
|
60
|
+
const templates: Record<string, string> = {
|
|
61
|
+
...DEFAULT_SERVICE_ENV,
|
|
62
|
+
...(readConfig()?.service_env ?? {}),
|
|
63
|
+
};
|
|
64
|
+
for (const template of Object.values(templates)) {
|
|
65
|
+
if (!template) continue; // an empty template is skipped
|
|
66
|
+
for (const match of template.matchAll(/\{repo:([^}]+)\}/g)) {
|
|
67
|
+
providers.add(match[1]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
/* best-effort — an unreadable config just means no known providers */
|
|
72
|
+
}
|
|
73
|
+
return providers;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the shared environment to inject into a service command. Never overrides
|
|
78
|
+
* a variable already present (and non-empty) in `baseEnv` — an explicit value in
|
|
79
|
+
* the caller's shell wins. Variables whose template can't be resolved are
|
|
80
|
+
* omitted (e.g. the providing repo is not in this workspace).
|
|
81
|
+
*/
|
|
82
|
+
export function buildServiceEnv(
|
|
83
|
+
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
84
|
+
resolve: (repoName: string) => string | null = resolveRepoPath,
|
|
85
|
+
readConfig: () =>
|
|
86
|
+
| { service_env?: Record<string, string> }
|
|
87
|
+
| null
|
|
88
|
+
| undefined = readDenConfig,
|
|
89
|
+
): NodeJS.ProcessEnv {
|
|
90
|
+
// Best-effort: injecting shared env must never break a service command, so any
|
|
91
|
+
// failure to read config / resolve a repo path degrades to "no shared env".
|
|
92
|
+
try {
|
|
93
|
+
const templates: Record<string, string> = {
|
|
94
|
+
...DEFAULT_SERVICE_ENV,
|
|
95
|
+
...(readConfig()?.service_env ?? {}),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const out: NodeJS.ProcessEnv = {};
|
|
99
|
+
for (const [key, template] of Object.entries(templates)) {
|
|
100
|
+
const existing = baseEnv[key];
|
|
101
|
+
if (existing != null && existing !== "") continue; // respect caller-set value
|
|
102
|
+
if (!template) continue; // an empty template is skipped
|
|
103
|
+
const value = expandServiceEnvTemplate(template, resolve);
|
|
104
|
+
if (value !== null) out[key] = value;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
} catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|