deployml-core 0.0.58__tar.gz → 0.0.59__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.
- {deployml_core-0.0.58 → deployml_core-0.0.59}/PKG-INFO +1 -1
- {deployml_core-0.0.58 → deployml_core-0.0.59}/pyproject.toml +1 -1
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/cli/cli.py +153 -6
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/fastapi/Dockerfile +3 -2
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/fastapi/main.py +3 -3
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/mlflow/Dockerfile +3 -2
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/mlflow_main.tf.j2 +31 -38
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/bigquery/cloud/gcp/main.tf +4 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/main.tf +3 -4
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/main.tf +8 -1
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/helpers.py +49 -87
- {deployml_core-0.0.58 → deployml_core-0.0.59}/LICENSE +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/README.md +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/api.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/cli/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/diagnostics/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/diagnostics/doctor.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/fastapi/.dockerignore +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/fastapi/requirements.txt +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/grafana-container/Dockerfile +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/grafana-container/README.md +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/grafana-container/entrypoint.sh +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/mlflow/requirements.txt +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/enum/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/enum/cloud_provider.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/enum/deployment_type.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/notebook/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/notebook/deployment.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/notebook/display.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/notebook/docker.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/notebook/stack.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/notebook/urls.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/notebook.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/base_main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/terraform.tfvars.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/variables.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/wandb_main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/terraform.tfvars.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/variables.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/deployment.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/mlflow-deployment.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/mlflow-service.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/service.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/bigquery/cloud/gcp/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/drift_metrics.json +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/ground_truth.json +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/predictions.json +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/bigquery/cloud/gcp/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/feast_env.tpl +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/main.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/requirements.txt +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloudbuild.yaml +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/teardown_script.sh +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/banner.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/constants.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/infracost.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/kubernetes_gke.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/kubernetes_local.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/menu.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/utils/teardown.py +0 -0
|
@@ -788,6 +788,9 @@ def deploy(
|
|
|
788
788
|
generate_only: bool = typer.Option(
|
|
789
789
|
False, "--generate-only", "-g", help="Only generate manifests, do not apply (for GKE deployments)"
|
|
790
790
|
),
|
|
791
|
+
verbose: bool = typer.Option(
|
|
792
|
+
False, "--verbose", "-v", help="Stream Terraform logs to stdout instead of showing progress bar"
|
|
793
|
+
),
|
|
791
794
|
):
|
|
792
795
|
"""
|
|
793
796
|
Deploy infrastructure based on a YAML configuration file.
|
|
@@ -1027,6 +1030,34 @@ def deploy(
|
|
|
1027
1030
|
|
|
1028
1031
|
typer.echo(f" Unified bucket creation: {create_artifact_bucket}")
|
|
1029
1032
|
|
|
1033
|
+
# Auto-resolve image URIs: if a service image is not specified in the config (or still
|
|
1034
|
+
# points to a legacy gcr.io path), derive it from the Artifact Registry path that
|
|
1035
|
+
# `deployml build-images` produces: {region}-docker.pkg.dev/{project}/mlops-images/{name}
|
|
1036
|
+
# This keeps the YAML config free of hardcoded image URIs — project/region are enough.
|
|
1037
|
+
_TOOL_IMAGE_NAMES = {
|
|
1038
|
+
"mlflow": "mlflow",
|
|
1039
|
+
"feast": "feast",
|
|
1040
|
+
"fastapi": "fastapi",
|
|
1041
|
+
"grafana": "grafana-container",
|
|
1042
|
+
"wandb": "wandb",
|
|
1043
|
+
}
|
|
1044
|
+
_ar_base = f"{region}-docker.pkg.dev/{project_id}/mlops-images"
|
|
1045
|
+
for stage in stack:
|
|
1046
|
+
for stage_name, tool in stage.items():
|
|
1047
|
+
tool_name = tool.get("name", "")
|
|
1048
|
+
params = tool.setdefault("params", {})
|
|
1049
|
+
existing_image = params.get("image", "")
|
|
1050
|
+
if not existing_image or existing_image.startswith("gcr.io/"):
|
|
1051
|
+
image_name = _TOOL_IMAGE_NAMES.get(tool_name)
|
|
1052
|
+
if image_name:
|
|
1053
|
+
params["image"] = f"{_ar_base}/{image_name}:latest"
|
|
1054
|
+
# Cron job images are per-job and must be set explicitly — skip here
|
|
1055
|
+
if stage_name == "workflow_orchestration" and tool_name == "cron":
|
|
1056
|
+
for job in params.get("jobs", []):
|
|
1057
|
+
if not job.get("image") or job.get("image", "").startswith("gcr.io/"):
|
|
1058
|
+
job_name = job.get("service_name", "")
|
|
1059
|
+
job["image"] = f"{_ar_base}/{job_name}:latest"
|
|
1060
|
+
|
|
1030
1061
|
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR))
|
|
1031
1062
|
# PATCH: Use wandb_main.tf.j2 or mlflow_main.tf.j2 for cloud_run if present
|
|
1032
1063
|
if deployment_type == "cloud_run":
|
|
@@ -1253,6 +1284,7 @@ def deploy(
|
|
|
1253
1284
|
["terraform", "apply", "-auto-approve"],
|
|
1254
1285
|
DEPLOYML_TERRAFORM_DIR,
|
|
1255
1286
|
minutes,
|
|
1287
|
+
verbose=verbose,
|
|
1256
1288
|
)
|
|
1257
1289
|
if result_code == 0:
|
|
1258
1290
|
typer.echo(" Deployment complete!")
|
|
@@ -1421,6 +1453,84 @@ def deploy(
|
|
|
1421
1453
|
typer.echo(" Deployment cancelled")
|
|
1422
1454
|
|
|
1423
1455
|
|
|
1456
|
+
@cli.command()
|
|
1457
|
+
def get_urls(
|
|
1458
|
+
config_path: Path = typer.Option(
|
|
1459
|
+
..., "--config-path", "-c", help="Path to YAML config file"
|
|
1460
|
+
),
|
|
1461
|
+
env_path: Path = typer.Option(
|
|
1462
|
+
Path(".env"), "--env-path", help="Path to write .env file"
|
|
1463
|
+
),
|
|
1464
|
+
):
|
|
1465
|
+
"""
|
|
1466
|
+
Print service URLs from the last deployment and write them to a .env file.
|
|
1467
|
+
"""
|
|
1468
|
+
if not config_path.exists():
|
|
1469
|
+
typer.echo(f" Config file not found: {config_path}")
|
|
1470
|
+
raise typer.Exit(code=1)
|
|
1471
|
+
|
|
1472
|
+
config = yaml.safe_load(config_path.read_text())
|
|
1473
|
+
workspace_name = config.get("name") or "development"
|
|
1474
|
+
terraform_dir = Path.cwd() / ".deployml" / workspace_name / "terraform"
|
|
1475
|
+
|
|
1476
|
+
if not terraform_dir.exists():
|
|
1477
|
+
typer.echo(f" No deployment found at {terraform_dir}. Run 'deployml deploy' first.")
|
|
1478
|
+
raise typer.Exit(code=1)
|
|
1479
|
+
|
|
1480
|
+
output_proc = subprocess.run(
|
|
1481
|
+
["terraform", "output", "-json"],
|
|
1482
|
+
cwd=terraform_dir,
|
|
1483
|
+
capture_output=True,
|
|
1484
|
+
text=True,
|
|
1485
|
+
)
|
|
1486
|
+
if output_proc.returncode != 0:
|
|
1487
|
+
typer.echo(" Could not retrieve Terraform outputs. Is the stack deployed?")
|
|
1488
|
+
raise typer.Exit(code=1)
|
|
1489
|
+
|
|
1490
|
+
try:
|
|
1491
|
+
outputs = json.loads(output_proc.stdout)
|
|
1492
|
+
except Exception:
|
|
1493
|
+
typer.echo(" Failed to parse Terraform outputs.")
|
|
1494
|
+
raise typer.Exit(code=1)
|
|
1495
|
+
|
|
1496
|
+
if not outputs:
|
|
1497
|
+
typer.echo("No outputs found. Is the stack deployed?")
|
|
1498
|
+
raise typer.Exit(code=1)
|
|
1499
|
+
|
|
1500
|
+
typer.echo("\n DeployML Outputs:")
|
|
1501
|
+
env_lines = []
|
|
1502
|
+
for key, value in outputs.items():
|
|
1503
|
+
is_sensitive = value.get("sensitive", False)
|
|
1504
|
+
output_val = value.get("value")
|
|
1505
|
+
env_key = key.upper()
|
|
1506
|
+
if is_sensitive:
|
|
1507
|
+
typer.secho(f" {key}: [SENSITIVE] (value hidden)", fg=typer.colors.YELLOW)
|
|
1508
|
+
elif isinstance(output_val, str):
|
|
1509
|
+
if output_val.startswith("http://") or output_val.startswith("https://"):
|
|
1510
|
+
typer.secho(f" {key}: {output_val}", fg=typer.colors.BRIGHT_BLUE, bold=True)
|
|
1511
|
+
env_lines.append(f"{env_key}={output_val}")
|
|
1512
|
+
elif output_val == "":
|
|
1513
|
+
typer.secho(f" {key}: [No value]", fg=typer.colors.YELLOW)
|
|
1514
|
+
else:
|
|
1515
|
+
typer.echo(f" {key}: {output_val}")
|
|
1516
|
+
env_lines.append(f"{env_key}={output_val}")
|
|
1517
|
+
elif isinstance(output_val, dict):
|
|
1518
|
+
typer.echo(f" {key}:")
|
|
1519
|
+
for subkey, subval in output_val.items():
|
|
1520
|
+
sub_env_key = f"{env_key}_{subkey.upper()}"
|
|
1521
|
+
if isinstance(subval, str) and (subval.startswith("http://") or subval.startswith("https://")):
|
|
1522
|
+
typer.secho(f" {subkey}: {subval}", fg=typer.colors.BRIGHT_BLUE, bold=True)
|
|
1523
|
+
env_lines.append(f"{sub_env_key}={subval}")
|
|
1524
|
+
elif isinstance(subval, str) and subval:
|
|
1525
|
+
typer.echo(f" {subkey}: {subval}")
|
|
1526
|
+
env_lines.append(f"{sub_env_key}={subval}")
|
|
1527
|
+
else:
|
|
1528
|
+
typer.echo(f" {key}: {output_val}")
|
|
1529
|
+
|
|
1530
|
+
env_path.write_text("\n".join(env_lines) + "\n")
|
|
1531
|
+
typer.echo(f"\n .env written to {env_path.resolve()}")
|
|
1532
|
+
|
|
1533
|
+
|
|
1424
1534
|
@cli.command()
|
|
1425
1535
|
def destroy(
|
|
1426
1536
|
config_path: Path = typer.Option(
|
|
@@ -1487,16 +1597,53 @@ def destroy(
|
|
|
1487
1597
|
cwd=DEPLOYML_TERRAFORM_DIR,
|
|
1488
1598
|
)
|
|
1489
1599
|
|
|
1490
|
-
#
|
|
1491
|
-
|
|
1492
|
-
|
|
1600
|
+
# Shut down Cloud Run services first to close any open DB connections
|
|
1601
|
+
# before attempting to destroy Cloud SQL — otherwise active connections
|
|
1602
|
+
# prevent database/user deletion and the destroy fails.
|
|
1603
|
+
region = config.get("provider", {}).get("region", "us-central1")
|
|
1604
|
+
cr_result = subprocess.run(
|
|
1605
|
+
["gcloud", "run", "services", "list",
|
|
1606
|
+
"--project", project_id,
|
|
1607
|
+
"--region", region,
|
|
1608
|
+
"--format", "value(metadata.name)"],
|
|
1609
|
+
capture_output=True, text=True
|
|
1610
|
+
)
|
|
1611
|
+
if cr_result.returncode == 0:
|
|
1612
|
+
services = [s.strip() for s in cr_result.stdout.splitlines() if s.strip()]
|
|
1613
|
+
for service in services:
|
|
1614
|
+
typer.echo(f" Deleting Cloud Run service: {service}")
|
|
1615
|
+
subprocess.run(
|
|
1616
|
+
["gcloud", "run", "services", "delete", service,
|
|
1617
|
+
"--project", project_id,
|
|
1618
|
+
"--region", region,
|
|
1619
|
+
"--quiet"],
|
|
1620
|
+
capture_output=True,
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
# Remove Cloud SQL databases and user from Terraform state so Terraform
|
|
1624
|
+
# doesn't try to delete them individually — the instance deletion handles
|
|
1625
|
+
# that automatically, avoiding active-connection errors on destroy.
|
|
1626
|
+
state_result = subprocess.run(
|
|
1627
|
+
["terraform", "state", "list"],
|
|
1493
1628
|
cwd=DEPLOYML_TERRAFORM_DIR,
|
|
1494
1629
|
capture_output=True,
|
|
1495
1630
|
text=True,
|
|
1496
1631
|
)
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1632
|
+
if state_result.returncode == 0:
|
|
1633
|
+
resources_to_remove = [
|
|
1634
|
+
r.strip() for r in state_result.stdout.splitlines()
|
|
1635
|
+
if any(x in r for x in [
|
|
1636
|
+
"google_sql_database.",
|
|
1637
|
+
"google_sql_user.",
|
|
1638
|
+
])
|
|
1639
|
+
]
|
|
1640
|
+
for resource in resources_to_remove:
|
|
1641
|
+
typer.echo(f" Removing from state: {resource}")
|
|
1642
|
+
subprocess.run(
|
|
1643
|
+
["terraform", "state", "rm", resource],
|
|
1644
|
+
cwd=DEPLOYML_TERRAFORM_DIR,
|
|
1645
|
+
capture_output=True,
|
|
1646
|
+
)
|
|
1500
1647
|
|
|
1501
1648
|
# Build destroy command
|
|
1502
1649
|
cmd = ["terraform", "destroy", "--auto-approve"]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Build for Linux/amd64 platform (not Apple Silicon)
|
|
2
|
-
FROM
|
|
2
|
+
FROM python:3.9-slim
|
|
3
3
|
|
|
4
4
|
# Set working directory
|
|
5
5
|
WORKDIR /app
|
|
@@ -34,4 +34,5 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
|
34
34
|
CMD curl -f http://localhost:8000/health || exit 1
|
|
35
35
|
|
|
36
36
|
# Run the application
|
|
37
|
-
|
|
37
|
+
# Uses PORT env var so it works on Cloud Run (PORT=8080) and locally (PORT=8000)
|
|
38
|
+
CMD uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}
|
|
@@ -14,9 +14,9 @@ app = FastAPI(
|
|
|
14
14
|
version="1.0.0"
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
MLFLOW_TRACKING_URI = "http://mlflow-service:5000"
|
|
19
|
-
MODEL_NAME = "HousingPriceModel"
|
|
17
|
+
# MLflow configuration — override via MLFLOW_TRACKING_URI env var
|
|
18
|
+
MLFLOW_TRACKING_URI = os.getenv("MLFLOW_TRACKING_URI", "http://mlflow-service:5000")
|
|
19
|
+
MODEL_NAME = os.getenv("MODEL_NAME", "HousingPriceModel")
|
|
20
20
|
|
|
21
21
|
# Global model variable
|
|
22
22
|
model = None
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Build for Linux/amd64 platform (not Apple Silicon)
|
|
2
|
-
FROM
|
|
2
|
+
FROM python:3.11-slim
|
|
3
3
|
|
|
4
4
|
# Set working directory
|
|
5
5
|
WORKDIR /app
|
|
@@ -35,5 +35,6 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
|
35
35
|
CMD curl -f http://localhost:5000/health || exit 1
|
|
36
36
|
|
|
37
37
|
# Default command - MLflow server
|
|
38
|
-
|
|
38
|
+
# Uses PORT env var so it works on Cloud Run (PORT=8080) and locally (PORT=5000)
|
|
39
|
+
CMD mlflow server --host 0.0.0.0 --port ${PORT:-5000} --dev
|
|
39
40
|
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/mlflow_main.tf.j2
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{% extends "gcp/cloud_run/base_main.tf.j2" %}
|
|
2
2
|
|
|
3
|
-
{% set flags = namespace(needs_postgres=false) %}
|
|
3
|
+
{% set flags = namespace(needs_postgres=false, has_grafana=false) %}
|
|
4
4
|
{% for stage in stack %}
|
|
5
5
|
{% for stage_name, tool in stage.items() %}
|
|
6
6
|
{% if tool.name == "mlflow" and tool.params.get("backend_store_uri", "") == "postgresql" %}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
{% if tool.name == "feast" and tool.params.get("backend_store_uri", "") == "postgresql" %}
|
|
10
10
|
{% set flags.needs_postgres = true %}
|
|
11
11
|
{% endif %}
|
|
12
|
+
{% if stage_name == "model_monitoring" and tool.name == "grafana" %}
|
|
13
|
+
{% set flags.has_grafana = true %}
|
|
14
|
+
{% endif %}
|
|
12
15
|
{% endfor %}
|
|
13
16
|
{% endfor %}
|
|
14
17
|
|
|
@@ -44,6 +47,8 @@ module "cloud_sql_postgres" {
|
|
|
44
47
|
db_instance_name = "mlflow-postgres-{{ project_id }}"
|
|
45
48
|
db_name = "mlflow"
|
|
46
49
|
db_user = "mlflow"
|
|
50
|
+
# Create the metrics DB when Grafana is in the stack so it has a backend to connect to
|
|
51
|
+
create_metrics_db = {{ "true" if flags.has_grafana else "false" }}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
|
|
@@ -288,69 +293,57 @@ module "{{ stage_name }}_{{ tool.name }}" {
|
|
|
288
293
|
{% endif %}
|
|
289
294
|
{% endfor %}
|
|
290
295
|
{% endfor %}
|
|
296
|
+
module "bigquery" {
|
|
297
|
+
source = "./modules/bigquery/cloud/gcp"
|
|
298
|
+
project_id = var.project_id
|
|
299
|
+
region = var.region
|
|
300
|
+
dataset_id = "mlops"
|
|
301
|
+
}
|
|
291
302
|
{% endblock %}
|
|
292
303
|
|
|
293
304
|
{% block outputs %}
|
|
305
|
+
# MLflow — single URL and bucket regardless of how many stack stages use it
|
|
306
|
+
{% set mlflow_stage = namespace(found=false, name="") %}
|
|
294
307
|
{% for stage in stack %}
|
|
295
308
|
{% for stage_name, tool in stage.items() %}
|
|
296
|
-
{% if tool.name == "mlflow" %}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
output "{{ stage_name }}_mlflow_bucket" {
|
|
301
|
-
value = var.enable_{{ stage_name }}_mlflow && length(module.{{ stage_name }}_mlflow) > 0 ? module.{{ stage_name }}_mlflow[0].bucket_name : ""
|
|
302
|
-
}
|
|
309
|
+
{% if tool.name == "mlflow" and not mlflow_stage.found %}
|
|
310
|
+
{% set mlflow_stage.found = true %}
|
|
311
|
+
{% set mlflow_stage.name = stage_name %}
|
|
303
312
|
{% endif %}
|
|
304
313
|
{% endfor %}
|
|
305
314
|
{% endfor %}
|
|
306
|
-
{% if
|
|
307
|
-
|
|
308
|
-
{
|
|
309
|
-
|
|
310
|
-
output "
|
|
311
|
-
value =
|
|
315
|
+
{% if mlflow_stage.found %}
|
|
316
|
+
output "mlflow_url" {
|
|
317
|
+
value = var.enable_{{ mlflow_stage.name }}_mlflow && length(module.{{ mlflow_stage.name }}_mlflow) > 0 ? module.{{ mlflow_stage.name }}_mlflow[0].service_url : ""
|
|
318
|
+
}
|
|
319
|
+
output "artifact_bucket" {
|
|
320
|
+
value = var.enable_{{ mlflow_stage.name }}_mlflow && length(module.{{ mlflow_stage.name }}_mlflow) > 0 ? module.{{ mlflow_stage.name }}_mlflow[0].bucket_name : ""
|
|
312
321
|
}
|
|
313
|
-
{% endif %}
|
|
314
|
-
{% endfor %}
|
|
315
|
-
{% endfor %}
|
|
316
322
|
{% endif %}
|
|
317
323
|
{% if flags.needs_postgres %}
|
|
318
|
-
output "instance_connection_name" {
|
|
319
|
-
value = module.cloud_sql_postgres.instance_connection_name
|
|
320
|
-
}
|
|
321
324
|
output "postgresql_credentials" {
|
|
322
325
|
value = module.cloud_sql_postgres.postgresql_credentials
|
|
323
326
|
sensitive = true
|
|
324
327
|
}
|
|
325
328
|
{% endif %}
|
|
329
|
+
output "bigquery_dataset" {
|
|
330
|
+
value = module.bigquery.dataset_id
|
|
331
|
+
}
|
|
326
332
|
|
|
327
|
-
# Output for Grafana if present
|
|
328
333
|
{% for stage in stack %}
|
|
329
334
|
{% for stage_name, tool in stage.items() %}
|
|
330
|
-
{% if stage_name == "
|
|
331
|
-
output "
|
|
335
|
+
{% if stage_name == "model_serving" and tool.name == "fastapi" %}
|
|
336
|
+
output "fastapi_url" {
|
|
332
337
|
value = length(module.{{ stage_name }}_{{ tool.name }}) > 0 ? module.{{ stage_name }}_{{ tool.name }}[0].service_url : ""
|
|
333
338
|
}
|
|
334
339
|
{% endif %}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
# Output for FastAPI model serving if present
|
|
339
|
-
{% for stage in stack %}
|
|
340
|
-
{% for stage_name, tool in stage.items() %}
|
|
341
|
-
{% if stage_name == "model_serving" and tool.name == "fastapi" %}
|
|
342
|
-
output "{{ stage_name }}_{{ tool.name }}_url" {
|
|
340
|
+
{% if stage_name == "model_monitoring" and tool.name == "grafana" %}
|
|
341
|
+
output "grafana_url" {
|
|
343
342
|
value = length(module.{{ stage_name }}_{{ tool.name }}) > 0 ? module.{{ stage_name }}_{{ tool.name }}[0].service_url : ""
|
|
344
343
|
}
|
|
345
344
|
{% endif %}
|
|
346
|
-
{% endfor %}
|
|
347
|
-
{% endfor %}
|
|
348
|
-
|
|
349
|
-
# Output for Feast feature store if present
|
|
350
|
-
{% for stage in stack %}
|
|
351
|
-
{% for stage_name, tool in stage.items() %}
|
|
352
345
|
{% if stage_name == "feature_store" and tool.name == "feast" %}
|
|
353
|
-
output "
|
|
346
|
+
output "feast_url" {
|
|
354
347
|
value = length(module.{{ stage_name }}_{{ tool.name }}) > 0 ? module.{{ stage_name }}_{{ tool.name }}[0].service_url : ""
|
|
355
348
|
}
|
|
356
349
|
{% endif %}
|
|
@@ -10,6 +10,7 @@ resource "google_bigquery_dataset" "mlops" {
|
|
|
10
10
|
project = var.project_id
|
|
11
11
|
|
|
12
12
|
schema = file("${path.module}/schemas/drift_metrics.json")
|
|
13
|
+
deletion_protection = false
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
resource "google_bigquery_table" "ground_truth" {
|
|
@@ -18,6 +19,7 @@ resource "google_bigquery_table" "ground_truth" {
|
|
|
18
19
|
project = var.project_id
|
|
19
20
|
|
|
20
21
|
schema = file("${path.module}/schemas/ground_truth.json")
|
|
22
|
+
deletion_protection = false
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
resource "google_bigquery_table" "offline_features" {
|
|
@@ -26,6 +28,7 @@ resource "google_bigquery_table" "offline_features" {
|
|
|
26
28
|
project = var.project_id
|
|
27
29
|
|
|
28
30
|
schema = file("${path.module}/schemas/offline_features.json")
|
|
31
|
+
deletion_protection = false
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
resource "google_bigquery_table" "predictions" {
|
|
@@ -34,5 +37,6 @@ resource "google_bigquery_table" "predictions" {
|
|
|
34
37
|
project = var.project_id
|
|
35
38
|
|
|
36
39
|
schema = file("${path.module}/schemas/predictions.json")
|
|
40
|
+
deletion_protection = false
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -90,7 +90,7 @@ resource "google_sql_database" "feast_db" {
|
|
|
90
90
|
instance = google_sql_database_instance.postgres.name
|
|
91
91
|
project = var.project_id
|
|
92
92
|
depends_on = [null_resource.verify_instance_running]
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
lifecycle {
|
|
95
95
|
ignore_changes = [name]
|
|
96
96
|
}
|
|
@@ -102,7 +102,7 @@ resource "google_sql_database" "metrics_db" {
|
|
|
102
102
|
instance = google_sql_database_instance.postgres.name
|
|
103
103
|
project = var.project_id
|
|
104
104
|
depends_on = [null_resource.verify_instance_running]
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
lifecycle {
|
|
107
107
|
ignore_changes = [name]
|
|
108
108
|
}
|
|
@@ -114,9 +114,8 @@ resource "google_sql_user" "users" {
|
|
|
114
114
|
password = random_password.db_password.result
|
|
115
115
|
project = var.project_id
|
|
116
116
|
depends_on = [null_resource.verify_instance_running]
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
lifecycle {
|
|
119
|
-
# Retry on failure in case instance is still starting
|
|
120
119
|
create_before_destroy = true
|
|
121
120
|
}
|
|
122
121
|
}
|
|
@@ -38,11 +38,18 @@ resource "google_cloud_run_service" "mlflow" {
|
|
|
38
38
|
name = "MLFLOW_SERVER_HOST"
|
|
39
39
|
value = "0.0.0.0"
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
env {
|
|
43
43
|
name = "MLFLOW_SERVER_PORT"
|
|
44
44
|
value = "8080"
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
# Allow all host headers — required for Cloud Run since the Host header
|
|
48
|
+
# is the dynamic *.run.app URL which MLflow's DNS rebinding check rejects by default
|
|
49
|
+
env {
|
|
50
|
+
name = "MLFLOW_SERVER_ALLOWED_HOSTS"
|
|
51
|
+
value = "*"
|
|
52
|
+
}
|
|
46
53
|
|
|
47
54
|
# Backend store URI
|
|
48
55
|
dynamic "env" {
|
|
@@ -99,9 +99,19 @@ def copy_modules_to_workspace(
|
|
|
99
99
|
if teardown_enabled:
|
|
100
100
|
used_modules.add("teardown")
|
|
101
101
|
|
|
102
|
+
# BigQuery is always included — provides the mlops dataset and tables
|
|
103
|
+
used_modules.add("bigquery")
|
|
104
|
+
|
|
102
105
|
# Only copy the modules that are being used, and only the specific deployment type
|
|
103
106
|
for module_path in MODULE_TEMPLATES_DIR.iterdir():
|
|
104
107
|
if module_path.is_dir() and module_path.name in used_modules:
|
|
108
|
+
# Special case: always copy full bigquery module
|
|
109
|
+
if module_path.name == "bigquery":
|
|
110
|
+
dest_module_path = modules_dir / module_path.name
|
|
111
|
+
if dest_module_path.exists():
|
|
112
|
+
shutil.rmtree(dest_module_path)
|
|
113
|
+
shutil.copytree(module_path, dest_module_path)
|
|
114
|
+
continue
|
|
105
115
|
# Special case: always copy full cloud_sql_postgres module
|
|
106
116
|
if module_path.name == "cloud_sql_postgres":
|
|
107
117
|
dest_module_path = modules_dir / module_path.name
|
|
@@ -257,97 +267,38 @@ def estimate_terraform_time(plan_output: str, operation: str = "apply") -> str:
|
|
|
257
267
|
|
|
258
268
|
def cleanup_cloud_sql_resources(terraform_dir: Path, project_id: str):
|
|
259
269
|
"""
|
|
260
|
-
|
|
270
|
+
Terminate all Cloud SQL connections before destroy so Terraform can cleanly
|
|
271
|
+
delete databases and users. We just restart the instance — that kills all
|
|
272
|
+
active connections — and let Terraform handle the actual resource deletion.
|
|
261
273
|
"""
|
|
262
274
|
import subprocess
|
|
263
275
|
import time as _time
|
|
264
276
|
|
|
265
277
|
try:
|
|
266
|
-
# Get the instance name from terraform state
|
|
267
278
|
result = subprocess.run(
|
|
268
279
|
["terraform", "output", "-raw", "instance_connection_name"],
|
|
269
280
|
cwd=terraform_dir,
|
|
270
281
|
capture_output=True,
|
|
271
282
|
text=True,
|
|
272
283
|
)
|
|
273
|
-
if result.returncode
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
]
|
|
292
|
-
subprocess.run(restart_cmd, capture_output=True, text=True)
|
|
293
|
-
_time.sleep(5)
|
|
294
|
-
|
|
295
|
-
# List existing databases to avoid failing on non-existent ones
|
|
296
|
-
list_db_cmd = [
|
|
297
|
-
"gcloud",
|
|
298
|
-
"sql",
|
|
299
|
-
"databases",
|
|
300
|
-
"list",
|
|
301
|
-
"--instance",
|
|
302
|
-
instance_name,
|
|
303
|
-
"--project",
|
|
304
|
-
project_id,
|
|
305
|
-
"--format=value(name)",
|
|
306
|
-
]
|
|
307
|
-
list_proc = subprocess.run(list_db_cmd, capture_output=True, text=True)
|
|
308
|
-
existing_dbs = set(list_proc.stdout.strip().splitlines()) if list_proc.returncode == 0 else set()
|
|
309
|
-
|
|
310
|
-
# Attempt to delete known course DBs if present
|
|
311
|
-
target_dbs = ["mlflow", "feast", "metrics"]
|
|
312
|
-
for db_name in target_dbs:
|
|
313
|
-
if db_name in existing_dbs:
|
|
314
|
-
delete_db_cmd = [
|
|
315
|
-
"gcloud",
|
|
316
|
-
"sql",
|
|
317
|
-
"databases",
|
|
318
|
-
"delete",
|
|
319
|
-
db_name,
|
|
320
|
-
"--instance",
|
|
321
|
-
instance_name,
|
|
322
|
-
"--project",
|
|
323
|
-
project_id,
|
|
324
|
-
"--quiet",
|
|
325
|
-
]
|
|
326
|
-
# Retry a few times in case connections are still draining
|
|
327
|
-
for attempt in range(3):
|
|
328
|
-
proc = subprocess.run(delete_db_cmd, capture_output=True, text=True)
|
|
329
|
-
if proc.returncode == 0:
|
|
330
|
-
break
|
|
331
|
-
_time.sleep(5)
|
|
332
|
-
|
|
333
|
-
# Drop common users after DBs are removed
|
|
334
|
-
target_users = ["mlflow", "feast", "metrics"]
|
|
335
|
-
for user in target_users:
|
|
336
|
-
drop_user_cmd = [
|
|
337
|
-
"gcloud",
|
|
338
|
-
"sql",
|
|
339
|
-
"users",
|
|
340
|
-
"delete",
|
|
341
|
-
user,
|
|
342
|
-
"--instance",
|
|
343
|
-
instance_name,
|
|
344
|
-
"--project",
|
|
345
|
-
project_id,
|
|
346
|
-
"--quiet",
|
|
347
|
-
]
|
|
348
|
-
subprocess.run(drop_user_cmd, capture_output=True, text=True)
|
|
349
|
-
|
|
350
|
-
print("✅ Cloud SQL cleanup completed (best-effort)")
|
|
284
|
+
if result.returncode != 0:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
instance_connection_name = result.stdout.strip()
|
|
288
|
+
parts = instance_connection_name.split(":")
|
|
289
|
+
instance_name = parts[2] if len(parts) == 3 else instance_connection_name
|
|
290
|
+
|
|
291
|
+
print(f"🗄️ Restarting Cloud SQL instance to close active connections: {instance_name}")
|
|
292
|
+
subprocess.run(
|
|
293
|
+
["gcloud", "sql", "instances", "restart", instance_name,
|
|
294
|
+
"--project", project_id, "--quiet"],
|
|
295
|
+
capture_output=True,
|
|
296
|
+
text=True,
|
|
297
|
+
)
|
|
298
|
+
# Give the instance time to fully restart and drop all connections
|
|
299
|
+
_time.sleep(30)
|
|
300
|
+
print("✅ Cloud SQL connections cleared")
|
|
301
|
+
|
|
351
302
|
except Exception as e:
|
|
352
303
|
print(f"⚠️ Cloud SQL cleanup failed (continuing with destroy): {e}")
|
|
353
304
|
|
|
@@ -377,7 +328,7 @@ def cleanup_terraform_files(terraform_dir: Path):
|
|
|
377
328
|
print("✅ Cleanup completed")
|
|
378
329
|
|
|
379
330
|
|
|
380
|
-
def run_terraform_with_loading_bar(cmd, cwd, estimated_minutes, stack=None):
|
|
331
|
+
def run_terraform_with_loading_bar(cmd, cwd, estimated_minutes, stack=None, verbose=False):
|
|
381
332
|
"""
|
|
382
333
|
Run a subprocess command with a loading bar using rich.progress.
|
|
383
334
|
Progress messages are based on the stack/resources from the YAML config if provided.
|
|
@@ -413,7 +364,18 @@ def run_terraform_with_loading_bar(cmd, cwd, estimated_minutes, stack=None):
|
|
|
413
364
|
|
|
414
365
|
# Log output to file for debugging
|
|
415
366
|
log_file = cwd / "terraform_apply.log"
|
|
416
|
-
|
|
367
|
+
|
|
368
|
+
if verbose:
|
|
369
|
+
with open(log_file, "w") as f:
|
|
370
|
+
process = subprocess.Popen(
|
|
371
|
+
cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
372
|
+
)
|
|
373
|
+
for line in iter(process.stdout.readline, ""):
|
|
374
|
+
print(line, end="", flush=True)
|
|
375
|
+
f.write(line)
|
|
376
|
+
returncode = process.wait()
|
|
377
|
+
return returncode
|
|
378
|
+
|
|
417
379
|
with Progress(
|
|
418
380
|
SpinnerColumn(),
|
|
419
381
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -422,7 +384,7 @@ def run_terraform_with_loading_bar(cmd, cwd, estimated_minutes, stack=None):
|
|
|
422
384
|
TimeElapsedColumn(),
|
|
423
385
|
) as progress:
|
|
424
386
|
task = progress.add_task(resource_msgs[0], total=100)
|
|
425
|
-
|
|
387
|
+
|
|
426
388
|
# Open log file and keep it open until process completes
|
|
427
389
|
f = open(log_file, "w")
|
|
428
390
|
try:
|
|
@@ -441,7 +403,7 @@ def run_terraform_with_loading_bar(cmd, cwd, estimated_minutes, stack=None):
|
|
|
441
403
|
# If we exceed estimated time, slowly approach 95%
|
|
442
404
|
excess_time = elapsed - estimated_seconds
|
|
443
405
|
progress_percent = min(95, 85 + int(excess_time / 30)) # +1% per 30 seconds
|
|
444
|
-
|
|
406
|
+
|
|
445
407
|
# Choose message based on progress
|
|
446
408
|
msg_idx = min(
|
|
447
409
|
int(progress_percent / (100 / (n_msgs - 1))), n_msgs - 2
|
|
@@ -451,17 +413,17 @@ def run_terraform_with_loading_bar(cmd, cwd, estimated_minutes, stack=None):
|
|
|
451
413
|
task, completed=progress_percent, description=message
|
|
452
414
|
)
|
|
453
415
|
time.sleep(1)
|
|
454
|
-
|
|
416
|
+
|
|
455
417
|
# Wait for process to fully complete and flush all output
|
|
456
418
|
returncode = process.wait()
|
|
457
419
|
f.flush() # Ensure all output is written
|
|
458
|
-
|
|
420
|
+
|
|
459
421
|
# Only show 100% if returncode is 0 (success)
|
|
460
422
|
if returncode == 0:
|
|
461
423
|
progress.update(task, completed=100, description=resource_msgs[-1])
|
|
462
424
|
else:
|
|
463
425
|
progress.update(task, completed=progress_percent, description=f"⚠️ Terraform apply returned code {returncode}")
|
|
464
|
-
|
|
426
|
+
|
|
465
427
|
return returncode
|
|
466
428
|
finally:
|
|
467
429
|
f.close() # Always close the file
|
|
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
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/grafana-container/Dockerfile
RENAMED
|
File without changes
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/grafana-container/README.md
RENAMED
|
File without changes
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/docker/grafana-container/entrypoint.sh
RENAMED
|
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
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/base_main.tf.j2
RENAMED
|
File without changes
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/main.tf.j2
RENAMED
|
File without changes
|
|
File without changes
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/variables.tf.j2
RENAMED
|
File without changes
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/wandb_main.tf.j2
RENAMED
|
File without changes
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/main.tf.j2
RENAMED
|
File without changes
|
|
File without changes
|
{deployml_core-0.0.58 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/variables.tf.j2
RENAMED
|
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
|