vm-tool 1.0.42__py3-none-any.whl → 1.0.44__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.
- vm_tool/cli.py +80 -1
- vm_tool/generator.py +121 -37
- vm_tool/secrets.py +75 -0
- {vm_tool-1.0.42.dist-info → vm_tool-1.0.44.dist-info}/METADATA +1 -1
- {vm_tool-1.0.42.dist-info → vm_tool-1.0.44.dist-info}/RECORD +9 -9
- {vm_tool-1.0.42.dist-info → vm_tool-1.0.44.dist-info}/WHEEL +0 -0
- {vm_tool-1.0.42.dist-info → vm_tool-1.0.44.dist-info}/entry_points.txt +0 -0
- {vm_tool-1.0.42.dist-info → vm_tool-1.0.44.dist-info}/licenses/LICENSE +0 -0
- {vm_tool-1.0.42.dist-info → vm_tool-1.0.44.dist-info}/top_level.txt +0 -0
vm_tool/cli.py
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import sys
|
|
3
|
+
import importlib.metadata
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
def main():
|
|
6
7
|
parser = argparse.ArgumentParser(
|
|
7
8
|
description="VM Tool: Setup, Provision, and Manage VMs"
|
|
8
9
|
)
|
|
9
|
-
|
|
10
|
+
try:
|
|
11
|
+
version = importlib.metadata.version("vm_tool")
|
|
12
|
+
except importlib.metadata.PackageNotFoundError:
|
|
13
|
+
version = "unknown"
|
|
14
|
+
parser.add_argument("--version", action="version", version=version)
|
|
10
15
|
parser.add_argument(
|
|
11
16
|
"--verbose", "-v", action="store_true", help="Enable verbose output"
|
|
12
17
|
)
|
|
@@ -294,6 +299,23 @@ def main():
|
|
|
294
299
|
help="CI/CD Platform",
|
|
295
300
|
)
|
|
296
301
|
|
|
302
|
+
# Secrets command
|
|
303
|
+
secrets_parser = subparsers.add_parser("secrets", help="Manage secrets")
|
|
304
|
+
secrets_subparsers = secrets_parser.add_subparsers(
|
|
305
|
+
dest="secrets_command", help="Secrets operations"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# secrets sync
|
|
309
|
+
sync_parser = secrets_subparsers.add_parser(
|
|
310
|
+
"sync", help="Sync local .env to GitHub Secrets"
|
|
311
|
+
)
|
|
312
|
+
sync_parser.add_argument(
|
|
313
|
+
"--env-file", type=str, default=".env", help="Path to .env file"
|
|
314
|
+
)
|
|
315
|
+
sync_parser.add_argument(
|
|
316
|
+
"--repo", type=str, help="Target GitHub repository (owner/repo)"
|
|
317
|
+
)
|
|
318
|
+
|
|
297
319
|
args = parser.parse_args()
|
|
298
320
|
|
|
299
321
|
# Configure logging based on flags
|
|
@@ -802,6 +824,63 @@ def main():
|
|
|
802
824
|
except Exception as e:
|
|
803
825
|
print(f"Error: {e}")
|
|
804
826
|
sys.exit(1)
|
|
827
|
+
elif args.command == "secrets":
|
|
828
|
+
if args.secrets_command == "sync":
|
|
829
|
+
from vm_tool.secrets import SecretsManager
|
|
830
|
+
import os
|
|
831
|
+
|
|
832
|
+
if not os.path.exists(args.env_file):
|
|
833
|
+
print(f"❌ Env file not found: {args.env_file}")
|
|
834
|
+
sys.exit(1)
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
manager = SecretsManager.from_github(repo=args.repo)
|
|
838
|
+
|
|
839
|
+
print(f"📖 Reading secrets from {args.env_file}...")
|
|
840
|
+
secrets_to_sync = {}
|
|
841
|
+
with open(args.env_file, "r") as f:
|
|
842
|
+
for line in f:
|
|
843
|
+
line = line.strip()
|
|
844
|
+
if not line or line.startswith("#"):
|
|
845
|
+
continue
|
|
846
|
+
# Basic env parsing
|
|
847
|
+
if "=" in line:
|
|
848
|
+
key, value = line.split("=", 1)
|
|
849
|
+
key = key.strip()
|
|
850
|
+
value = value.strip().strip("'").strip('"')
|
|
851
|
+
secrets_to_sync[key] = value
|
|
852
|
+
|
|
853
|
+
if not secrets_to_sync:
|
|
854
|
+
print("⚠️ No secrets found to sync.")
|
|
855
|
+
sys.exit(0)
|
|
856
|
+
|
|
857
|
+
print(f"🚀 Syncing {len(secrets_to_sync)} secrets to GitHub...")
|
|
858
|
+
# Confirm with user
|
|
859
|
+
confirm = (
|
|
860
|
+
input(
|
|
861
|
+
f"Proceed to upload {len(secrets_to_sync)} secrets? (yes/no): "
|
|
862
|
+
)
|
|
863
|
+
.strip()
|
|
864
|
+
.lower()
|
|
865
|
+
)
|
|
866
|
+
if confirm != "yes":
|
|
867
|
+
print("❌ Operation cancelled.")
|
|
868
|
+
sys.exit(0)
|
|
869
|
+
|
|
870
|
+
success_count = 0
|
|
871
|
+
for key, value in secrets_to_sync.items():
|
|
872
|
+
print(f" Uploading {key}...")
|
|
873
|
+
if manager.set(key, value):
|
|
874
|
+
success_count += 1
|
|
875
|
+
|
|
876
|
+
print(
|
|
877
|
+
f"\n✨ Successfully synced {success_count}/{len(secrets_to_sync)} secrets!"
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
except Exception as e:
|
|
881
|
+
print(f"❌ Error syncing secrets: {e}")
|
|
882
|
+
sys.exit(1)
|
|
883
|
+
|
|
805
884
|
else:
|
|
806
885
|
parser.print_help()
|
|
807
886
|
|
vm_tool/generator.py
CHANGED
|
@@ -22,6 +22,8 @@ class PipelineGenerator:
|
|
|
22
22
|
health_url: Optional[str] = None,
|
|
23
23
|
backup_paths: Optional[list] = None,
|
|
24
24
|
app_port: int = 8000,
|
|
25
|
+
hydrate_env: bool = True,
|
|
26
|
+
combine_compose: bool = True,
|
|
25
27
|
):
|
|
26
28
|
self.platform = platform
|
|
27
29
|
self.strategy = strategy
|
|
@@ -37,6 +39,8 @@ class PipelineGenerator:
|
|
|
37
39
|
)
|
|
38
40
|
self.backup_paths = backup_paths or ["/app", "/etc/nginx"]
|
|
39
41
|
self.app_port = app_port
|
|
42
|
+
self.hydrate_env = hydrate_env
|
|
43
|
+
self.combine_compose = combine_compose
|
|
40
44
|
|
|
41
45
|
# New options
|
|
42
46
|
self.run_linting = False
|
|
@@ -50,8 +54,7 @@ class PipelineGenerator:
|
|
|
50
54
|
run_tests: bool = False,
|
|
51
55
|
python_version: str = "3.11",
|
|
52
56
|
branch: str = "main",
|
|
53
|
-
):
|
|
54
|
-
"""Set additional options for the pipeline."""
|
|
57
|
+
): """Set additional options for the pipeline."""
|
|
55
58
|
self.run_linting = run_linting
|
|
56
59
|
self.run_tests = run_tests
|
|
57
60
|
self.python_version = python_version
|
|
@@ -120,6 +123,12 @@ class PipelineGenerator:
|
|
|
120
123
|
if self.enable_dry_run:
|
|
121
124
|
steps.append(self._step_dry_run())
|
|
122
125
|
|
|
126
|
+
if self.hydrate_env:
|
|
127
|
+
steps.append(self._step_hydrate_env())
|
|
128
|
+
|
|
129
|
+
if self.combine_compose:
|
|
130
|
+
steps.append(self._step_combine_compose())
|
|
131
|
+
|
|
123
132
|
# Main deployment
|
|
124
133
|
steps.append(self._step_deploy())
|
|
125
134
|
|
|
@@ -143,7 +152,7 @@ class PipelineGenerator:
|
|
|
143
152
|
# Combine all steps
|
|
144
153
|
steps_yaml = "\n".join(steps)
|
|
145
154
|
|
|
146
|
-
return f"""name: Deploy
|
|
155
|
+
return f"""name: Deploy with vm_tool
|
|
147
156
|
|
|
148
157
|
on:
|
|
149
158
|
push:
|
|
@@ -153,8 +162,8 @@ on:
|
|
|
153
162
|
workflow_dispatch:
|
|
154
163
|
|
|
155
164
|
env:
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
SSH_HOSTNAME: ${{{{ secrets.SSH_HOSTNAME }}}}
|
|
166
|
+
SSH_USERNAME: ${{{{ secrets.SSH_USERNAME }}}}
|
|
158
167
|
APP_PORT: {self.app_port}
|
|
159
168
|
|
|
160
169
|
jobs:
|
|
@@ -176,16 +185,16 @@ jobs:
|
|
|
176
185
|
echo "🔐 Validating GitHub Secrets..."
|
|
177
186
|
MISSING_SECRETS=()
|
|
178
187
|
|
|
179
|
-
if [ -z "${{ secrets.
|
|
180
|
-
MISSING_SECRETS+=("
|
|
188
|
+
if [ -z "${{ secrets.SSH_HOSTNAME }}" ]; then
|
|
189
|
+
MISSING_SECRETS+=("SSH_HOSTNAME")
|
|
181
190
|
fi
|
|
182
191
|
|
|
183
|
-
if [ -z "${{ secrets.
|
|
184
|
-
MISSING_SECRETS+=("
|
|
192
|
+
if [ -z "${{ secrets.SSH_USERNAME }}" ]; then
|
|
193
|
+
MISSING_SECRETS+=("SSH_USERNAME")
|
|
185
194
|
fi
|
|
186
195
|
|
|
187
|
-
if [ -z "${{ secrets.
|
|
188
|
-
MISSING_SECRETS+=("
|
|
196
|
+
if [ -z "${{ secrets.SSH_ID_RSA }}" ]; then
|
|
197
|
+
MISSING_SECRETS+=("SSH_ID_RSA")
|
|
189
198
|
fi
|
|
190
199
|
|
|
191
200
|
if [ ${#MISSING_SECRETS[@]} -ne 0 ]; then
|
|
@@ -199,16 +208,16 @@ jobs:
|
|
|
199
208
|
echo "2. Add each secret:"
|
|
200
209
|
echo ""
|
|
201
210
|
|
|
202
|
-
if [[ " ${MISSING_SECRETS[*]} " =~ "
|
|
203
|
-
echo "
|
|
211
|
+
if [[ " ${MISSING_SECRETS[*]} " =~ " SSH_HOSTNAME " ]]; then
|
|
212
|
+
echo " SSH_HOSTNAME: Your Server IP (e.g., 54.123.45.67)"
|
|
204
213
|
fi
|
|
205
214
|
|
|
206
|
-
if [[ " ${MISSING_SECRETS[*]} " =~ "
|
|
207
|
-
echo "
|
|
215
|
+
if [[ " ${MISSING_SECRETS[*]} " =~ " SSH_USERNAME " ]]; then
|
|
216
|
+
echo " SSH_USERNAME: SSH username (e.g., ubuntu)"
|
|
208
217
|
fi
|
|
209
218
|
|
|
210
|
-
if [[ " ${MISSING_SECRETS[*]} " =~ "
|
|
211
|
-
echo "
|
|
219
|
+
if [[ " ${MISSING_SECRETS[*]} " =~ " SSH_ID_RSA " ]]; then
|
|
220
|
+
echo " SSH_ID_RSA: Run 'cat ~/.ssh/id_rsa' and copy output"
|
|
212
221
|
fi
|
|
213
222
|
|
|
214
223
|
echo ""
|
|
@@ -293,9 +302,54 @@ jobs:
|
|
|
293
302
|
- name: Set up SSH
|
|
294
303
|
run: |
|
|
295
304
|
mkdir -p ~/.ssh
|
|
296
|
-
echo "${{ secrets.
|
|
305
|
+
echo "${{ secrets.SSH_ID_RSA }}" > ~/.ssh/deploy_key
|
|
297
306
|
chmod 600 ~/.ssh/deploy_key
|
|
298
|
-
ssh-keyscan -H ${{ secrets.
|
|
307
|
+
ssh-keyscan -H ${{ secrets.SSH_HOSTNAME }} >> ~/.ssh/known_hosts
|
|
308
|
+
|
|
309
|
+
# Create SSH config to use the key for the specific host
|
|
310
|
+
cat > ~/.ssh/config <<EOF
|
|
311
|
+
Host ${{ secrets.SSH_HOSTNAME }}
|
|
312
|
+
User ${{ secrets.SSH_USERNAME }}
|
|
313
|
+
IdentityFile ~/.ssh/deploy_key
|
|
314
|
+
StrictHostKeyChecking no
|
|
315
|
+
EOF"""
|
|
316
|
+
|
|
317
|
+
def _step_hydrate_env(self) -> str:
|
|
318
|
+
return """
|
|
319
|
+
- name: Hydrate Environment Files from Secrets
|
|
320
|
+
env:
|
|
321
|
+
SECRETS_CONTEXT: ${{ toJSON(secrets) }}
|
|
322
|
+
run: |
|
|
323
|
+
# Use vm_tool to create local env files from secrets based on compose file
|
|
324
|
+
vm_tool hydrate-env \\
|
|
325
|
+
--compose-file docker-compose.yml \\
|
|
326
|
+
--secrets "$SECRETS_CONTEXT"
|
|
327
|
+
echo "✅ Hydrated environment files from secrets"
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def _step_combine_compose(self) -> str:
|
|
331
|
+
return """
|
|
332
|
+
- name: Combine Docker Compose files
|
|
333
|
+
run: |
|
|
334
|
+
# Combine base and prod compose files if prod exists
|
|
335
|
+
if [ -f docker/docker-compose.prod.yml ]; then
|
|
336
|
+
echo "Merging docker-compose.yml and docker/docker-compose.prod.yml..."
|
|
337
|
+
docker compose -f docker-compose.yml -f docker/docker-compose.prod.yml config > docker-compose.released.yml
|
|
338
|
+
elif [ -f docker-compose.prod.yml ]; then
|
|
339
|
+
echo "Merging docker-compose.yml and docker-compose.prod.yml..."
|
|
340
|
+
docker compose -f docker-compose.yml -f docker-compose.prod.yml config > docker-compose.released.yml
|
|
341
|
+
else
|
|
342
|
+
echo "No prod overriding file found, using base file..."
|
|
343
|
+
docker compose -f docker-compose.yml config > docker-compose.released.yml
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# Replace absolute paths (from CI runner) with relative paths
|
|
347
|
+
# This assumes the structure: /home/runner/work/repo/repo/ -> ./
|
|
348
|
+
sed -i 's|/home/runner/work/[^/]*/[^/]*||g' docker-compose.released.yml
|
|
349
|
+
|
|
350
|
+
echo "✅ Generated combined Docker Compose file"
|
|
351
|
+
cat docker-compose.released.yml
|
|
352
|
+
"""
|
|
299
353
|
|
|
300
354
|
def _step_validate_ssh(self) -> str:
|
|
301
355
|
return """
|
|
@@ -356,25 +410,52 @@ jobs:
|
|
|
356
410
|
def _step_deploy(self) -> str:
|
|
357
411
|
return """
|
|
358
412
|
- name: Deploy with vm_tool (Ansible-based)
|
|
413
|
+
env:
|
|
414
|
+
# Define deployment command for the released file
|
|
415
|
+
DEPLOY_COMMAND: "docker compose -f docker-compose.released.yml up -d --remove-orphans"
|
|
359
416
|
run: |
|
|
360
417
|
# Create inventory file for Ansible
|
|
361
418
|
cat > inventory.yml << EOF
|
|
362
419
|
all:
|
|
363
420
|
hosts:
|
|
364
421
|
production:
|
|
365
|
-
ansible_host: ${{ secrets.
|
|
366
|
-
ansible_user: ${{ secrets.
|
|
422
|
+
ansible_host: ${{ secrets.SSH_HOSTNAME }}
|
|
423
|
+
ansible_user: ${{ secrets.SSH_USERNAME }}
|
|
367
424
|
ansible_ssh_private_key_file: ~/.ssh/deploy_key
|
|
368
425
|
EOF
|
|
369
426
|
|
|
370
427
|
# Deploy using vm_tool (uses Ansible under the hood)
|
|
371
428
|
export GITHUB_REPOSITORY_OWNER=${{ github.repository_owner }}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
429
|
+
|
|
430
|
+
# Determine Project Directory based on user (handle root case)
|
|
431
|
+
if [ "${{ secrets.SSH_USERNAME }}" = "root" ]; then
|
|
432
|
+
HOME_DIR="/root"
|
|
433
|
+
else
|
|
434
|
+
HOME_DIR="/home/${{ secrets.SSH_USERNAME }}"
|
|
435
|
+
fi
|
|
436
|
+
PROJECT_DIR="${HOME_DIR}/apps/${{ github.event.repository.name }}"
|
|
437
|
+
|
|
438
|
+
# Prepare arguments
|
|
439
|
+
ARGS=(
|
|
440
|
+
"--inventory" "inventory.yml"
|
|
441
|
+
"--compose-file" "docker-compose.released.yml"
|
|
442
|
+
"--project-dir" "$PROJECT_DIR"
|
|
443
|
+
"--deploy-command" "${{ env.DEPLOY_COMMAND }}"
|
|
444
|
+
"--force"
|
|
445
|
+
"--health-url" "http://${{ secrets.SSH_HOSTNAME }}:${{ env.APP_PORT }}/health"
|
|
446
|
+
"--host" "${{ secrets.SSH_HOSTNAME }}"
|
|
447
|
+
"--user" "${{ secrets.SSH_USERNAME }}"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Move .env if it exists and add to args
|
|
451
|
+
if [ -f .env ]; then
|
|
452
|
+
mkdir -p env
|
|
453
|
+
mv .env env/.env
|
|
454
|
+
ARGS+=("--env-file" "env/.env")
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
vm_tool deploy-docker "${ARGS[@]}"
|
|
458
|
+
"""
|
|
378
459
|
|
|
379
460
|
def _step_health_check(self) -> str:
|
|
380
461
|
return f"""
|
|
@@ -394,10 +475,13 @@ jobs:
|
|
|
394
475
|
return """
|
|
395
476
|
- name: Verify
|
|
396
477
|
run: |
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
478
|
+
# Use absolute path for verify step too
|
|
479
|
+
PROJECT_DIR="/home/${{ secrets.SSH_USERNAME }}/apps/${{ github.event.repository.name }}"
|
|
480
|
+
|
|
481
|
+
ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOSTNAME }} << EOF
|
|
482
|
+
cd $PROJECT_DIR
|
|
483
|
+
docker compose ps
|
|
484
|
+
docker compose logs --tail=20
|
|
401
485
|
EOF"""
|
|
402
486
|
|
|
403
487
|
def _step_rollback(self) -> str:
|
|
@@ -406,12 +490,12 @@ jobs:
|
|
|
406
490
|
if: failure()
|
|
407
491
|
run: |
|
|
408
492
|
echo "⚠️ Rolling back..."
|
|
409
|
-
|
|
493
|
+
# Note: Rollback logic might need adjustment for absolute paths, keeping simple for now
|
|
494
|
+
ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOSTNAME }} << 'EOF'
|
|
410
495
|
BACKUP=$(ls -t ~/backups/*.tar.gz 2>/dev/null | head -1)
|
|
411
496
|
if [ -n "$BACKUP" ]; then
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
echo "✅ Rolled back"
|
|
497
|
+
# TODO: Improve rollback to handle dynamic dirs
|
|
498
|
+
echo "Rollback not fully implemented for dynamic paths yet"
|
|
415
499
|
fi
|
|
416
500
|
EOF"""
|
|
417
501
|
|
|
@@ -420,7 +504,7 @@ jobs:
|
|
|
420
504
|
- name: Cleanup
|
|
421
505
|
if: success()
|
|
422
506
|
run: |
|
|
423
|
-
ssh -i ~/.ssh/deploy_key ${{ secrets.
|
|
507
|
+
ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOSTNAME }} << 'EOF'
|
|
424
508
|
cd ~/backups 2>/dev/null || exit 0
|
|
425
509
|
ls -t *.tar.gz 2>/dev/null | tail -n +6 | xargs rm -f || true
|
|
426
510
|
EOF"""
|
|
@@ -431,7 +515,7 @@ jobs:
|
|
|
431
515
|
if: always()
|
|
432
516
|
run: |
|
|
433
517
|
if [ "${{ job.status }}" == "success" ]; then
|
|
434
|
-
echo "✅ Deployed to ${{ secrets.
|
|
518
|
+
echo "✅ Deployed to ${{ secrets.SSH_HOSTNAME }}:${{ env.APP_PORT }}"
|
|
435
519
|
else
|
|
436
520
|
echo "❌ Deployment failed"
|
|
437
521
|
fi"""
|
vm_tool/secrets.py
CHANGED
|
@@ -246,6 +246,76 @@ class EncryptedFileBackend(SecretsBackend):
|
|
|
246
246
|
return False
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
class GitHubSecretsBackend(SecretsBackend):
|
|
250
|
+
"""GitHub Actions Secrets backend using 'gh' CLI."""
|
|
251
|
+
|
|
252
|
+
def __init__(self, repo: Optional[str] = None):
|
|
253
|
+
import shutil
|
|
254
|
+
|
|
255
|
+
self.gh_cli = shutil.which("gh")
|
|
256
|
+
if not self.gh_cli:
|
|
257
|
+
raise RuntimeError("GitHub CLI ('gh') is not installed.")
|
|
258
|
+
self.repo = repo
|
|
259
|
+
|
|
260
|
+
def get_secret(self, key: str) -> Optional[str]:
|
|
261
|
+
"""
|
|
262
|
+
Get secret from GitHub.
|
|
263
|
+
Note: GitHub Secrets are write-only. We can't retrieve the value.
|
|
264
|
+
"""
|
|
265
|
+
logger.warning("Cannot retrieve secret value from GitHub (write-only).")
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def set_secret(self, key: str, value: str) -> bool:
|
|
269
|
+
"""Set secret in GitHub Actions."""
|
|
270
|
+
import subprocess
|
|
271
|
+
|
|
272
|
+
cmd = ["gh", "secret", "set", key]
|
|
273
|
+
if self.repo:
|
|
274
|
+
cmd.extend(["--repo", self.repo])
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
# Pass value via stdin
|
|
278
|
+
subprocess.run(cmd, input=value, check=True, text=True, capture_output=True)
|
|
279
|
+
logger.info(f"Secret '{key}' stored in GitHub Secrets")
|
|
280
|
+
return True
|
|
281
|
+
except subprocess.CalledProcessError as e:
|
|
282
|
+
logger.error(f"Failed to set secret in GitHub: {e.stderr.strip()}")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def delete_secret(self, key: str) -> bool:
|
|
286
|
+
"""Delete secret from GitHub Actions."""
|
|
287
|
+
import subprocess
|
|
288
|
+
|
|
289
|
+
cmd = ["gh", "secret", "delete", key]
|
|
290
|
+
if self.repo:
|
|
291
|
+
cmd.extend(["--repo", self.repo])
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
subprocess.run(cmd, check=True, text=True, capture_output=True)
|
|
295
|
+
logger.info(f"Secret '{key}' deleted from GitHub Secrets")
|
|
296
|
+
return True
|
|
297
|
+
except subprocess.CalledProcessError as e:
|
|
298
|
+
logger.error(f"Failed to delete secret from GitHub: {e.stderr.strip()}")
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def list_secrets(self) -> list:
|
|
302
|
+
"""List all secret keys in GitHub Actions."""
|
|
303
|
+
import subprocess
|
|
304
|
+
import json
|
|
305
|
+
|
|
306
|
+
cmd = ["gh", "secret", "list", "--json", "name"]
|
|
307
|
+
if self.repo:
|
|
308
|
+
cmd.extend(["--repo", self.repo])
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
result = subprocess.run(cmd, check=True, text=True, capture_output=True)
|
|
312
|
+
secrets = json.loads(result.stdout)
|
|
313
|
+
return [s["name"] for s in secrets]
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Failed to list secrets from GitHub: {e}")
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
|
|
249
319
|
class SecretsManager:
|
|
250
320
|
"""Unified secrets manager supporting multiple backends."""
|
|
251
321
|
|
|
@@ -283,3 +353,8 @@ class SecretsManager:
|
|
|
283
353
|
def from_file(cls, secrets_file: str = ".secrets.enc", **kwargs):
|
|
284
354
|
"""Create secrets manager with encrypted file backend."""
|
|
285
355
|
return cls(EncryptedFileBackend(secrets_file, **kwargs))
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def from_github(cls, repo: Optional[str] = None):
|
|
359
|
+
"""Create secrets manager with GitHub backend."""
|
|
360
|
+
return cls(GitHubSecretsBackend(repo))
|
|
@@ -24,13 +24,13 @@ vm_tool/alerting.py,sha256=6bNfQNSAQ_SJW9dQfB8s8gDZ-7ocYw4Q211VM6FWiXU,8269
|
|
|
24
24
|
vm_tool/audit.py,sha256=spVtMcwadTkD9lELH1tur7Dr3OHjajk44ZatKYEbeVI,3388
|
|
25
25
|
vm_tool/backup.py,sha256=WhwCXXJDYCQgoexroksNhWgB5ki6HUly0JhQ9ybCdMg,4130
|
|
26
26
|
vm_tool/benchmarking.py,sha256=9AZ4JwKd5aEjwtKnhsuFa77y6WSO_fSEIqgMh3w_Btw,6212
|
|
27
|
-
vm_tool/cli.py,sha256=
|
|
27
|
+
vm_tool/cli.py,sha256=0Gyon9B5BXgrd4TG9b980seiD6YmYye89qysmafGbfo,33014
|
|
28
28
|
vm_tool/cloud.py,sha256=1fQV1o3wCth55B1hSenME_j2n5gCJDo9gtUcqDxCs7s,4047
|
|
29
29
|
vm_tool/completion.py,sha256=U8W8mmNdR0Ci0WjlEIa8CLOUtaVLZ5tWYpqViBBIBF8,7431
|
|
30
30
|
vm_tool/compliance.py,sha256=7yXrvRoPxJMW0DTVhJ-ZxbaJo7quDSVgae2_mYbVAmE,3173
|
|
31
31
|
vm_tool/config.py,sha256=F589SFyxTWmXyvZLWgvvMypJjTO9PeH9sR_g655tgJo,3270
|
|
32
32
|
vm_tool/drift.py,sha256=Rzw5TItsfqVMqgV8JFbqJzJe-qAb1x1G_2ue8wzNCAs,3301
|
|
33
|
-
vm_tool/generator.py,sha256=
|
|
33
|
+
vm_tool/generator.py,sha256=2JDOQ84mEWsnTum1JEtb8NLrE7rcrR_XUe36Pp2tLDU,18317
|
|
34
34
|
vm_tool/health.py,sha256=CupB1JAJprlarcCmP2WJwtAekNxPCPcUGipZQcRiZg4,6468
|
|
35
35
|
vm_tool/history.py,sha256=6Nh0-L4rrKTKfunJXvovOzO1uHyuDCQvZIb7PgZaBh8,4207
|
|
36
36
|
vm_tool/kubernetes.py,sha256=0CABF2Wyc9y56ueFGFduShe7T6GlIOqmg8MGG-Ro2BY,3137
|
|
@@ -42,7 +42,7 @@ vm_tool/rbac.py,sha256=hdFzh4QhUuQmGmMXBuG-o21FdEp1fb8WSJrCWAz-drI,3999
|
|
|
42
42
|
vm_tool/recovery.py,sha256=fesho6Pi_ItRkdtiNK2bh-8Q_T98kSn2LZwKovMFsWI,5473
|
|
43
43
|
vm_tool/reporting.py,sha256=yiLdArTfu7A8-PWL3dmFSRNpcMvBSq1iHNw6YJRO1hI,7419
|
|
44
44
|
vm_tool/runner.py,sha256=V5ssrj6z1RwCC3GJhlnwxn-CqoFm7NfkzJpUr5U9CS8,24049
|
|
45
|
-
vm_tool/secrets.py,sha256=
|
|
45
|
+
vm_tool/secrets.py,sha256=9P0ENQE7zak1b6BV_3KLFWlPxFnokLew6-Eo04xk1zY,11991
|
|
46
46
|
vm_tool/ssh.py,sha256=c9Nxs20VEq--qWCLZyc6uRqgny2-OCSwnTlcx9bZJtg,4905
|
|
47
47
|
vm_tool/state.py,sha256=DxJoRLITwT8mWcs_eAgFNTDfcrGh4PMdQuFjdGb1n6Y,4079
|
|
48
48
|
vm_tool/validation.py,sha256=DdfCzTsMU9-Br0MnhV8ZjIjYKBjSYLko30U0nXGzF1g,8990
|
|
@@ -66,9 +66,9 @@ vm_tool/vm_setup/docker/docker_setup.yml,sha256=kg6Abu3TjcLwV0JHnCAVUkj5l2HaGf7O
|
|
|
66
66
|
vm_tool/vm_setup/docker/install_docker_and_compose.yml,sha256=NxPm4YkOekRH0dpJQVgxjcSqrmT6f912Dv0owYsLBxE,3402
|
|
67
67
|
vm_tool/vm_setup/docker/login_to_docker_hub.yml,sha256=y9WaWLb1f1SuHVa1NNm24Gvf5bbwQ8eqEsImGLHGHGU,223
|
|
68
68
|
vm_tool/vm_setup/github/git_configuration.yml,sha256=hKuzOFsVlYRoBAXujREfrLiqV7j4RnQYbL5s63AEAqk,2355
|
|
69
|
-
vm_tool-1.0.
|
|
70
|
-
vm_tool-1.0.
|
|
71
|
-
vm_tool-1.0.
|
|
72
|
-
vm_tool-1.0.
|
|
73
|
-
vm_tool-1.0.
|
|
74
|
-
vm_tool-1.0.
|
|
69
|
+
vm_tool-1.0.44.dist-info/licenses/LICENSE,sha256=4OO6Zd_hYEOemzTNdgUR_E7oNvUJLuN2E7ZAgm7DcSM,1063
|
|
70
|
+
vm_tool-1.0.44.dist-info/METADATA,sha256=zES2orqKs1IqqH0SnFZ7kBwVa9qDS4FtWaGqpr0OZNA,6191
|
|
71
|
+
vm_tool-1.0.44.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
72
|
+
vm_tool-1.0.44.dist-info/entry_points.txt,sha256=A2EAvw95ftFXzVAWfHKIM-SsxQVxrIrByRNe-ArOp2k,45
|
|
73
|
+
vm_tool-1.0.44.dist-info/top_level.txt,sha256=jTLckJpKvJplpmKoMhPz2YKp_sQB9Q5-cCGmUHCp06M,17
|
|
74
|
+
vm_tool-1.0.44.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|