vm-tool 1.0.43__tar.gz → 1.0.45__tar.gz
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-1.0.43 → vm_tool-1.0.45}/PKG-INFO +1 -1
- {vm_tool-1.0.43 → vm_tool-1.0.45}/pyproject.toml +3 -6
- {vm_tool-1.0.43 → vm_tool-1.0.45}/setup.py +1 -1
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/cli.py +87 -7
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/generator.py +3 -5
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/health.py +2 -2
- vm_tool-1.0.45/vm_tool/release.py +123 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/runner.py +82 -15
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/push_code_tasks.yml +12 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool.egg-info/PKG-INFO +1 -1
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool.egg-info/SOURCES.txt +1 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.agent/workflows/push.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.agent/workflows/test_and_lint.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.devcontainer/devcontainer.json +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.github/dependabot.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.github/workflows/ci.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.github/workflows/deploy.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.github/workflows/publish.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.gitignore +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/.pre-commit-config.yaml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/CONTRIBUTING +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/CONTRIBUTING.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/LICENSE +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/MANIFEST.in +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/Makefile +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/README.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/codePushToGithub.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/MODULE_GUIDE.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/deployment-approaches.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/ec2-github-actions-guide.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/features.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/generator.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/index.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/pipeline-generator.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/reference/runner.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/reference/ssh.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/ssh-key-setup.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/docs/usage.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/README.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/__init__.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/cloud/README.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/cloud/__init__.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/cloud/ssh_identity_file.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/cloud/ssh_password.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/cloud/template_cloud_setup.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/deploy_full_setup.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/docker-compose.example.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/ec2-setup.sh +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/github-actions-ec2.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/github-actions-full-setup.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/local/.keep +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/local/README.md +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/local/__init__.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/local/template_local_setup.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/production-deploy.sh +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/rollback.sh +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/setup.sh +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/ssh_key_management.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/examples/version_check.sh +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/jingo.code-workspace +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/mkdocs.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/molecule/default/converge.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/molecule/default/molecule.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/molecule/default/verify.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/requirements-docs.txt +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/requirements.txt +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/runtime.txt +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/setup.cfg +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/conftest.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/integration/test_deployment.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_config.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_generator.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_health.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_history.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_logging.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_runner.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_ssh.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/tests/test_state.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/__init__.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/alerting.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/audit.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/backup.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/benchmarking.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/cloud.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/completion.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/compliance.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/config.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/drift.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/history.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/kubernetes.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/metrics.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/notifications.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/plugins.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/policy.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/rbac.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/recovery.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/reporting.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/secrets.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/ssh.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/state.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/strategies/__init__.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/strategies/ab_testing.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/strategies/blue_green.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/strategies/canary.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/validation.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/cleanup.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/docker/create_docker_service.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/docker/docker_setup.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/docker/install_docker_and_compose.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/docker/login_to_docker_hub.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/github/git_configuration.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/inventory.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/k8s.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/main.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/monitoring.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/project_service.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/push_code.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/setup.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/vm_setup/setup_project_env.yml +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool/webhooks.py +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool.egg-info/dependency_links.txt +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool.egg-info/entry_points.txt +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool.egg-info/requires.txt +0 -0
- {vm_tool-1.0.43 → vm_tool-1.0.45}/vm_tool.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vm_tool"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.45"
|
|
8
8
|
description = "A Comprehensive Tool for Setting Up Virtual Machines."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -76,7 +76,7 @@ include = '\.pyi?$'
|
|
|
76
76
|
profile = "black"
|
|
77
77
|
multi_line_output = 3
|
|
78
78
|
[tool.bumpversion]
|
|
79
|
-
current_version = "1.0.
|
|
79
|
+
current_version = "1.0.45"
|
|
80
80
|
commit = true
|
|
81
81
|
tag = true
|
|
82
82
|
|
|
@@ -85,10 +85,7 @@ filename = "setup.py"
|
|
|
85
85
|
search = 'version="{current_version}"'
|
|
86
86
|
replace = 'version="{new_version}"'
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
filename = "vm_tool/cli.py"
|
|
90
|
-
search = 'version="{current_version}"'
|
|
91
|
-
replace = 'version="{new_version}"'
|
|
88
|
+
|
|
92
89
|
|
|
93
90
|
[[tool.bumpversion.files]]
|
|
94
91
|
filename = "pyproject.toml"
|
|
@@ -12,7 +12,7 @@ else:
|
|
|
12
12
|
|
|
13
13
|
setup(
|
|
14
14
|
name="vm_tool",
|
|
15
|
-
version="1.0.
|
|
15
|
+
version="1.0.45", # This will be updated by bump2version
|
|
16
16
|
packages=find_packages(),
|
|
17
17
|
description="A Comprehensive Tool for Setting Up Virtual Machines.",
|
|
18
18
|
long_description=long_description,
|
|
@@ -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
|
)
|
|
@@ -259,11 +264,21 @@ def main():
|
|
|
259
264
|
action="store_true",
|
|
260
265
|
help="Preview deployment without executing (shows what would be deployed)",
|
|
261
266
|
)
|
|
267
|
+
docker_parser.add_argument(
|
|
268
|
+
"--repo-name",
|
|
269
|
+
type=str,
|
|
270
|
+
help="Repository name (used to default project-dir to ~/apps/NAME)",
|
|
271
|
+
)
|
|
262
272
|
docker_parser.add_argument(
|
|
263
273
|
"--project-dir",
|
|
264
274
|
type=str,
|
|
265
|
-
|
|
266
|
-
|
|
275
|
+
help="Target directory on the server (default: ~/apps/<repo-name>)",
|
|
276
|
+
)
|
|
277
|
+
docker_parser.add_argument(
|
|
278
|
+
"--health-timeout",
|
|
279
|
+
type=int,
|
|
280
|
+
default=300,
|
|
281
|
+
help="Timeout in seconds for health checks (default: 300)",
|
|
267
282
|
)
|
|
268
283
|
|
|
269
284
|
# Completion command
|
|
@@ -294,6 +309,32 @@ def main():
|
|
|
294
309
|
help="CI/CD Platform",
|
|
295
310
|
)
|
|
296
311
|
|
|
312
|
+
# Release Prep command
|
|
313
|
+
release_parser = subparsers.add_parser(
|
|
314
|
+
"prepare-release", help="Prepare Docker Compose file for release"
|
|
315
|
+
)
|
|
316
|
+
release_parser.add_argument(
|
|
317
|
+
"--base-file", required=True, help="Base docker-compose file"
|
|
318
|
+
)
|
|
319
|
+
release_parser.add_argument(
|
|
320
|
+
"--prod-file", required=True, help="Production overlay file"
|
|
321
|
+
)
|
|
322
|
+
release_parser.add_argument("--output", required=True, help="Output file path")
|
|
323
|
+
release_parser.add_argument(
|
|
324
|
+
"--strip-volumes", help="Comma-separated list of services to strip volumes from"
|
|
325
|
+
)
|
|
326
|
+
release_parser.add_argument(
|
|
327
|
+
"--fix-paths", action="store_true", help="Fix CI absolute paths to relative"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Setup CI command
|
|
331
|
+
setup_ci_parser = subparsers.add_parser(
|
|
332
|
+
"setup-ci", help="Configure CI environment (SSH, Inventory)"
|
|
333
|
+
)
|
|
334
|
+
setup_ci_parser.add_argument(
|
|
335
|
+
"--provider", default="github", choices=["github"], help="CI Provider"
|
|
336
|
+
)
|
|
337
|
+
|
|
297
338
|
# Secrets command
|
|
298
339
|
secrets_parser = subparsers.add_parser("secrets", help="Manage secrets")
|
|
299
340
|
secrets_subparsers = secrets_parser.add_subparsers(
|
|
@@ -607,6 +648,14 @@ def main():
|
|
|
607
648
|
"compose_file", "docker-compose.yml"
|
|
608
649
|
)
|
|
609
650
|
|
|
651
|
+
# Project Directory Logic
|
|
652
|
+
project_dir = args.project_dir
|
|
653
|
+
if not project_dir:
|
|
654
|
+
if args.repo_name:
|
|
655
|
+
project_dir = f"~/apps/{args.repo_name}"
|
|
656
|
+
else:
|
|
657
|
+
project_dir = "~/app"
|
|
658
|
+
|
|
610
659
|
# Dry-run mode: show what would be deployed
|
|
611
660
|
if args.dry_run:
|
|
612
661
|
print("\n🔍 DRY-RUN MODE - No changes will be made\n")
|
|
@@ -614,6 +663,7 @@ def main():
|
|
|
614
663
|
print(f" Target Host: {host or 'from inventory'}")
|
|
615
664
|
print(f" SSH User: {user or 'from inventory'}")
|
|
616
665
|
print(f" Compose File: {compose_file}")
|
|
666
|
+
print(f" Project Dir: {project_dir}")
|
|
617
667
|
print(f" Inventory: {args.inventory}")
|
|
618
668
|
if args.env_file:
|
|
619
669
|
print(f" Env File: {args.env_file}")
|
|
@@ -646,15 +696,15 @@ def main():
|
|
|
646
696
|
env_file=args.env_file,
|
|
647
697
|
deploy_command=args.deploy_command,
|
|
648
698
|
force=args.force,
|
|
649
|
-
project_dir=
|
|
699
|
+
project_dir=project_dir,
|
|
650
700
|
)
|
|
651
701
|
|
|
652
702
|
# Run health checks if specified
|
|
653
703
|
if host and (args.health_check or args.health_port or args.health_url):
|
|
654
704
|
from vm_tool.health import SmokeTestSuite
|
|
655
705
|
|
|
656
|
-
print("\n🏥 Running Health Checks...")
|
|
657
|
-
suite = SmokeTestSuite(host)
|
|
706
|
+
print(f"\n🏥 Running Health Checks (Timeout: {args.health_timeout}s)...")
|
|
707
|
+
suite = SmokeTestSuite(host, timeout=args.health_timeout)
|
|
658
708
|
|
|
659
709
|
if args.health_port:
|
|
660
710
|
suite.add_port_check(args.health_port)
|
|
@@ -774,7 +824,37 @@ def main():
|
|
|
774
824
|
if deployment_type == "custom":
|
|
775
825
|
deploy_command = input("Enter custom deployment command: ").strip()
|
|
776
826
|
|
|
777
|
-
|
|
827
|
+
elif args.command == "prepare-release":
|
|
828
|
+
from vm_tool.release import ReleaseManager
|
|
829
|
+
manager = ReleaseManager(verbose=args.verbose)
|
|
830
|
+
try:
|
|
831
|
+
manager.prepare_release(
|
|
832
|
+
base_file=args.base_file,
|
|
833
|
+
prod_file=args.prod_file,
|
|
834
|
+
output_file=args.output,
|
|
835
|
+
strip_volumes=args.strip_volumes,
|
|
836
|
+
fix_paths=args.fix_paths
|
|
837
|
+
)
|
|
838
|
+
except Exception as e:
|
|
839
|
+
print(f"❌ Release preparation failed: {e}")
|
|
840
|
+
sys.exit(1)
|
|
841
|
+
|
|
842
|
+
except Exception as e:
|
|
843
|
+
print(f"❌ Release preparation failed: {e}")
|
|
844
|
+
sys.exit(1)
|
|
845
|
+
|
|
846
|
+
elif args.command == "setup-ci":
|
|
847
|
+
from vm_tool.runner import SetupRunner
|
|
848
|
+
# No config needed for this utility
|
|
849
|
+
try:
|
|
850
|
+
# We can add a static method or simple function for this
|
|
851
|
+
SetupRunner.setup_ci_environment(provider=args.provider)
|
|
852
|
+
print("✅ CI Environment configured successfully")
|
|
853
|
+
except Exception as e:
|
|
854
|
+
print(f"❌ CI Setup failed: {e}")
|
|
855
|
+
sys.exit(1)
|
|
856
|
+
|
|
857
|
+
elif deployment_type == "docker":
|
|
778
858
|
docker_compose_file = (
|
|
779
859
|
input(
|
|
780
860
|
"Enter Docker Compose file name [docker-compose.yml]: "
|
|
@@ -345,7 +345,7 @@ jobs:
|
|
|
345
345
|
|
|
346
346
|
# Replace absolute paths (from CI runner) with relative paths
|
|
347
347
|
# This assumes the structure: /home/runner/work/repo/repo/ -> ./
|
|
348
|
-
sed -i 's|/home/runner/work/[^/]*/[^/]
|
|
348
|
+
sed -i 's|/home/runner/work/[^/]*/[^/]*|.|g' docker-compose.released.yml
|
|
349
349
|
|
|
350
350
|
echo "✅ Generated combined Docker Compose file"
|
|
351
351
|
cat docker-compose.released.yml
|
|
@@ -447,11 +447,9 @@ jobs:
|
|
|
447
447
|
"--user" "${{ secrets.SSH_USERNAME }}"
|
|
448
448
|
)
|
|
449
449
|
|
|
450
|
-
#
|
|
450
|
+
# Add .env if it exists
|
|
451
451
|
if [ -f .env ]; then
|
|
452
|
-
|
|
453
|
-
mv .env env/.env
|
|
454
|
-
ARGS+=("--env-file" "env/.env")
|
|
452
|
+
ARGS+=("--env-file" ".env")
|
|
455
453
|
fi
|
|
456
454
|
|
|
457
455
|
vm_tool deploy-docker "${ARGS[@]}"
|
|
@@ -120,9 +120,9 @@ class HealthCheck:
|
|
|
120
120
|
class SmokeTestSuite:
|
|
121
121
|
"""Manages a suite of smoke tests."""
|
|
122
122
|
|
|
123
|
-
def __init__(self, host: str):
|
|
123
|
+
def __init__(self, host: str, timeout: int = 300):
|
|
124
124
|
self.host = host
|
|
125
|
-
self.health_check = HealthCheck(host)
|
|
125
|
+
self.health_check = HealthCheck(host, timeout=timeout)
|
|
126
126
|
self.tests: List[Dict] = []
|
|
127
127
|
|
|
128
128
|
def add_port_check(self, port: int, name: str = None):
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import yaml
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReleaseManager:
|
|
8
|
+
def __init__(self, verbose=False):
|
|
9
|
+
self.verbose = verbose
|
|
10
|
+
|
|
11
|
+
def _log(self, message):
|
|
12
|
+
if self.verbose:
|
|
13
|
+
print(f"DEBUG: {message}")
|
|
14
|
+
|
|
15
|
+
def merge_compose_files(self, files, output_file):
|
|
16
|
+
"""
|
|
17
|
+
Merge multiple docker-compose files using `docker compose config`.
|
|
18
|
+
"""
|
|
19
|
+
if not files:
|
|
20
|
+
raise ValueError("No input files provided")
|
|
21
|
+
|
|
22
|
+
cmd = ["docker", "compose"]
|
|
23
|
+
for f in files:
|
|
24
|
+
if not os.path.exists(f):
|
|
25
|
+
# Fallback: check if it exists relative to current dir?
|
|
26
|
+
# Or just skip/warn? For now, fail if explicit file missing.
|
|
27
|
+
# But our logic handles missing optional files upstream.
|
|
28
|
+
if self.verbose:
|
|
29
|
+
print(f"Warning: File {f} does not exist, skipping.")
|
|
30
|
+
continue
|
|
31
|
+
cmd.extend(["-f", f])
|
|
32
|
+
|
|
33
|
+
cmd.append("config")
|
|
34
|
+
|
|
35
|
+
self._log(f"Running: {' '.join(cmd)}")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
39
|
+
with open(output_file, "w") as f:
|
|
40
|
+
f.write(result.stdout)
|
|
41
|
+
print(f"✅ Merged configuration written to {output_file}")
|
|
42
|
+
return True
|
|
43
|
+
except subprocess.CalledProcessError as e:
|
|
44
|
+
print(f"❌ Error merging compose files: {e.stderr}")
|
|
45
|
+
raise
|
|
46
|
+
|
|
47
|
+
def fix_paths(
|
|
48
|
+
self,
|
|
49
|
+
file_path,
|
|
50
|
+
target_pattern=r"/home/runner/work/[^/]*/[^/]*",
|
|
51
|
+
replacement=".",
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Replace absolute CI paths with relative paths.
|
|
55
|
+
Uses sed-like logic but in Python.
|
|
56
|
+
"""
|
|
57
|
+
import re
|
|
58
|
+
|
|
59
|
+
with open(file_path, "r") as f:
|
|
60
|
+
content = f.read()
|
|
61
|
+
|
|
62
|
+
# Regex replacement
|
|
63
|
+
new_content = re.sub(target_pattern, replacement, content)
|
|
64
|
+
|
|
65
|
+
if content != new_content:
|
|
66
|
+
with open(file_path, "w") as f:
|
|
67
|
+
f.write(new_content)
|
|
68
|
+
print(f"✅ Fixed paths in {file_path}")
|
|
69
|
+
else:
|
|
70
|
+
self._log("No path replacements needed.")
|
|
71
|
+
|
|
72
|
+
def strip_service_volumes(self, file_path, service_name):
|
|
73
|
+
"""
|
|
74
|
+
Load YAML, remove 'volumes' from the specified service, and write back.
|
|
75
|
+
Also remove nginx-specific hardcoded volume keys like 'nginx.conf' if present
|
|
76
|
+
in a way that pyyaml doesn't mess up.
|
|
77
|
+
Actually, let's use a pure YAML approach.
|
|
78
|
+
"""
|
|
79
|
+
with open(file_path, "r") as f:
|
|
80
|
+
data = yaml.safe_load(f)
|
|
81
|
+
|
|
82
|
+
services = data.get("services", {})
|
|
83
|
+
if service_name in services:
|
|
84
|
+
service = services[service_name]
|
|
85
|
+
if "volumes" in service:
|
|
86
|
+
del service["volumes"]
|
|
87
|
+
print(f"✅ Stripped volumes from service '{service_name}'")
|
|
88
|
+
|
|
89
|
+
# Write back
|
|
90
|
+
with open(file_path, "w") as f:
|
|
91
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
92
|
+
else:
|
|
93
|
+
self._log(f"Service '{service_name}' has no volumes to strip.")
|
|
94
|
+
else:
|
|
95
|
+
self._log(f"Service '{service_name}' not found in compose file.")
|
|
96
|
+
|
|
97
|
+
def prepare_release(
|
|
98
|
+
self, base_file, prod_file, output_file, strip_volumes=None, fix_paths=True
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Orchestrate the release preparation.
|
|
102
|
+
"""
|
|
103
|
+
files_to_merge = []
|
|
104
|
+
if os.path.exists(base_file):
|
|
105
|
+
files_to_merge.append(base_file)
|
|
106
|
+
|
|
107
|
+
# Check prod file variants
|
|
108
|
+
if os.path.exists(prod_file):
|
|
109
|
+
files_to_merge.append(prod_file)
|
|
110
|
+
elif os.path.exists(os.path.basename(prod_file)):
|
|
111
|
+
files_to_merge.append(os.path.basename(prod_file))
|
|
112
|
+
|
|
113
|
+
if not files_to_merge:
|
|
114
|
+
raise FileNotFoundError("No valid docker-compose files found to merge.")
|
|
115
|
+
|
|
116
|
+
self.merge_compose_files(files_to_merge, output_file)
|
|
117
|
+
|
|
118
|
+
if fix_paths:
|
|
119
|
+
self.fix_paths(output_file)
|
|
120
|
+
|
|
121
|
+
if strip_volumes:
|
|
122
|
+
for svc in strip_volumes.split(","):
|
|
123
|
+
self.strip_service_volumes(output_file, svc.strip())
|
|
@@ -215,25 +215,36 @@ class SetupRunner:
|
|
|
215
215
|
# Check volumes (bind mounts)
|
|
216
216
|
volumes = service.get("volumes", [])
|
|
217
217
|
for vol in volumes:
|
|
218
|
+
host_path = None
|
|
218
219
|
if isinstance(vol, str):
|
|
219
220
|
parts = vol.split(":")
|
|
220
221
|
if len(parts) >= 2:
|
|
221
222
|
host_path = parts[0]
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
223
|
+
elif isinstance(vol, dict):
|
|
224
|
+
# Handle object-style volume definitions
|
|
225
|
+
if vol.get("type") == "bind":
|
|
226
|
+
host_path = vol.get("source")
|
|
227
|
+
elif "source" in vol: # Sometimes type is omitted
|
|
228
|
+
host_path = vol.get("source")
|
|
229
|
+
|
|
230
|
+
if host_path:
|
|
231
|
+
# Check if it's a relative path bind mount
|
|
232
|
+
# Normalize path for existence check
|
|
233
|
+
if host_path.startswith("./"):
|
|
234
|
+
check_path = host_path[2:]
|
|
235
|
+
else:
|
|
236
|
+
check_path = host_path
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
host_path.startswith("./")
|
|
240
|
+
or host_path.startswith("../")
|
|
241
|
+
or not host_path.startswith("/")
|
|
242
|
+
) and os.path.exists(check_path):
|
|
243
|
+
if check_path not in found_paths:
|
|
244
|
+
found_paths.add(check_path)
|
|
245
|
+
dependencies.append(
|
|
246
|
+
{"src": check_path, "dest": check_path}
|
|
247
|
+
)
|
|
237
248
|
|
|
238
249
|
except Exception as e:
|
|
239
250
|
logger.warning(f"Failed to parse compose file for dependencies: {e}")
|
|
@@ -594,3 +605,59 @@ class SetupRunner:
|
|
|
594
605
|
if host:
|
|
595
606
|
state.mark_failed(host, service_name, str(e))
|
|
596
607
|
raise
|
|
608
|
+
|
|
609
|
+
@staticmethod
|
|
610
|
+
def setup_ci_environment(provider="github"):
|
|
611
|
+
"""
|
|
612
|
+
Configures CI environment (SSH, Inventory) based on standard env vars.
|
|
613
|
+
Inputs (Env Vars): SSH_ID_RSA, SSH_HOSTNAME, SSH_USERNAME
|
|
614
|
+
Outputs: ~/.ssh/deploy_key, inventory.yml
|
|
615
|
+
"""
|
|
616
|
+
ssh_key = os.environ.get("SSH_ID_RSA")
|
|
617
|
+
ssh_host = os.environ.get("SSH_HOSTNAME")
|
|
618
|
+
ssh_user = os.environ.get("SSH_USERNAME")
|
|
619
|
+
|
|
620
|
+
missing = []
|
|
621
|
+
if not ssh_key:
|
|
622
|
+
missing.append("SSH_ID_RSA")
|
|
623
|
+
if not ssh_host:
|
|
624
|
+
missing.append("SSH_HOSTNAME")
|
|
625
|
+
if not ssh_user:
|
|
626
|
+
missing.append("SSH_USERNAME")
|
|
627
|
+
|
|
628
|
+
if missing:
|
|
629
|
+
raise ValueError(
|
|
630
|
+
f"Missing required environment variables: {', '.join(missing)}"
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# 1. Setup SSH Key
|
|
634
|
+
ssh_dir = os.path.expanduser("~/.ssh")
|
|
635
|
+
os.makedirs(ssh_dir, exist_ok=True)
|
|
636
|
+
key_path = os.path.join(ssh_dir, "deploy_key")
|
|
637
|
+
|
|
638
|
+
# Clean newlines if potentially mangled
|
|
639
|
+
ssh_key_clean = ssh_key.replace(r"\n", "\n")
|
|
640
|
+
|
|
641
|
+
with open(key_path, "w") as f:
|
|
642
|
+
f.write(ssh_key_clean)
|
|
643
|
+
os.chmod(key_path, 0o600)
|
|
644
|
+
logger.info(f"✅ SSH key written to {key_path}")
|
|
645
|
+
|
|
646
|
+
# 2. Create Inventory
|
|
647
|
+
inventory_content = {
|
|
648
|
+
"all": {
|
|
649
|
+
"hosts": {
|
|
650
|
+
"production": {
|
|
651
|
+
"ansible_host": ssh_host,
|
|
652
|
+
"ansible_user": ssh_user,
|
|
653
|
+
"ansible_ssh_private_key_file": key_path,
|
|
654
|
+
# Using standard CI strict checking disabled for automation
|
|
655
|
+
"ansible_ssh_common_args": "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=60 -o ServerAliveCountMax=10",
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
with open("inventory.yml", "w") as f:
|
|
662
|
+
yaml.dump(inventory_content, f)
|
|
663
|
+
logger.info("✅ inventory.yml created")
|
|
@@ -40,6 +40,18 @@
|
|
|
40
40
|
loop: "{{ FILES_TO_COPY | default([]) }}"
|
|
41
41
|
when: FILES_TO_COPY is defined and (item.dest | dirname) != ''
|
|
42
42
|
|
|
43
|
+
- name: Clean up conflicting directories
|
|
44
|
+
shell: |
|
|
45
|
+
target="{{ project_dest_dir }}/{{ item.dest }}"
|
|
46
|
+
if [ -d "$target" ]; then
|
|
47
|
+
rm -rf "$target"
|
|
48
|
+
echo "removed_directory"
|
|
49
|
+
fi
|
|
50
|
+
loop: "{{ FILES_TO_COPY | default([]) }}"
|
|
51
|
+
register: dir_cleanup
|
|
52
|
+
changed_when: "'removed_directory' in dir_cleanup.stdout"
|
|
53
|
+
when: FILES_TO_COPY is defined
|
|
54
|
+
|
|
43
55
|
- name: Copy dependency files
|
|
44
56
|
copy:
|
|
45
57
|
src: "{{ SOURCE_PATH }}/{{ item.src }}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|