deployml-core 0.0.57__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.57 → deployml_core-0.0.59}/PKG-INFO +1 -1
- {deployml_core-0.0.57 → deployml_core-0.0.59}/pyproject.toml +1 -1
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/cli/cli.py +153 -6
- deployml_core-0.0.59/src/deployml/docker/fastapi/.dockerignore +11 -0
- deployml_core-0.0.59/src/deployml/docker/fastapi/Dockerfile +38 -0
- deployml_core-0.0.59/src/deployml/docker/fastapi/main.py +127 -0
- deployml_core-0.0.59/src/deployml/docker/fastapi/requirements.txt +7 -0
- deployml_core-0.0.59/src/deployml/docker/grafana-container/Dockerfile +23 -0
- deployml_core-0.0.59/src/deployml/docker/grafana-container/README.md +3 -0
- deployml_core-0.0.59/src/deployml/docker/grafana-container/entrypoint.sh +10 -0
- deployml_core-0.0.59/src/deployml/docker/mlflow/Dockerfile +40 -0
- deployml_core-0.0.59/src/deployml/docker/mlflow/requirements.txt +4 -0
- deployml_core-0.0.59/src/deployml/notebook/docker.py +224 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/mlflow_main.tf.j2 +31 -38
- deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/main.tf +42 -0
- deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/outputs.tf +3 -0
- deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/drift_metrics.json +9 -0
- deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/ground_truth.json +5 -0
- deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +7 -0
- deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/predictions.json +8 -0
- deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/variables.tf +11 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/main.tf +3 -4
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/main.tf +8 -1
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/helpers.py +49 -87
- {deployml_core-0.0.57 → deployml_core-0.0.59}/LICENSE +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/README.md +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/__init__.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/api.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/cli/__init__.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/diagnostics/__init__.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/diagnostics/doctor.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/enum/__init__.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/enum/cloud_provider.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/enum/deployment_type.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/__init__.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/deployment.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/display.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/stack.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/urls.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/base_main.tf.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/main.tf.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/terraform.tfvars.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/variables.tf.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/wandb_main.tf.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/main.tf.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/terraform.tfvars.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/variables.tf.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/deployment.yaml.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/mlflow-deployment.yaml.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/mlflow-service.yaml.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/service.yaml.j2 +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/feast_env.tpl +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/main.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/requirements.txt +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloudbuild.yaml +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/teardown_script.sh +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/__init__.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/banner.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/constants.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/infracost.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/kubernetes_gke.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/kubernetes_local.py +0 -0
- {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/menu.py +0 -0
- {deployml_core-0.0.57 → 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"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Build for Linux/amd64 platform (not Apple Silicon)
|
|
2
|
+
FROM python:3.9-slim
|
|
3
|
+
|
|
4
|
+
# Set working directory
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
# Install system dependencies
|
|
8
|
+
RUN apt-get update && apt-get install -y \
|
|
9
|
+
curl \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
# Install Python dependencies
|
|
13
|
+
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
|
14
|
+
|
|
15
|
+
# Copy requirements and install
|
|
16
|
+
COPY requirements.txt .
|
|
17
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
18
|
+
|
|
19
|
+
# Copy application code
|
|
20
|
+
COPY main.py .
|
|
21
|
+
|
|
22
|
+
# Create non-root user
|
|
23
|
+
RUN useradd -m -s /bin/bash fastapi && \
|
|
24
|
+
chown -R fastapi:fastapi /app
|
|
25
|
+
|
|
26
|
+
# Switch to non-root user
|
|
27
|
+
USER fastapi
|
|
28
|
+
|
|
29
|
+
# Expose port
|
|
30
|
+
EXPOSE 8000
|
|
31
|
+
|
|
32
|
+
# Health check
|
|
33
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
34
|
+
CMD curl -f http://localhost:8000/health || exit 1
|
|
35
|
+
|
|
36
|
+
# Run the application
|
|
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}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from fastapi import FastAPI, HTTPException
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import mlflow
|
|
9
|
+
import mlflow.pyfunc
|
|
10
|
+
|
|
11
|
+
app = FastAPI(
|
|
12
|
+
title="FastAPI Demo",
|
|
13
|
+
description="Simple FastAPI application for Kubernetes deployment demo",
|
|
14
|
+
version="1.0.0"
|
|
15
|
+
)
|
|
16
|
+
|
|
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
|
+
|
|
21
|
+
# Global model variable
|
|
22
|
+
model = None
|
|
23
|
+
|
|
24
|
+
# Pydantic models
|
|
25
|
+
class PredictionRequest(BaseModel):
|
|
26
|
+
features: Dict[str, float]
|
|
27
|
+
|
|
28
|
+
class PredictionResponse(BaseModel):
|
|
29
|
+
prediction: float
|
|
30
|
+
timestamp: str
|
|
31
|
+
model_used: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
class HealthResponse(BaseModel):
|
|
34
|
+
status: str
|
|
35
|
+
timestamp: str
|
|
36
|
+
port: int
|
|
37
|
+
mlflow_connected: bool
|
|
38
|
+
model_loaded: bool
|
|
39
|
+
|
|
40
|
+
def load_model_from_mlflow():
|
|
41
|
+
"""Load model from MLflow Model Registry"""
|
|
42
|
+
global model
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
|
|
46
|
+
model = mlflow.pyfunc.load_model(f"models:/{MODEL_NAME}/Production")
|
|
47
|
+
print(f"✓ Loaded model '{MODEL_NAME}' from Production stage")
|
|
48
|
+
return True
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"⚠️ Could not load model from MLflow: {e}")
|
|
51
|
+
model = None
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Load model at startup
|
|
55
|
+
@app.on_event("startup")
|
|
56
|
+
async def startup_event():
|
|
57
|
+
"""Load MLflow model on startup"""
|
|
58
|
+
load_model_from_mlflow()
|
|
59
|
+
|
|
60
|
+
@app.get("/")
|
|
61
|
+
async def root():
|
|
62
|
+
"""Root endpoint"""
|
|
63
|
+
return {
|
|
64
|
+
"service": "FastAPI Demo",
|
|
65
|
+
"version": "1.0.0",
|
|
66
|
+
"model_loaded": model is not None,
|
|
67
|
+
"model_name": MODEL_NAME,
|
|
68
|
+
"mlflow_uri": MLFLOW_TRACKING_URI,
|
|
69
|
+
"endpoints": {
|
|
70
|
+
"health": "/health",
|
|
71
|
+
"predict": "/predict",
|
|
72
|
+
"docs": "/docs"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@app.get("/health", response_model=HealthResponse)
|
|
77
|
+
async def health():
|
|
78
|
+
"""Health check endpoint"""
|
|
79
|
+
port = int(os.getenv("FASTAPI_PORT", "8000"))
|
|
80
|
+
return HealthResponse(
|
|
81
|
+
status="healthy",
|
|
82
|
+
timestamp=datetime.now().isoformat(),
|
|
83
|
+
port=port,
|
|
84
|
+
mlflow_connected=True,
|
|
85
|
+
model_loaded=model is not None
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@app.post("/predict", response_model=PredictionResponse)
|
|
89
|
+
async def predict(request: PredictionRequest):
|
|
90
|
+
"""Prediction endpoint using MLflow model"""
|
|
91
|
+
global model
|
|
92
|
+
|
|
93
|
+
# If model is not loaded, return -1
|
|
94
|
+
if model is None:
|
|
95
|
+
return PredictionResponse(
|
|
96
|
+
prediction=-1.0,
|
|
97
|
+
timestamp=datetime.now().isoformat(),
|
|
98
|
+
model_used=None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Convert features dict to DataFrame in correct order
|
|
103
|
+
# Features: bedrooms, bathrooms, area_sqft, lot_size, year_built, city, state
|
|
104
|
+
feature_order = ['bedrooms', 'bathrooms', 'area_sqft', 'lot_size', 'year_built', 'city', 'state']
|
|
105
|
+
feature_values = [request.features.get(f, 0.0) for f in feature_order]
|
|
106
|
+
X = pd.DataFrame([feature_values], columns=feature_order)
|
|
107
|
+
|
|
108
|
+
prediction = float(model.predict(X)[0])
|
|
109
|
+
model_used = f"MLflow: {MODEL_NAME}"
|
|
110
|
+
|
|
111
|
+
return PredictionResponse(
|
|
112
|
+
prediction=prediction,
|
|
113
|
+
timestamp=datetime.now().isoformat(),
|
|
114
|
+
model_used=model_used
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
# On any error, return -1
|
|
118
|
+
return PredictionResponse(
|
|
119
|
+
prediction=-1.0,
|
|
120
|
+
timestamp=datetime.now().isoformat(),
|
|
121
|
+
model_used=None
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
import uvicorn
|
|
126
|
+
port = int(os.getenv("FASTAPI_PORT", "8000"))
|
|
127
|
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
FROM grafana/grafana:10.4.5
|
|
2
|
+
|
|
3
|
+
# Switch to root to copy files and change permissions
|
|
4
|
+
USER root
|
|
5
|
+
|
|
6
|
+
# Configure Grafana to listen on Cloud Run's expected port
|
|
7
|
+
ENV GF_SERVER_HTTP_PORT=8080 \
|
|
8
|
+
GF_SECURITY_ADMIN_USER=admin \
|
|
9
|
+
GF_SECURITY_ADMIN_PASSWORD=admin
|
|
10
|
+
|
|
11
|
+
# Copy entrypoint script and set permissions
|
|
12
|
+
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
13
|
+
RUN chmod 755 /usr/local/bin/entrypoint.sh
|
|
14
|
+
|
|
15
|
+
# Create provisioning directories
|
|
16
|
+
RUN mkdir -p /etc/grafana/provisioning/datasources \
|
|
17
|
+
/etc/grafana/provisioning/dashboards
|
|
18
|
+
|
|
19
|
+
# Switch back to the default grafana user for runtime
|
|
20
|
+
USER grafana
|
|
21
|
+
|
|
22
|
+
# Set custom entrypoint
|
|
23
|
+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Map Cloud Run's PORT → Grafana's expected var
|
|
5
|
+
export GF_SERVER_HTTP_PORT="${PORT:-${GF_SERVER_HTTP_PORT:-8080}}"
|
|
6
|
+
|
|
7
|
+
echo "Starting Grafana on port ${GF_SERVER_HTTP_PORT}..."
|
|
8
|
+
[ -n "${GF_DATABASE_URL:-}" ] && echo "Using database: ${GF_DATABASE_URL}"
|
|
9
|
+
|
|
10
|
+
exec /run.sh
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Build for Linux/amd64 platform (not Apple Silicon)
|
|
2
|
+
FROM python:3.11-slim
|
|
3
|
+
|
|
4
|
+
# Set working directory
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
# Install system dependencies
|
|
8
|
+
RUN apt-get update && apt-get install -y \
|
|
9
|
+
curl \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
# Install Python dependencies
|
|
13
|
+
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
|
14
|
+
|
|
15
|
+
# Copy requirements and install
|
|
16
|
+
COPY requirements.txt .
|
|
17
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
18
|
+
|
|
19
|
+
# Create non-root user
|
|
20
|
+
RUN useradd -m -s /bin/bash mlflow && \
|
|
21
|
+
chown -R mlflow:mlflow /app
|
|
22
|
+
|
|
23
|
+
# Create directories for MLflow data
|
|
24
|
+
RUN mkdir -p /mlflow-artifacts && \
|
|
25
|
+
chown -R mlflow:mlflow /mlflow-artifacts
|
|
26
|
+
|
|
27
|
+
# Switch to non-root user
|
|
28
|
+
USER mlflow
|
|
29
|
+
|
|
30
|
+
# Expose port
|
|
31
|
+
EXPOSE 5000
|
|
32
|
+
|
|
33
|
+
# Health check
|
|
34
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
35
|
+
CMD curl -f http://localhost:5000/health || exit 1
|
|
36
|
+
|
|
37
|
+
# Default command - MLflow server
|
|
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
|
|
40
|
+
|