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.
Files changed (103) hide show
  1. {deployml_core-0.0.57 → deployml_core-0.0.59}/PKG-INFO +1 -1
  2. {deployml_core-0.0.57 → deployml_core-0.0.59}/pyproject.toml +1 -1
  3. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/cli/cli.py +153 -6
  4. deployml_core-0.0.59/src/deployml/docker/fastapi/.dockerignore +11 -0
  5. deployml_core-0.0.59/src/deployml/docker/fastapi/Dockerfile +38 -0
  6. deployml_core-0.0.59/src/deployml/docker/fastapi/main.py +127 -0
  7. deployml_core-0.0.59/src/deployml/docker/fastapi/requirements.txt +7 -0
  8. deployml_core-0.0.59/src/deployml/docker/grafana-container/Dockerfile +23 -0
  9. deployml_core-0.0.59/src/deployml/docker/grafana-container/README.md +3 -0
  10. deployml_core-0.0.59/src/deployml/docker/grafana-container/entrypoint.sh +10 -0
  11. deployml_core-0.0.59/src/deployml/docker/mlflow/Dockerfile +40 -0
  12. deployml_core-0.0.59/src/deployml/docker/mlflow/requirements.txt +4 -0
  13. deployml_core-0.0.59/src/deployml/notebook/docker.py +224 -0
  14. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/mlflow_main.tf.j2 +31 -38
  15. deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/main.tf +42 -0
  16. deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/outputs.tf +3 -0
  17. deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/drift_metrics.json +9 -0
  18. deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/ground_truth.json +5 -0
  19. deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +7 -0
  20. deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/predictions.json +8 -0
  21. deployml_core-0.0.59/src/deployml/terraform/modules/bigquery/cloud/gcp/variables.tf +11 -0
  22. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/main.tf +3 -4
  23. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/main.tf +8 -1
  24. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/helpers.py +49 -87
  25. {deployml_core-0.0.57 → deployml_core-0.0.59}/LICENSE +0 -0
  26. {deployml_core-0.0.57 → deployml_core-0.0.59}/README.md +0 -0
  27. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/__init__.py +0 -0
  28. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/api.py +0 -0
  29. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/cli/__init__.py +0 -0
  30. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/diagnostics/__init__.py +0 -0
  31. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/diagnostics/doctor.py +0 -0
  32. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/enum/__init__.py +0 -0
  33. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/enum/cloud_provider.py +0 -0
  34. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/enum/deployment_type.py +0 -0
  35. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/__init__.py +0 -0
  36. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/deployment.py +0 -0
  37. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/display.py +0 -0
  38. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/stack.py +0 -0
  39. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook/urls.py +0 -0
  40. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/notebook.py +0 -0
  41. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/base_main.tf.j2 +0 -0
  42. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/main.tf.j2 +0 -0
  43. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/terraform.tfvars.j2 +0 -0
  44. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/variables.tf.j2 +0 -0
  45. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_run/wandb_main.tf.j2 +0 -0
  46. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/main.tf.j2 +0 -0
  47. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/terraform.tfvars.j2 +0 -0
  48. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/gcp/cloud_vm/variables.tf.j2 +0 -0
  49. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/deployment.yaml.j2 +0 -0
  50. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/mlflow-deployment.yaml.j2 +0 -0
  51. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/mlflow-service.yaml.j2 +0 -0
  52. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/templates/kubernetes_local/service.yaml.j2 +0 -0
  53. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/outputs.tf +0 -0
  54. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cloud_sql_postgres/variables.tf +0 -0
  55. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/main.tf +0 -0
  56. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/outputs.tf +0 -0
  57. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/variables.tf +0 -0
  58. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
  59. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  60. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
  61. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
  62. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  63. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
  64. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/main.tf +0 -0
  65. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/outputs.tf +0 -0
  66. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/variables.tf +0 -0
  67. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/main.tf +0 -0
  68. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/outputs.tf +0 -0
  69. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/variables.tf +0 -0
  70. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/feast_env.tpl +0 -0
  71. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/main.tf +0 -0
  72. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/outputs.tf +0 -0
  73. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/variables.tf +0 -0
  74. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/main.tf +0 -0
  75. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/outputs.tf +0 -0
  76. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/variables.tf +0 -0
  77. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/outputs.tf +0 -0
  78. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/variables.tf +0 -0
  79. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/main.tf +0 -0
  80. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/variables.tf +0 -0
  81. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/main.tf +0 -0
  82. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  83. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/variables.tf +0 -0
  84. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/main.py +0 -0
  85. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/requirements.txt +0 -0
  86. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/cloudbuild.yaml +0 -0
  87. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/main.tf +0 -0
  88. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/outputs.tf +0 -0
  89. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/teardown_script.sh +0 -0
  90. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/teardown/cloud/gcp/variables.tf +0 -0
  91. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/main.tf +0 -0
  92. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/outputs.tf +0 -0
  93. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/variables.tf +0 -0
  94. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/main.tf +0 -0
  95. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/variables.tf +0 -0
  96. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/__init__.py +0 -0
  97. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/banner.py +0 -0
  98. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/constants.py +0 -0
  99. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/infracost.py +0 -0
  100. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/kubernetes_gke.py +0 -0
  101. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/kubernetes_local.py +0 -0
  102. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/menu.py +0 -0
  103. {deployml_core-0.0.57 → deployml_core-0.0.59}/src/deployml/utils/teardown.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deployml-core
3
- Version: 0.0.57
3
+ Version: 0.0.59
4
4
  Summary: Infra for academia
5
5
  License-File: LICENSE
6
6
  Author: Drew Hoang
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "deployml-core"
4
- version = "0.0.57"
4
+ version = "0.0.59"
5
5
  description = "Infra for academia"
6
6
  authors = [
7
7
  {name = "Drew Hoang", email = "codentell@gmail.com"},
@@ -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
- # Check if we have Cloud SQL resources and clean them up first
1491
- plan_result = subprocess.run(
1492
- ["terraform", "plan", "-destroy"],
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
- if "google_sql_database_instance" in plan_result.stdout:
1499
- cleanup_cloud_sql_resources(DEPLOYML_TERRAFORM_DIR, project_id)
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,11 @@
1
+ __pycache__
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ env/
7
+ venv/
8
+ .venv
9
+ .git
10
+ .gitignore
11
+ README.md
@@ -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,7 @@
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ pydantic==2.5.2
4
+ mlflow>=2.8.0
5
+ scikit-learn>=1.0.0
6
+ pandas>=1.3.0
7
+ numpy>=1.21.0
@@ -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,3 @@
1
+ docker build --platform=linux/amd64 -t gcr.io/mlops-intro-461805/grafana/grafana:latest .
2
+
3
+ docker push gcr.io/mlops-intro-461805/grafana/grafana:latest
@@ -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
+
@@ -0,0 +1,4 @@
1
+ mlflow==3.10.0
2
+ psycopg2-binary
3
+ boto3
4
+ google-cloud-storage