composable-data-stack 0.4.0__py3-none-any.whl
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.
- cli/__init__.py +1 -0
- cli/diagnostics.py +13 -0
- cli/graph.py +58 -0
- cli/image_updates.py +326 -0
- cli/image_verification.py +484 -0
- cli/loader.py +180 -0
- cli/main.py +1656 -0
- cli/overlay.py +239 -0
- cli/planner.py +618 -0
- cli/preflight.py +418 -0
- cli/renderer.py +791 -0
- cli/resolver.py +28 -0
- cli/resources/__init__.py +1 -0
- cli/resources/rule-schema.json +274 -0
- cli/resources/rule-set.json +919 -0
- cli/secrets.py +169 -0
- cli/security.py +768 -0
- cli/security_common.py +41 -0
- cli/state.py +112 -0
- cli/up_runner.py +257 -0
- cli/validator.py +570 -0
- composable_data_stack-0.4.0.dist-info/METADATA +872 -0
- composable_data_stack-0.4.0.dist-info/RECORD +27 -0
- composable_data_stack-0.4.0.dist-info/WHEEL +5 -0
- composable_data_stack-0.4.0.dist-info/entry_points.txt +2 -0
- composable_data_stack-0.4.0.dist-info/licenses/LICENSE +201 -0
- composable_data_stack-0.4.0.dist-info/top_level.txt +1 -0
cli/secrets.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secret resolution from .env files and environment.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .diagnostics import Diagnostic
|
|
11
|
+
|
|
12
|
+
def load_secrets_from_env(env_file: Path | None = None) -> tuple[dict[str, str], list[Diagnostic]]:
|
|
13
|
+
"""
|
|
14
|
+
Load CDS_* secrets from .env file and environment variables.
|
|
15
|
+
|
|
16
|
+
Only keys prefixed with CDS_ are loaded — arbitrary .env keys are ignored.
|
|
17
|
+
Profile-defined logical aliases (spec.secrets.values) are handled separately
|
|
18
|
+
by load_profile_secrets and are the sole source of non-CDS_ keys in secrets.
|
|
19
|
+
|
|
20
|
+
Priority (lowest to highest):
|
|
21
|
+
1. .env file (if provided)
|
|
22
|
+
2. Environment variables
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
env_file: Optional path to .env file. If None, looks for .env in current directory.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (secrets_dict, diagnostics)
|
|
29
|
+
"""
|
|
30
|
+
diagnostics: list[Diagnostic] = []
|
|
31
|
+
secrets: dict[str, str] = {}
|
|
32
|
+
|
|
33
|
+
if env_file is None:
|
|
34
|
+
env_file = Path(".env")
|
|
35
|
+
else:
|
|
36
|
+
env_file = Path(env_file)
|
|
37
|
+
|
|
38
|
+
if env_file.exists():
|
|
39
|
+
try:
|
|
40
|
+
with open(env_file, encoding="utf-8-sig") as f:
|
|
41
|
+
for line_num, line in enumerate(f, 1):
|
|
42
|
+
line = line.rstrip("\n\r")
|
|
43
|
+
|
|
44
|
+
if not line or line.startswith("#"):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if "=" not in line:
|
|
48
|
+
diagnostics.append(
|
|
49
|
+
Diagnostic(
|
|
50
|
+
level="warning",
|
|
51
|
+
code="W090",
|
|
52
|
+
message=f'Invalid .env line format: "{line}" (expected KEY=VALUE)',
|
|
53
|
+
path=f"{env_file}:{line_num}",
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
key, _, value = line.partition("=")
|
|
59
|
+
key = key.strip()
|
|
60
|
+
value = value.strip()
|
|
61
|
+
|
|
62
|
+
if (value.startswith('"') and value.endswith('"')) or \
|
|
63
|
+
(value.startswith("'") and value.endswith("'")):
|
|
64
|
+
value = value[1:-1]
|
|
65
|
+
|
|
66
|
+
# Only accept CDS_* keys from .env — logical aliases come from the profile
|
|
67
|
+
if key and key.startswith("CDS_"):
|
|
68
|
+
secrets[key] = value
|
|
69
|
+
|
|
70
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
71
|
+
diagnostics.append(
|
|
72
|
+
Diagnostic(
|
|
73
|
+
level="error",
|
|
74
|
+
code="E080",
|
|
75
|
+
message=f"Failed to read .env file: {e}",
|
|
76
|
+
path=str(env_file),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
return {}, diagnostics
|
|
80
|
+
|
|
81
|
+
# Environment variables — already CDS_* filtered, no change needed
|
|
82
|
+
for key, value in os.environ.items():
|
|
83
|
+
if key.startswith("CDS_"):
|
|
84
|
+
secrets[key] = value
|
|
85
|
+
|
|
86
|
+
return secrets, diagnostics
|
|
87
|
+
|
|
88
|
+
def resolve_secret(key: str, secrets: dict[str, str], required: bool = False) -> tuple[str | None, Diagnostic | None]:
|
|
89
|
+
"""
|
|
90
|
+
Resolve a single secret by key.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
key: Secret key (e.g., "CDS_ANALYTICS_POSTGRES_PASSWORD")
|
|
94
|
+
secrets: Dictionary of available secrets
|
|
95
|
+
required: If True, emit error if secret is missing
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Tuple of (value, diagnostic). Value is None if not found.
|
|
99
|
+
"""
|
|
100
|
+
if key in secrets:
|
|
101
|
+
return secrets[key], None
|
|
102
|
+
|
|
103
|
+
if required:
|
|
104
|
+
return None, Diagnostic(
|
|
105
|
+
level="error",
|
|
106
|
+
code="E081",
|
|
107
|
+
message=f'Required secret "{key}" not found in .env or environment',
|
|
108
|
+
path=f"secrets.{key}",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return None, None
|
|
112
|
+
|
|
113
|
+
def load_profile_secrets(
|
|
114
|
+
spec_secrets: dict[str, Any] | None,
|
|
115
|
+
env_file: Path | None = None,
|
|
116
|
+
) -> tuple[dict[str, str], list[Diagnostic]]:
|
|
117
|
+
"""
|
|
118
|
+
Returns:
|
|
119
|
+
secrets dict where:
|
|
120
|
+
- CDS_* keys map to themselves (e.g. CDS_DB_PASSWORD -> CDS_DB_PASSWORD)
|
|
121
|
+
- logical alias keys map to their CDS_* env var NAME (not value)
|
|
122
|
+
"""
|
|
123
|
+
diagnostics: list[Diagnostic] = []
|
|
124
|
+
raw_secrets, secret_diags = load_secrets_from_env(env_file)
|
|
125
|
+
diagnostics.extend(secret_diags)
|
|
126
|
+
|
|
127
|
+
# Keep only env variable names in the returned mapping; never include values.
|
|
128
|
+
secrets: dict[str, str] = {name: name for name in raw_secrets.keys()}
|
|
129
|
+
|
|
130
|
+
if not isinstance(spec_secrets, dict):
|
|
131
|
+
return secrets, diagnostics
|
|
132
|
+
|
|
133
|
+
values = spec_secrets.get("values", {})
|
|
134
|
+
if not isinstance(values, dict):
|
|
135
|
+
return secrets, diagnostics
|
|
136
|
+
|
|
137
|
+
for secret_name, secret_def in values.items():
|
|
138
|
+
if not isinstance(secret_def, dict):
|
|
139
|
+
diagnostics.append(Diagnostic(
|
|
140
|
+
level="error", code="E082",
|
|
141
|
+
message=f'Secret definition "{secret_name}" must be an object.',
|
|
142
|
+
path=f"spec.secrets.values.{secret_name}",
|
|
143
|
+
))
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
env_name = secret_def.get("env")
|
|
147
|
+
required = secret_def.get("required", False)
|
|
148
|
+
|
|
149
|
+
if not isinstance(env_name, str) or not env_name:
|
|
150
|
+
diagnostics.append(Diagnostic(
|
|
151
|
+
level="error", code="E082",
|
|
152
|
+
message=f'Secret definition "{secret_name}" must include a valid env name.',
|
|
153
|
+
path=f"spec.secrets.values.{secret_name}.env",
|
|
154
|
+
))
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
if env_name not in raw_secrets:
|
|
158
|
+
if required:
|
|
159
|
+
diagnostics.append(Diagnostic(
|
|
160
|
+
level="error", code="E081",
|
|
161
|
+
message=f'Required secret "{env_name}" not found in environment.',
|
|
162
|
+
path=f"spec.secrets.values.{secret_name}",
|
|
163
|
+
))
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Map logical name → CDS_* var name, NOT the value.
|
|
167
|
+
secrets[secret_name] = env_name # e.g. "analytics_postgres_password" → "CDS_ANALYTICS_POSTGRES_PASSWORD"
|
|
168
|
+
|
|
169
|
+
return secrets, diagnostics
|