envdrift 4.2.1__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.
- envdrift/__init__.py +30 -0
- envdrift/_version.py +34 -0
- envdrift/api.py +192 -0
- envdrift/cli.py +42 -0
- envdrift/cli_commands/__init__.py +1 -0
- envdrift/cli_commands/diff.py +91 -0
- envdrift/cli_commands/encryption.py +630 -0
- envdrift/cli_commands/encryption_helpers.py +93 -0
- envdrift/cli_commands/hook.py +75 -0
- envdrift/cli_commands/init_cmd.py +117 -0
- envdrift/cli_commands/partial.py +222 -0
- envdrift/cli_commands/sync.py +1140 -0
- envdrift/cli_commands/validate.py +109 -0
- envdrift/cli_commands/vault.py +376 -0
- envdrift/cli_commands/version.py +15 -0
- envdrift/config.py +489 -0
- envdrift/constants.json +18 -0
- envdrift/core/__init__.py +30 -0
- envdrift/core/diff.py +233 -0
- envdrift/core/encryption.py +400 -0
- envdrift/core/parser.py +260 -0
- envdrift/core/partial_encryption.py +239 -0
- envdrift/core/schema.py +253 -0
- envdrift/core/validator.py +312 -0
- envdrift/encryption/__init__.py +117 -0
- envdrift/encryption/base.py +217 -0
- envdrift/encryption/dotenvx.py +236 -0
- envdrift/encryption/sops.py +458 -0
- envdrift/env_files.py +60 -0
- envdrift/integrations/__init__.py +21 -0
- envdrift/integrations/dotenvx.py +689 -0
- envdrift/integrations/precommit.py +266 -0
- envdrift/integrations/sops.py +85 -0
- envdrift/output/__init__.py +21 -0
- envdrift/output/rich.py +424 -0
- envdrift/py.typed +0 -0
- envdrift/sync/__init__.py +26 -0
- envdrift/sync/config.py +218 -0
- envdrift/sync/engine.py +383 -0
- envdrift/sync/operations.py +138 -0
- envdrift/sync/result.py +99 -0
- envdrift/vault/__init__.py +107 -0
- envdrift/vault/aws.py +282 -0
- envdrift/vault/azure.py +170 -0
- envdrift/vault/base.py +150 -0
- envdrift/vault/gcp.py +210 -0
- envdrift/vault/hashicorp.py +238 -0
- envdrift-4.2.1.dist-info/METADATA +160 -0
- envdrift-4.2.1.dist-info/RECORD +52 -0
- envdrift-4.2.1.dist-info/WHEEL +4 -0
- envdrift-4.2.1.dist-info/entry_points.txt +2 -0
- envdrift-4.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Pre-commit hook integration for envdrift."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Pre-commit hook configuration template
|
|
8
|
+
HOOK_CONFIG = """# envdrift pre-commit hooks
|
|
9
|
+
# Add this to your .pre-commit-config.yaml
|
|
10
|
+
|
|
11
|
+
repos:
|
|
12
|
+
- repo: local
|
|
13
|
+
hooks:
|
|
14
|
+
- id: envdrift-validate
|
|
15
|
+
name: Validate env files against schema
|
|
16
|
+
entry: envdrift validate --ci
|
|
17
|
+
language: system
|
|
18
|
+
files: ^\\.env\\.(production|staging|development)$
|
|
19
|
+
pass_filenames: true
|
|
20
|
+
description: Validates .env files match Pydantic schema
|
|
21
|
+
|
|
22
|
+
- id: envdrift-encryption
|
|
23
|
+
name: Check env encryption status
|
|
24
|
+
entry: envdrift encrypt --check
|
|
25
|
+
language: system
|
|
26
|
+
files: ^\\.env\\.(production|staging)$
|
|
27
|
+
pass_filenames: true
|
|
28
|
+
description: Ensures sensitive .env files are encrypted
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Minimal hook entry for injection
|
|
32
|
+
HOOK_ENTRY = {
|
|
33
|
+
"repo": "local",
|
|
34
|
+
"hooks": [
|
|
35
|
+
{
|
|
36
|
+
"id": "envdrift-validate",
|
|
37
|
+
"name": "Validate env files against schema",
|
|
38
|
+
"entry": "envdrift validate --ci",
|
|
39
|
+
"language": "system",
|
|
40
|
+
"files": r"^\.env\.(production|staging|development)$",
|
|
41
|
+
"pass_filenames": True,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "envdrift-encryption",
|
|
45
|
+
"name": "Check env encryption status",
|
|
46
|
+
"entry": "envdrift encrypt --check",
|
|
47
|
+
"language": "system",
|
|
48
|
+
"files": r"^\.env\.(production|staging)$",
|
|
49
|
+
"pass_filenames": True,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_hook_config() -> str:
|
|
56
|
+
"""
|
|
57
|
+
Provide the default pre-commit hook configuration template for envdrift.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The YAML string containing the pre-commit configuration for envdrift hooks.
|
|
61
|
+
"""
|
|
62
|
+
return HOOK_CONFIG
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_precommit_config(start_dir: Path | None = None) -> Path | None:
|
|
66
|
+
"""
|
|
67
|
+
Locate a .pre-commit-config.yaml file by searching the given directory and its parents.
|
|
68
|
+
|
|
69
|
+
Parameters:
|
|
70
|
+
start_dir (Path | None): Directory to start the search from. If None, the current working directory is used.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Path | None: Path to the first .pre-commit-config.yaml found while walking upward, or `None` if no file is found.
|
|
74
|
+
"""
|
|
75
|
+
if start_dir is None:
|
|
76
|
+
start_dir = Path.cwd()
|
|
77
|
+
|
|
78
|
+
current = start_dir.resolve()
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
config_path = current / ".pre-commit-config.yaml"
|
|
82
|
+
if config_path.exists():
|
|
83
|
+
return config_path
|
|
84
|
+
if current == current.parent:
|
|
85
|
+
# Reached filesystem root
|
|
86
|
+
break
|
|
87
|
+
current = current.parent
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def install_hooks(
|
|
93
|
+
config_path: Path | None = None,
|
|
94
|
+
create_if_missing: bool = True,
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""Install envdrift hooks to .pre-commit-config.yaml.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
config_path: Path to pre-commit config (auto-detected if None)
|
|
100
|
+
create_if_missing: Create config file if it doesn't exist
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if hooks were installed/updated
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
FileNotFoundError: If config not found and create_if_missing=False
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
import yaml
|
|
110
|
+
except ImportError:
|
|
111
|
+
raise ImportError(
|
|
112
|
+
"PyYAML is required for pre-commit integration. Install with: pip install pyyaml"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Find or create config
|
|
116
|
+
if config_path is None:
|
|
117
|
+
config_path = find_precommit_config()
|
|
118
|
+
|
|
119
|
+
if config_path is None:
|
|
120
|
+
if create_if_missing:
|
|
121
|
+
config_path = Path.cwd() / ".pre-commit-config.yaml"
|
|
122
|
+
else:
|
|
123
|
+
raise FileNotFoundError(
|
|
124
|
+
".pre-commit-config.yaml not found. "
|
|
125
|
+
"Run from repository root or specify --config path."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Load existing config or create new
|
|
129
|
+
if config_path.exists():
|
|
130
|
+
content = config_path.read_text()
|
|
131
|
+
config = yaml.safe_load(content) or {}
|
|
132
|
+
else:
|
|
133
|
+
config = {}
|
|
134
|
+
|
|
135
|
+
# Initialize repos list if needed
|
|
136
|
+
if "repos" not in config:
|
|
137
|
+
config["repos"] = []
|
|
138
|
+
|
|
139
|
+
# Check if envdrift hooks already exist
|
|
140
|
+
has_envdrift = False
|
|
141
|
+
for repo in config["repos"]:
|
|
142
|
+
if repo.get("repo") == "local":
|
|
143
|
+
hooks = repo.get("hooks", [])
|
|
144
|
+
for hook in hooks:
|
|
145
|
+
if hook.get("id", "").startswith("envdrift-"):
|
|
146
|
+
has_envdrift = True
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
# Add hooks if not present
|
|
150
|
+
if not has_envdrift:
|
|
151
|
+
# Find existing local repo or create new one
|
|
152
|
+
local_repo = None
|
|
153
|
+
for repo in config["repos"]:
|
|
154
|
+
if repo.get("repo") == "local":
|
|
155
|
+
local_repo = repo
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
if local_repo:
|
|
159
|
+
# Add to existing local repo
|
|
160
|
+
if "hooks" not in local_repo:
|
|
161
|
+
local_repo["hooks"] = []
|
|
162
|
+
local_repo["hooks"].extend(HOOK_ENTRY["hooks"])
|
|
163
|
+
else:
|
|
164
|
+
# Add new local repo entry
|
|
165
|
+
config["repos"].append(HOOK_ENTRY)
|
|
166
|
+
|
|
167
|
+
# Write config
|
|
168
|
+
with open(config_path, "w") as f:
|
|
169
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
170
|
+
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def uninstall_hooks(config_path: Path | None = None) -> bool:
|
|
175
|
+
"""
|
|
176
|
+
Remove any envdrift hooks from a .pre-commit-config.yaml file.
|
|
177
|
+
|
|
178
|
+
Parameters:
|
|
179
|
+
config_path (Path | None): Path to the pre-commit config file. If None, the repository tree is searched upward to locate .pre-commit-config.yaml.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
bool: `True` if one or more envdrift hooks were removed and the file was updated, `False` otherwise.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ImportError: If PyYAML is not available.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
import yaml
|
|
189
|
+
except ImportError:
|
|
190
|
+
raise ImportError("PyYAML is required for pre-commit integration.")
|
|
191
|
+
|
|
192
|
+
if config_path is None:
|
|
193
|
+
config_path = find_precommit_config()
|
|
194
|
+
|
|
195
|
+
if config_path is None or not config_path.exists():
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
content = config_path.read_text()
|
|
199
|
+
config = yaml.safe_load(content) or {}
|
|
200
|
+
|
|
201
|
+
if "repos" not in config:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
modified = False
|
|
205
|
+
|
|
206
|
+
for repo in config["repos"]:
|
|
207
|
+
if repo.get("repo") == "local":
|
|
208
|
+
hooks = repo.get("hooks", [])
|
|
209
|
+
original_count = len(hooks)
|
|
210
|
+
|
|
211
|
+
# Remove envdrift hooks
|
|
212
|
+
repo["hooks"] = [
|
|
213
|
+
hook for hook in hooks if not hook.get("id", "").startswith("envdrift-")
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
if len(repo["hooks"]) != original_count:
|
|
217
|
+
modified = True
|
|
218
|
+
|
|
219
|
+
if modified:
|
|
220
|
+
# Remove empty local repos
|
|
221
|
+
config["repos"] = [
|
|
222
|
+
repo
|
|
223
|
+
for repo in config["repos"]
|
|
224
|
+
if not (repo.get("repo") == "local" and not repo.get("hooks"))
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
with open(config_path, "w") as f:
|
|
228
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
229
|
+
|
|
230
|
+
return modified
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def verify_hooks_installed(config_path: Path | None = None) -> dict[str, bool]:
|
|
234
|
+
"""
|
|
235
|
+
Check which envdrift pre-commit hooks are present in a given pre-commit configuration.
|
|
236
|
+
|
|
237
|
+
Parameters:
|
|
238
|
+
config_path (Path | None): Path to a .pre-commit-config.yaml file. If None, the file is searched for by walking up from the current working directory.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
dict[str, bool]: Mapping of hook id to installation status: `{"envdrift-validate": bool, "envdrift-encryption": bool}`. Returns both `False` if the config file is missing or unreadable, or if the PyYAML package is not available.
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
import yaml
|
|
245
|
+
except ImportError:
|
|
246
|
+
return {"envdrift-validate": False, "envdrift-encryption": False}
|
|
247
|
+
|
|
248
|
+
if config_path is None:
|
|
249
|
+
config_path = find_precommit_config()
|
|
250
|
+
|
|
251
|
+
if config_path is None or not config_path.exists():
|
|
252
|
+
return {"envdrift-validate": False, "envdrift-encryption": False}
|
|
253
|
+
|
|
254
|
+
content = config_path.read_text()
|
|
255
|
+
config = yaml.safe_load(content) or {}
|
|
256
|
+
|
|
257
|
+
result = {"envdrift-validate": False, "envdrift-encryption": False}
|
|
258
|
+
|
|
259
|
+
for repo in config.get("repos", []):
|
|
260
|
+
if repo.get("repo") == "local":
|
|
261
|
+
for hook in repo.get("hooks", []):
|
|
262
|
+
hook_id = hook.get("id", "")
|
|
263
|
+
if hook_id in result:
|
|
264
|
+
result[hook_id] = True
|
|
265
|
+
|
|
266
|
+
return result
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""SOPS installer helpers for optional auto-install."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import stat
|
|
8
|
+
import urllib.request
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from envdrift.integrations.dotenvx import get_platform_info, get_venv_bin_dir
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SopsInstallError(Exception):
|
|
15
|
+
"""Failed to install SOPS."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_constants() -> dict:
|
|
21
|
+
constants_path = Path(__file__).parent.parent / "constants.json"
|
|
22
|
+
with open(constants_path) as f:
|
|
23
|
+
return json.load(f)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_sops_version() -> str:
|
|
27
|
+
return _load_constants()["sops_version"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_download_url_templates() -> dict[str, str]:
|
|
31
|
+
return _load_constants()["sops_download_urls"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SOPS_VERSION = _get_sops_version()
|
|
35
|
+
|
|
36
|
+
_URL_TEMPLATES = _get_download_url_templates()
|
|
37
|
+
SOPS_DOWNLOAD_URLS = {
|
|
38
|
+
("Darwin", "x86_64"): _URL_TEMPLATES["darwin_amd64"],
|
|
39
|
+
("Darwin", "arm64"): _URL_TEMPLATES["darwin_arm64"],
|
|
40
|
+
("Linux", "x86_64"): _URL_TEMPLATES["linux_amd64"],
|
|
41
|
+
("Linux", "aarch64"): _URL_TEMPLATES["linux_arm64"],
|
|
42
|
+
("Windows", "AMD64"): _URL_TEMPLATES["windows_amd64"],
|
|
43
|
+
("Windows", "x86_64"): _URL_TEMPLATES["windows_amd64"],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_sops_path() -> Path:
|
|
48
|
+
bin_dir = get_venv_bin_dir()
|
|
49
|
+
binary_name = "sops.exe" if platform.system() == "Windows" else "sops"
|
|
50
|
+
return bin_dir / binary_name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SopsInstaller:
|
|
54
|
+
"""Install SOPS binary to the virtual environment or user bin directory."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, version: str = SOPS_VERSION):
|
|
57
|
+
self.version = version
|
|
58
|
+
|
|
59
|
+
def _get_download_url(self) -> str:
|
|
60
|
+
system, machine = get_platform_info()
|
|
61
|
+
template = SOPS_DOWNLOAD_URLS.get((system, machine))
|
|
62
|
+
if not template:
|
|
63
|
+
raise SopsInstallError(f"Unsupported platform: {system} {machine}")
|
|
64
|
+
return template.format(version=self.version)
|
|
65
|
+
|
|
66
|
+
def install(self, target_path: Path | None = None) -> Path:
|
|
67
|
+
if target_path is None:
|
|
68
|
+
target_path = get_sops_path()
|
|
69
|
+
|
|
70
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
url = self._get_download_url()
|
|
72
|
+
tmp_path = target_path.with_suffix(target_path.suffix + ".download")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
urllib.request.urlretrieve(url, tmp_path) # nosec B310
|
|
76
|
+
if platform.system() != "Windows":
|
|
77
|
+
st = tmp_path.stat()
|
|
78
|
+
tmp_path.chmod(st.st_mode | stat.S_IEXEC)
|
|
79
|
+
tmp_path.replace(target_path)
|
|
80
|
+
except Exception as e: # nosec B110
|
|
81
|
+
if tmp_path.exists():
|
|
82
|
+
tmp_path.unlink()
|
|
83
|
+
raise SopsInstallError(f"Failed to install SOPS: {e}") from e
|
|
84
|
+
|
|
85
|
+
return target_path
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Output formatting modules."""
|
|
2
|
+
|
|
3
|
+
from envdrift.output.rich import (
|
|
4
|
+
console,
|
|
5
|
+
print_diff_result,
|
|
6
|
+
print_encryption_report,
|
|
7
|
+
print_error,
|
|
8
|
+
print_success,
|
|
9
|
+
print_validation_result,
|
|
10
|
+
print_warning,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"console",
|
|
15
|
+
"print_diff_result",
|
|
16
|
+
"print_encryption_report",
|
|
17
|
+
"print_error",
|
|
18
|
+
"print_success",
|
|
19
|
+
"print_validation_result",
|
|
20
|
+
"print_warning",
|
|
21
|
+
]
|