mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-22 08:06:42 +00:00
feat(stacks): per-stack environment inventory and secret-safe guardrails (#1397)
* feat(stacks): per-stack environment inventory and secret-safe guardrails
Add an Environment tab to Stack Anatomy that derives a per-stack inventory
of environment variables from the compose files and env files. Each variable
shows its source, whether Compose interpolates it or injects it into a
container, and a status (present, missing, unused, duplicate, or shell-only),
plus likely-secret classification. The inventory works from variable names
only: a value is never read, returned, or logged, and a likely secret shows
presence only. A copy env checklist action exports names and status without
values.
Surface a missing required env_file as a Compose Doctor preflight finding,
and add an opt-in node setting that refuses a deploy or update when a
required ${VAR:?...} variable is unset or empty, before any backup, pull, or
up runs. Default off.
The Environment tab is capability-gated so it hides on older remote nodes.
* fix(stacks): harden env-file reader against a stat-then-open race
Open the env-file handle first and fstat the open handle instead of
stat-ing the path before opening, removing the check-then-use window in
readEnvFileKeys. Use a secure mkdtemp directory for the out-of-base test
path instead of a predictable name in the temp root.
* fix(stacks): resolve nested env_file paths per compose file, reconcile inline keys per service
Resolve each env_file relative to the directory of the compose file that
declared it, so a nested multi-file Git override (infra/prod.yml referencing
./prod.env) lands next to that file instead of the stack root. The root
compose file is unaffected, since its directory is the stack directory.
Reconcile inline environment provenance per service, so a key an override
removed from one service's effective env is not labeled compose-inline just
because another service injects the same name from a different source.
This commit is contained in:
@@ -87,6 +87,21 @@ const envUnset: PreflightRule = {
|
||||
},
|
||||
};
|
||||
|
||||
const envFileMissing: PreflightRule = {
|
||||
id: 'env-file-missing',
|
||||
run(ctx) {
|
||||
return ctx.missingEnvFiles.map(f => ({
|
||||
ruleId: 'env-file-missing',
|
||||
severity: 'high' as const,
|
||||
title: `Missing env file ${f.rawPath}`,
|
||||
message: `The Compose file declares env_file "${f.rawPath}"${f.services.length ? ` for service ${f.services.join(', ')}` : ''}, but no such file exists in the stack directory. Compose fails to start the stack when a required env_file is absent.`,
|
||||
sourcePath: f.rawPath,
|
||||
remediation: `Create ${f.rawPath} in the stack directory, fix the path, or mark the entry optional with "required: false".`,
|
||||
service: f.services[0],
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
const portConflictNode: PreflightRule = {
|
||||
id: 'port-conflict-node',
|
||||
run(ctx) {
|
||||
@@ -671,6 +686,7 @@ const sensitiveServiceBroadExposure: PreflightRule = {
|
||||
export const PREFLIGHT_RULES: PreflightRule[] = [
|
||||
renderFailed,
|
||||
envUnset,
|
||||
envFileMissing,
|
||||
portConflictNode,
|
||||
portConflictInternal,
|
||||
portExposedAllInterfaces,
|
||||
|
||||
@@ -43,6 +43,12 @@ export interface PreflightReport {
|
||||
findings: PreflightFinding[];
|
||||
}
|
||||
|
||||
/** A declared `env_file:` that is required and absent on disk (names only). */
|
||||
export interface MissingEnvFile {
|
||||
rawPath: string;
|
||||
services: string[];
|
||||
}
|
||||
|
||||
/** A host port bound by a running container on the target node. */
|
||||
export interface NodePortBinding {
|
||||
publishedPort: number;
|
||||
@@ -83,6 +89,8 @@ export interface PreflightContext {
|
||||
renderError: string | null;
|
||||
/** Variable names Compose reported as unset (defaulted to empty string). */
|
||||
unsetEnvVars: string[];
|
||||
/** Declared `env_file:` paths that are required but absent on disk (names only). */
|
||||
missingEnvFiles: MissingEnvFile[];
|
||||
/** Service names parsed from the literal source file (pre-render). */
|
||||
sourceServiceNames: string[];
|
||||
/** Whether the source file could be read; gates source-derived checks so an
|
||||
|
||||
Reference in New Issue
Block a user