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,109 @@
|
|
|
1
|
+
"""Validation command for envdrift."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from envdrift.core.parser import EnvParser
|
|
11
|
+
from envdrift.core.schema import SchemaLoader, SchemaLoadError
|
|
12
|
+
from envdrift.core.validator import Validator
|
|
13
|
+
from envdrift.output.rich import console, print_error, print_validation_result
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate(
|
|
17
|
+
env_file: Annotated[Path, typer.Argument(help="Path to .env file to validate")] = Path(".env"),
|
|
18
|
+
schema: Annotated[
|
|
19
|
+
str | None,
|
|
20
|
+
typer.Option("--schema", "-s", help="Dotted path to Settings class"),
|
|
21
|
+
] = None,
|
|
22
|
+
service_dir: Annotated[
|
|
23
|
+
Path | None,
|
|
24
|
+
typer.Option("--service-dir", "-d", help="Service directory for imports"),
|
|
25
|
+
] = None,
|
|
26
|
+
ci: Annotated[bool, typer.Option("--ci", help="CI mode: exit with code 1 on failure")] = False,
|
|
27
|
+
check_encryption: Annotated[
|
|
28
|
+
bool,
|
|
29
|
+
typer.Option("--check-encryption/--no-check-encryption", help="Check encryption"),
|
|
30
|
+
] = True,
|
|
31
|
+
fix: Annotated[
|
|
32
|
+
bool, typer.Option("--fix", help="Output template for missing variables")
|
|
33
|
+
] = False,
|
|
34
|
+
verbose: Annotated[
|
|
35
|
+
bool, typer.Option("--verbose", "-v", help="Show additional details")
|
|
36
|
+
] = False,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Validate an .env file against a Pydantic Settings schema and display results.
|
|
40
|
+
|
|
41
|
+
Loads the specified Settings class, parses the given .env file, runs validation
|
|
42
|
+
(including optional encryption checks and extra-key checks), and prints a
|
|
43
|
+
human-readable validation report. If --fix is provided and validation fails,
|
|
44
|
+
prints a generated template for missing values. Exits with code 1 on invalid
|
|
45
|
+
schema or missing env file; when --ci is set, also exits with code 1 if the
|
|
46
|
+
validation result is invalid.
|
|
47
|
+
|
|
48
|
+
Parameters:
|
|
49
|
+
schema (str | None): Dotted import path to the Pydantic Settings class
|
|
50
|
+
(for example: "app.config:Settings"). Required; the command exits with
|
|
51
|
+
code 1 if not provided or if loading fails.
|
|
52
|
+
service_dir (Path | None): Optional directory to add to imports when
|
|
53
|
+
resolving the schema.
|
|
54
|
+
ci (bool): When true, exit with code 1 if validation fails.
|
|
55
|
+
check_encryption (bool): When true, validate encryption-related metadata
|
|
56
|
+
on sensitive fields.
|
|
57
|
+
fix (bool): When true and validation fails, print a fix template with
|
|
58
|
+
missing variables and defaults when available.
|
|
59
|
+
verbose (bool): When true, include additional details in the validation
|
|
60
|
+
output.
|
|
61
|
+
"""
|
|
62
|
+
if schema is None:
|
|
63
|
+
print_error("--schema is required. Example: --schema 'app.config:Settings'")
|
|
64
|
+
raise typer.Exit(code=1)
|
|
65
|
+
|
|
66
|
+
# Check env file exists
|
|
67
|
+
if not env_file.exists():
|
|
68
|
+
print_error(f"ENV file not found: {env_file}")
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
|
|
71
|
+
# Load schema
|
|
72
|
+
loader = SchemaLoader()
|
|
73
|
+
try:
|
|
74
|
+
settings_cls = loader.load(schema, service_dir)
|
|
75
|
+
schema_meta = loader.extract_metadata(settings_cls)
|
|
76
|
+
except SchemaLoadError as e:
|
|
77
|
+
print_error(str(e))
|
|
78
|
+
raise typer.Exit(code=1) from None
|
|
79
|
+
|
|
80
|
+
# Parse env file
|
|
81
|
+
parser = EnvParser()
|
|
82
|
+
try:
|
|
83
|
+
env = parser.parse(env_file)
|
|
84
|
+
except FileNotFoundError as e:
|
|
85
|
+
print_error(str(e))
|
|
86
|
+
raise typer.Exit(code=1) from None
|
|
87
|
+
|
|
88
|
+
# Validate
|
|
89
|
+
validator = Validator()
|
|
90
|
+
result = validator.validate(
|
|
91
|
+
env,
|
|
92
|
+
schema_meta,
|
|
93
|
+
check_encryption=check_encryption,
|
|
94
|
+
check_extra=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Print result
|
|
98
|
+
print_validation_result(result, env_file, schema_meta, verbose=verbose)
|
|
99
|
+
|
|
100
|
+
# Generate fix template if requested
|
|
101
|
+
if fix and not result.valid:
|
|
102
|
+
template = validator.generate_fix_template(result, schema_meta)
|
|
103
|
+
if template:
|
|
104
|
+
console.print("[bold]Fix template:[/bold]")
|
|
105
|
+
console.print(template)
|
|
106
|
+
|
|
107
|
+
# Exit with appropriate code
|
|
108
|
+
if ci and not result.valid:
|
|
109
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Vault operations for envdrift."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from envdrift.env_files import detect_env_file
|
|
11
|
+
from envdrift.output.rich import console, print_error, print_success, print_warning
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def vault_push(
|
|
15
|
+
folder: Annotated[
|
|
16
|
+
Path | None,
|
|
17
|
+
typer.Argument(help="Service folder containing .env.keys file"),
|
|
18
|
+
] = None,
|
|
19
|
+
secret_name: Annotated[
|
|
20
|
+
str | None,
|
|
21
|
+
typer.Argument(help="Name of the secret in the vault"),
|
|
22
|
+
] = None,
|
|
23
|
+
env: Annotated[
|
|
24
|
+
str | None,
|
|
25
|
+
typer.Option(
|
|
26
|
+
"--env", "-e", help="Environment suffix (e.g., 'soak' for DOTENV_PRIVATE_KEY_SOAK)"
|
|
27
|
+
),
|
|
28
|
+
] = None,
|
|
29
|
+
direct: Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option(
|
|
32
|
+
"--direct",
|
|
33
|
+
help="Push a direct key-value pair (use with positional args: secret-name value)",
|
|
34
|
+
),
|
|
35
|
+
] = False,
|
|
36
|
+
all_services: Annotated[
|
|
37
|
+
bool,
|
|
38
|
+
typer.Option("--all", help="Push all secrets defined in sync config (skipping existing)"),
|
|
39
|
+
] = False,
|
|
40
|
+
skip_encrypt: Annotated[
|
|
41
|
+
bool,
|
|
42
|
+
typer.Option("--skip-encrypt", help="Skip encryption step, only push keys to vault"),
|
|
43
|
+
] = False,
|
|
44
|
+
config: Annotated[
|
|
45
|
+
Path | None,
|
|
46
|
+
typer.Option("--config", "-c", help="Path to sync config file"),
|
|
47
|
+
] = None,
|
|
48
|
+
provider: Annotated[
|
|
49
|
+
str | None,
|
|
50
|
+
typer.Option("--provider", "-p", help="Vault provider: azure, aws, hashicorp, gcp"),
|
|
51
|
+
] = None,
|
|
52
|
+
vault_url: Annotated[
|
|
53
|
+
str | None,
|
|
54
|
+
typer.Option("--vault-url", help="Vault URL (Azure Key Vault or HashiCorp Vault)"),
|
|
55
|
+
] = None,
|
|
56
|
+
region: Annotated[
|
|
57
|
+
str | None,
|
|
58
|
+
typer.Option("--region", help="AWS region (default: us-east-1)"),
|
|
59
|
+
] = None,
|
|
60
|
+
project_id: Annotated[
|
|
61
|
+
str | None,
|
|
62
|
+
typer.Option("--project-id", help="GCP project ID (Secret Manager)"),
|
|
63
|
+
] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Push encryption keys from local .env.keys files to cloud vaults.
|
|
67
|
+
|
|
68
|
+
This is the reverse of `envdrift sync` - uploads local keys to vault.
|
|
69
|
+
|
|
70
|
+
Three modes:
|
|
71
|
+
|
|
72
|
+
1. From .env.keys file (Single Service):
|
|
73
|
+
envdrift vault-push ./services/soak soak-machine --env soak
|
|
74
|
+
|
|
75
|
+
2. Direct value:
|
|
76
|
+
envdrift vault-push --direct soak-machine "DOTENV_PRIVATE_KEY_SOAK=abc123..."
|
|
77
|
+
|
|
78
|
+
3. All Services (from config):
|
|
79
|
+
envdrift vault-push --all
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
# Push from .env.keys (reads DOTENV_PRIVATE_KEY_SOAK)
|
|
83
|
+
envdrift vault-push ./services/soak soak-machine --env soak -p azure --vault-url https://myvault.vault.azure.net/
|
|
84
|
+
|
|
85
|
+
# Push direct value
|
|
86
|
+
envdrift vault-push --direct soak-machine "DOTENV_PRIVATE_KEY_SOAK=abc..." -p azure --vault-url https://myvault.vault.azure.net/
|
|
87
|
+
|
|
88
|
+
# Push all missing secrets defined in config
|
|
89
|
+
envdrift vault-push --all
|
|
90
|
+
|
|
91
|
+
# Push all without encrypting (when files are already encrypted)
|
|
92
|
+
envdrift vault-push --all --skip-encrypt
|
|
93
|
+
"""
|
|
94
|
+
import contextlib
|
|
95
|
+
import tomllib
|
|
96
|
+
|
|
97
|
+
from envdrift.config import ConfigNotFoundError, find_config, load_config
|
|
98
|
+
from envdrift.sync.operations import EnvKeysFile
|
|
99
|
+
from envdrift.vault import VaultError, get_vault_client
|
|
100
|
+
from envdrift.vault.base import SecretNotFoundError
|
|
101
|
+
|
|
102
|
+
# Validate --skip-encrypt is only used with --all
|
|
103
|
+
if skip_encrypt and not all_services:
|
|
104
|
+
print_warning("--skip-encrypt is only applicable with --all mode, ignoring")
|
|
105
|
+
|
|
106
|
+
# --all mode implementation
|
|
107
|
+
if all_services:
|
|
108
|
+
from envdrift.cli_commands.encryption_helpers import (
|
|
109
|
+
build_sops_encrypt_kwargs,
|
|
110
|
+
is_encrypted_content,
|
|
111
|
+
resolve_encryption_backend,
|
|
112
|
+
)
|
|
113
|
+
from envdrift.cli_commands.sync import load_sync_config_and_client
|
|
114
|
+
from envdrift.encryption import (
|
|
115
|
+
EncryptionBackendError,
|
|
116
|
+
EncryptionNotFoundError,
|
|
117
|
+
EncryptionProvider,
|
|
118
|
+
detect_encryption_provider,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Load sync config and client
|
|
122
|
+
sync_config, client, effective_provider, _, _, _ = load_sync_config_and_client(
|
|
123
|
+
config_file=config,
|
|
124
|
+
provider=provider,
|
|
125
|
+
vault_url=vault_url,
|
|
126
|
+
region=region,
|
|
127
|
+
project_id=project_id,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
encryption_backend, backend_provider, encryption_config = resolve_encryption_backend(
|
|
132
|
+
config
|
|
133
|
+
)
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
print_error(f"Unsupported encryption backend: {e}")
|
|
136
|
+
raise typer.Exit(code=1) from None
|
|
137
|
+
|
|
138
|
+
if not encryption_backend.is_installed():
|
|
139
|
+
print_error(f"{encryption_backend.name} is not installed")
|
|
140
|
+
console.print(encryption_backend.install_instructions())
|
|
141
|
+
raise typer.Exit(code=1)
|
|
142
|
+
|
|
143
|
+
sops_encrypt_kwargs = {}
|
|
144
|
+
if backend_provider == EncryptionProvider.SOPS:
|
|
145
|
+
sops_encrypt_kwargs = build_sops_encrypt_kwargs(encryption_config)
|
|
146
|
+
|
|
147
|
+
console.print("[bold]Vault Push All[/bold]")
|
|
148
|
+
console.print(f"Provider: {effective_provider}")
|
|
149
|
+
console.print(f"Services: {len(sync_config.mappings)}")
|
|
150
|
+
if skip_encrypt:
|
|
151
|
+
console.print("[dim]Encryption: skipped (--skip-encrypt)[/dim]")
|
|
152
|
+
console.print()
|
|
153
|
+
|
|
154
|
+
pushed_count = 0
|
|
155
|
+
skipped_count = 0
|
|
156
|
+
error_count = 0
|
|
157
|
+
dotenvx_mismatch = False
|
|
158
|
+
|
|
159
|
+
for mapping in sync_config.mappings:
|
|
160
|
+
try:
|
|
161
|
+
# Check/Detect .env file (unless --skip-encrypt, where we only need .env.keys)
|
|
162
|
+
env_file = mapping.folder_path / f".env.{mapping.effective_environment}"
|
|
163
|
+
effective_environment = mapping.effective_environment
|
|
164
|
+
|
|
165
|
+
if not skip_encrypt:
|
|
166
|
+
if not env_file.exists():
|
|
167
|
+
# Auto-detect logic similar to sync
|
|
168
|
+
detected = detect_env_file(mapping.folder_path)
|
|
169
|
+
if detected.status == "found" and detected.path:
|
|
170
|
+
env_file = detected.path
|
|
171
|
+
if detected.environment:
|
|
172
|
+
effective_environment = detected.environment
|
|
173
|
+
|
|
174
|
+
if not env_file.exists():
|
|
175
|
+
console.print(
|
|
176
|
+
f"[dim]Skipped[/dim] {mapping.folder_path}: No .env file found"
|
|
177
|
+
)
|
|
178
|
+
skipped_count += 1
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Check encryption (unless --skip-encrypt)
|
|
182
|
+
if not skip_encrypt:
|
|
183
|
+
content = env_file.read_text()
|
|
184
|
+
if not is_encrypted_content(backend_provider, encryption_backend, content):
|
|
185
|
+
detected_provider = detect_encryption_provider(env_file)
|
|
186
|
+
if detected_provider and detected_provider != backend_provider:
|
|
187
|
+
if (
|
|
188
|
+
detected_provider == EncryptionProvider.DOTENVX
|
|
189
|
+
and backend_provider != EncryptionProvider.DOTENVX
|
|
190
|
+
):
|
|
191
|
+
print_error(
|
|
192
|
+
f"{env_file}: encrypted with dotenvx, "
|
|
193
|
+
f"but config uses {backend_provider.value}"
|
|
194
|
+
)
|
|
195
|
+
error_count += 1
|
|
196
|
+
dotenvx_mismatch = True
|
|
197
|
+
continue
|
|
198
|
+
console.print(
|
|
199
|
+
f"[dim]Skipped[/dim] {mapping.folder_path}: "
|
|
200
|
+
f"Encrypted with {detected_provider.value}, "
|
|
201
|
+
f"config uses {backend_provider.value}"
|
|
202
|
+
)
|
|
203
|
+
skipped_count += 1
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
console.print(f"Encrypting {env_file} with {encryption_backend.name}...")
|
|
207
|
+
try:
|
|
208
|
+
result = encryption_backend.encrypt(env_file, **sops_encrypt_kwargs)
|
|
209
|
+
if not result.success:
|
|
210
|
+
print_error(result.message)
|
|
211
|
+
error_count += 1
|
|
212
|
+
continue
|
|
213
|
+
except (EncryptionNotFoundError, EncryptionBackendError) as e:
|
|
214
|
+
print_error(f"Failed to encrypt {env_file}: {e}")
|
|
215
|
+
error_count += 1
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# Check if secret exists in vault
|
|
219
|
+
try:
|
|
220
|
+
client.get_secret(mapping.secret_name)
|
|
221
|
+
# If successful, secret exists
|
|
222
|
+
console.print(
|
|
223
|
+
f"[dim]Skipped[/dim] {mapping.folder_path}: Secret '{mapping.secret_name}' already exists"
|
|
224
|
+
)
|
|
225
|
+
skipped_count += 1
|
|
226
|
+
continue
|
|
227
|
+
except SecretNotFoundError:
|
|
228
|
+
# Secret missing, proceed to push
|
|
229
|
+
pass
|
|
230
|
+
except VaultError as e:
|
|
231
|
+
print_error(f"Vault error checking {mapping.secret_name}: {e}")
|
|
232
|
+
error_count += 1
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Read key to push
|
|
236
|
+
env_keys_path = mapping.folder_path / sync_config.env_keys_filename
|
|
237
|
+
if not env_keys_path.exists():
|
|
238
|
+
print_error(f"Skipped {mapping.folder_path}: .env.keys not found")
|
|
239
|
+
error_count += 1
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
env_keys = EnvKeysFile(env_keys_path)
|
|
243
|
+
key_name = f"DOTENV_PRIVATE_KEY_{effective_environment.upper()}"
|
|
244
|
+
key_value = env_keys.read_key(key_name)
|
|
245
|
+
|
|
246
|
+
if not key_value:
|
|
247
|
+
print_error(f"Skipped {mapping.folder_path}: {key_name} not found in keys file")
|
|
248
|
+
error_count += 1
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
actual_value = f"{key_name}={key_value}"
|
|
252
|
+
|
|
253
|
+
# Push
|
|
254
|
+
client.set_secret(mapping.secret_name, actual_value)
|
|
255
|
+
print_success(f"Pushed {mapping.secret_name}")
|
|
256
|
+
pushed_count += 1
|
|
257
|
+
|
|
258
|
+
except (VaultError, OSError, ValueError) as e:
|
|
259
|
+
print_error(f"Error processing {mapping.folder_path}: {e}")
|
|
260
|
+
error_count += 1
|
|
261
|
+
|
|
262
|
+
console.print()
|
|
263
|
+
console.print(
|
|
264
|
+
f"Done. Pushed: {pushed_count}, Skipped: {skipped_count}, Errors: {error_count}"
|
|
265
|
+
)
|
|
266
|
+
if dotenvx_mismatch:
|
|
267
|
+
raise typer.Exit(code=1)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Normal/Direct mode preamble
|
|
271
|
+
envdrift_config = None
|
|
272
|
+
if config:
|
|
273
|
+
with contextlib.suppress(ConfigNotFoundError, tomllib.TOMLDecodeError):
|
|
274
|
+
envdrift_config = load_config(config)
|
|
275
|
+
else:
|
|
276
|
+
config_path = find_config()
|
|
277
|
+
if config_path:
|
|
278
|
+
with contextlib.suppress(ConfigNotFoundError, tomllib.TOMLDecodeError):
|
|
279
|
+
envdrift_config = load_config(config_path)
|
|
280
|
+
|
|
281
|
+
vault_config = getattr(envdrift_config, "vault", None)
|
|
282
|
+
|
|
283
|
+
# Determine effective provider
|
|
284
|
+
effective_provider = provider or getattr(vault_config, "provider", None)
|
|
285
|
+
if not effective_provider:
|
|
286
|
+
print_error("Vault provider required. Use --provider or configure in envdrift.toml")
|
|
287
|
+
raise typer.Exit(code=1)
|
|
288
|
+
|
|
289
|
+
# Determine effective vault URL
|
|
290
|
+
effective_vault_url = vault_url
|
|
291
|
+
if effective_vault_url is None and vault_config:
|
|
292
|
+
if effective_provider == "azure":
|
|
293
|
+
effective_vault_url = getattr(vault_config, "azure_vault_url", None)
|
|
294
|
+
elif effective_provider == "hashicorp":
|
|
295
|
+
effective_vault_url = getattr(vault_config, "hashicorp_url", None)
|
|
296
|
+
|
|
297
|
+
# Determine effective region
|
|
298
|
+
effective_region = region
|
|
299
|
+
if effective_region is None and vault_config:
|
|
300
|
+
effective_region = getattr(vault_config, "aws_region", None)
|
|
301
|
+
|
|
302
|
+
effective_project_id = project_id
|
|
303
|
+
if effective_project_id is None and vault_config:
|
|
304
|
+
effective_project_id = getattr(vault_config, "gcp_project_id", None)
|
|
305
|
+
|
|
306
|
+
# Validate provider-specific requirements
|
|
307
|
+
if effective_provider in ("azure", "hashicorp") and not effective_vault_url:
|
|
308
|
+
print_error(f"--vault-url required for {effective_provider}")
|
|
309
|
+
raise typer.Exit(code=1)
|
|
310
|
+
if effective_provider == "gcp" and not effective_project_id:
|
|
311
|
+
print_error("--project-id required for gcp")
|
|
312
|
+
raise typer.Exit(code=1)
|
|
313
|
+
|
|
314
|
+
# Handle direct mode
|
|
315
|
+
if direct:
|
|
316
|
+
if not folder or not secret_name:
|
|
317
|
+
print_error("Direct mode requires: envdrift vault-push --direct <secret-name> <value>")
|
|
318
|
+
raise typer.Exit(code=1)
|
|
319
|
+
# In direct mode, folder is actually the secret name, secret_name is the value
|
|
320
|
+
actual_secret_name = str(folder)
|
|
321
|
+
actual_value = secret_name
|
|
322
|
+
else:
|
|
323
|
+
# Normal mode: read from .env.keys
|
|
324
|
+
if not folder or not secret_name or not env:
|
|
325
|
+
print_error(
|
|
326
|
+
"Required: envdrift vault-push <folder> <secret-name> --env <environment> (or use --all)"
|
|
327
|
+
)
|
|
328
|
+
raise typer.Exit(code=1)
|
|
329
|
+
|
|
330
|
+
# Read the key from .env.keys
|
|
331
|
+
env_keys_path = folder / ".env.keys"
|
|
332
|
+
if not env_keys_path.exists():
|
|
333
|
+
print_error(f"File not found: {env_keys_path}")
|
|
334
|
+
raise typer.Exit(code=1)
|
|
335
|
+
|
|
336
|
+
env_keys = EnvKeysFile(env_keys_path)
|
|
337
|
+
key_name = f"DOTENV_PRIVATE_KEY_{env.upper()}"
|
|
338
|
+
key_value = env_keys.read_key(key_name)
|
|
339
|
+
|
|
340
|
+
if not key_value:
|
|
341
|
+
print_error(f"Key '{key_name}' not found in {env_keys_path}")
|
|
342
|
+
raise typer.Exit(code=1)
|
|
343
|
+
|
|
344
|
+
actual_secret_name = secret_name
|
|
345
|
+
actual_value = f"{key_name}={key_value}"
|
|
346
|
+
|
|
347
|
+
# Create vault client
|
|
348
|
+
try:
|
|
349
|
+
vault_client_config = {}
|
|
350
|
+
if effective_provider == "azure":
|
|
351
|
+
vault_client_config["vault_url"] = effective_vault_url
|
|
352
|
+
elif effective_provider == "aws":
|
|
353
|
+
vault_client_config["region"] = effective_region or "us-east-1"
|
|
354
|
+
elif effective_provider == "hashicorp":
|
|
355
|
+
vault_client_config["url"] = effective_vault_url
|
|
356
|
+
elif effective_provider == "gcp":
|
|
357
|
+
vault_client_config["project_id"] = effective_project_id
|
|
358
|
+
|
|
359
|
+
client = get_vault_client(effective_provider, **vault_client_config)
|
|
360
|
+
client.authenticate()
|
|
361
|
+
except ImportError as e:
|
|
362
|
+
print_error(str(e))
|
|
363
|
+
raise typer.Exit(code=1) from None
|
|
364
|
+
except VaultError as e:
|
|
365
|
+
print_error(f"Vault authentication failed: {e}")
|
|
366
|
+
raise typer.Exit(code=1) from None
|
|
367
|
+
|
|
368
|
+
# Push the secret
|
|
369
|
+
try:
|
|
370
|
+
result = client.set_secret(actual_secret_name, actual_value)
|
|
371
|
+
print_success(f"Pushed secret '{actual_secret_name}' to {effective_provider} vault")
|
|
372
|
+
if result.version:
|
|
373
|
+
console.print(f" Version: {result.version}")
|
|
374
|
+
except VaultError as e:
|
|
375
|
+
print_error(f"Failed to push secret: {e}")
|
|
376
|
+
raise typer.Exit(code=1) from None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Version command for envdrift."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from envdrift import __version__
|
|
6
|
+
from envdrift.output.rich import console
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def version() -> None:
|
|
10
|
+
"""
|
|
11
|
+
Display the installed envdrift version in the console.
|
|
12
|
+
|
|
13
|
+
Prints the current package version using the application's styled console output.
|
|
14
|
+
"""
|
|
15
|
+
console.print(f"envdrift [bold green]{__version__}[/bold green]")
|