feat: cost-guard komplett — dev-budsjett, Pub/Sub auto-teardown, webhook-alert, cost-check i CI/CD
This commit is contained in:
parent
4502ae09fd
commit
aff36f2f35
|
|
@ -1,7 +1,20 @@
|
||||||
# cloudbuild.yaml — Auto-build and deploy osvauco-agent on push to main
|
# cloudbuild.yaml — Auto-build, cost-check og deploy osvauco-agent på push til main
|
||||||
steps:
|
steps:
|
||||||
# Step 1: Build Docker image
|
# Steg 1: Kost-sjekk før deploy
|
||||||
|
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||||
|
id: cost-check
|
||||||
|
entrypoint: bash
|
||||||
|
args:
|
||||||
|
- '-c'
|
||||||
|
- |
|
||||||
|
echo "=== Cost Check ==="
|
||||||
|
bash infrastructure/09-cost-check.sh || true
|
||||||
|
echo "=== Cost Check ferdig ==="
|
||||||
|
|
||||||
|
# Steg 2: Bygg Docker-image
|
||||||
- name: 'gcr.io/cloud-builders/docker'
|
- name: 'gcr.io/cloud-builders/docker'
|
||||||
|
id: build
|
||||||
|
waitFor: ['cost-check']
|
||||||
args:
|
args:
|
||||||
- 'build'
|
- 'build'
|
||||||
- '-t'
|
- '-t'
|
||||||
|
|
@ -10,15 +23,19 @@ steps:
|
||||||
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REPO}/osvauco-agent:latest'
|
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REPO}/osvauco-agent:latest'
|
||||||
- 'agents/core-logic'
|
- 'agents/core-logic'
|
||||||
|
|
||||||
# Step 2: Push image
|
# Steg 3: Push image
|
||||||
- name: 'gcr.io/cloud-builders/docker'
|
- name: 'gcr.io/cloud-builders/docker'
|
||||||
|
id: push
|
||||||
|
waitFor: ['build']
|
||||||
args:
|
args:
|
||||||
- 'push'
|
- 'push'
|
||||||
- '--all-tags'
|
- '--all-tags'
|
||||||
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REPO}/osvauco-agent'
|
- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REPO}/osvauco-agent'
|
||||||
|
|
||||||
# Step 3: Deploy to Cloud Run
|
# Steg 4: Deploy til Cloud Run
|
||||||
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||||
|
id: deploy
|
||||||
|
waitFor: ['push']
|
||||||
entrypoint: gcloud
|
entrypoint: gcloud
|
||||||
args:
|
args:
|
||||||
- 'run'
|
- 'run'
|
||||||
|
|
@ -37,6 +54,27 @@ steps:
|
||||||
- '--max-instances=3'
|
- '--max-instances=3'
|
||||||
- '--quiet'
|
- '--quiet'
|
||||||
|
|
||||||
|
# Steg 5: Deploy-notifikasjon til webhook/chat-app
|
||||||
|
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||||
|
id: notify
|
||||||
|
waitFor: ['deploy']
|
||||||
|
entrypoint: bash
|
||||||
|
args:
|
||||||
|
- '-c'
|
||||||
|
- |
|
||||||
|
WEBHOOK_URL=$(gcloud secrets versions access latest \
|
||||||
|
--secret=webhook-url \
|
||||||
|
--project=${PROJECT_ID} 2>/dev/null || echo "")
|
||||||
|
if [[ -n "${WEBHOOK_URL}" ]]; then
|
||||||
|
MSG="✅ *OSVauco Deploy fullført*\nCommit: \`$COMMIT_SHA\`\nService: \`${_CLOUD_RUN_SERVICE}\`\nRegion: \`${_REGION}\`"
|
||||||
|
curl -s -X POST "${WEBHOOK_URL}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "{\"text\": \"$MSG\"}" || true
|
||||||
|
echo "✓ Deploy-notifikasjon sendt"
|
||||||
|
else
|
||||||
|
echo "⚠️ Ingen webhook-url i Secret Manager — hopper over varsling"
|
||||||
|
fi
|
||||||
|
|
||||||
substitutions:
|
substitutions:
|
||||||
_REGION: us-central1
|
_REGION: us-central1
|
||||||
_ARTIFACT_REPO: osvauco-repo
|
_ARTIFACT_REPO: osvauco-repo
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 01-setupenv.sh — Enable APIs, create staging bucket with lifecycle, SA, billing budget
|
# 01-setupenv.sh — Enable APIs, create staging bucket with lifecycle, SA, billing budgets
|
||||||
# Run in: local VS Code terminal OR Cloud Shell
|
# Run in: local VS Code terminal OR Cloud Shell
|
||||||
# Source .env before running: source .env
|
# Source .env before running: source .env
|
||||||
|
|
||||||
|
|
@ -11,7 +11,9 @@ set -euo pipefail
|
||||||
: "${AGENT_SA:?Set AGENT_SA in .env}"
|
: "${AGENT_SA:?Set AGENT_SA in .env}"
|
||||||
|
|
||||||
BUCKET_NAME="gs://${PROJECT_ID}-agent-staging"
|
BUCKET_NAME="gs://${PROJECT_ID}-agent-staging"
|
||||||
BUDGET_NAME="OSVauco-Agent-Budget-500USD"
|
BUDGET_PROD_NAME="OSVauco-Agent-Budget-500USD"
|
||||||
|
BUDGET_DEV_NAME="OSVauco-Dev-Budget-75USD"
|
||||||
|
PUBSUB_TOPIC="billing-alert-auto-teardown"
|
||||||
|
|
||||||
echo "=== 01: Environment Setup for ${PROJECT_ID} ==="
|
echo "=== 01: Environment Setup for ${PROJECT_ID} ==="
|
||||||
|
|
||||||
|
|
@ -29,10 +31,13 @@ gcloud services enable \
|
||||||
run.googleapis.com \
|
run.googleapis.com \
|
||||||
artifactregistry.googleapis.com \
|
artifactregistry.googleapis.com \
|
||||||
cloudbuild.googleapis.com \
|
cloudbuild.googleapis.com \
|
||||||
|
cloudbuildv2.googleapis.com \
|
||||||
secretmanager.googleapis.com \
|
secretmanager.googleapis.com \
|
||||||
monitoring.googleapis.com \
|
monitoring.googleapis.com \
|
||||||
logging.googleapis.com \
|
logging.googleapis.com \
|
||||||
cloudtrace.googleapis.com \
|
cloudtrace.googleapis.com \
|
||||||
|
pubsub.googleapis.com \
|
||||||
|
cloudfunctions.googleapis.com \
|
||||||
--project="$PROJECT_ID" --quiet
|
--project="$PROJECT_ID" --quiet
|
||||||
echo "✓ APIs enabled."
|
echo "✓ APIs enabled."
|
||||||
|
|
||||||
|
|
@ -73,24 +78,55 @@ for ROLE in \
|
||||||
done
|
done
|
||||||
echo "✓ IAM bindings configured."
|
echo "✓ IAM bindings configured."
|
||||||
|
|
||||||
# 5. Billing budget alert (idempotent check by display name)
|
# 5. Pub/Sub topic for billing auto-teardown (idempotent)
|
||||||
EXISTING=$(gcloud billing budgets list \
|
if ! gcloud pubsub topics describe "${PUBSUB_TOPIC}" --project="${PROJECT_ID}" >/dev/null 2>&1; then
|
||||||
|
gcloud pubsub topics create "${PUBSUB_TOPIC}" --project="${PROJECT_ID}" --quiet
|
||||||
|
echo "✓ Pub/Sub topic created: ${PUBSUB_TOPIC}"
|
||||||
|
else
|
||||||
|
echo "✓ Pub/Sub topic already exists: ${PUBSUB_TOPIC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Prod billing budget — $500, varsler på 50% / 80% / 100% + Pub/Sub (idempotent)
|
||||||
|
EXISTING_PROD=$(gcloud billing budgets list \
|
||||||
--billing-account="${BILLING_ACCOUNT_ID}" \
|
--billing-account="${BILLING_ACCOUNT_ID}" \
|
||||||
--filter="displayName=${BUDGET_NAME}" \
|
--filter="displayName=${BUDGET_PROD_NAME}" \
|
||||||
--format="value(name)" 2>/dev/null | head -1 || true)
|
--format="value(name)" 2>/dev/null | head -1 || true)
|
||||||
|
|
||||||
if [[ -z "${EXISTING}" ]]; then
|
if [[ -z "${EXISTING_PROD}" ]]; then
|
||||||
gcloud billing budgets create \
|
gcloud billing budgets create \
|
||||||
--billing-account="${BILLING_ACCOUNT_ID}" \
|
--billing-account="${BILLING_ACCOUNT_ID}" \
|
||||||
--display-name="${BUDGET_NAME}" \
|
--display-name="${BUDGET_PROD_NAME}" \
|
||||||
--budget-amount=500USD \
|
--budget-amount=500USD \
|
||||||
--threshold-rule=percent=0.5 \
|
--threshold-rule=percent=0.5 \
|
||||||
--threshold-rule=percent=0.8 \
|
--threshold-rule=percent=0.8 \
|
||||||
--threshold-rule=percent=1.0
|
--threshold-rule=percent=1.0 \
|
||||||
echo "✓ Budget alert created"
|
--notifications-rule-pubsub-topic="projects/${PROJECT_ID}/topics/${PUBSUB_TOPIC}"
|
||||||
|
echo "✓ Prod budget alert created ($500, Pub/Sub koblet)"
|
||||||
else
|
else
|
||||||
echo "✓ Budget alert already exists"
|
echo "✓ Prod budget alert already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7. Dev billing budget — $75, varsler på 70% / 90% + Pub/Sub (idempotent)
|
||||||
|
EXISTING_DEV=$(gcloud billing budgets list \
|
||||||
|
--billing-account="${BILLING_ACCOUNT_ID}" \
|
||||||
|
--filter="displayName=${BUDGET_DEV_NAME}" \
|
||||||
|
--format="value(name)" 2>/dev/null | head -1 || true)
|
||||||
|
|
||||||
|
if [[ -z "${EXISTING_DEV}" ]]; then
|
||||||
|
gcloud billing budgets create \
|
||||||
|
--billing-account="${BILLING_ACCOUNT_ID}" \
|
||||||
|
--display-name="${BUDGET_DEV_NAME}" \
|
||||||
|
--budget-amount=75USD \
|
||||||
|
--threshold-rule=percent=0.7 \
|
||||||
|
--threshold-rule=percent=0.9 \
|
||||||
|
--threshold-rule=percent=1.0 \
|
||||||
|
--notifications-rule-pubsub-topic="projects/${PROJECT_ID}/topics/${PUBSUB_TOPIC}"
|
||||||
|
echo "✓ Dev budget alert created ($75, Pub/Sub koblet)"
|
||||||
|
else
|
||||||
|
echo "✓ Dev budget alert already exists"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== 01: Environment setup COMPLETE ==="
|
echo "=== 01: Environment setup COMPLETE ==="
|
||||||
|
echo " Kjør nå: bash infrastructure/10-cost-guard.sh"
|
||||||
|
echo " for å aktivere auto-teardown Cloud Function"
|
||||||
|
|
|
||||||
156
infrastructure/10-cost-guard.sh
Normal file
156
infrastructure/10-cost-guard.sh
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# 10-cost-guard.sh — Deploy Cloud Function som lytter på billing Pub/Sub
|
||||||
|
# og utfører auto-teardown + sender webhook-alert til chat-app
|
||||||
|
# ved 100% av budsjett.
|
||||||
|
#
|
||||||
|
# Forutsetter:
|
||||||
|
# - Pub/Sub topic 'billing-alert-auto-teardown' finnes (01-setupenv.sh)
|
||||||
|
# - Secret Manager secret 'webhook-url' inneholder chat-webhook URL
|
||||||
|
# gcloud secrets create webhook-url --data-file=<(echo -n "https://DIN-WEBHOOK-URL")
|
||||||
|
# Source .env før kjøring: source .env
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${PROJECT_ID:?Set PROJECT_ID}"
|
||||||
|
: "${REGION:?Set REGION}"
|
||||||
|
: "${CLOUD_RUN_SERVICE:?Set CLOUD_RUN_SERVICE}"
|
||||||
|
|
||||||
|
FUNCTION_NAME="billing-auto-teardown"
|
||||||
|
PUBSUB_TOPIC="billing-alert-auto-teardown"
|
||||||
|
FUNC_DIR="/tmp/cost-guard-fn"
|
||||||
|
|
||||||
|
echo "=== 10: Cost Guard — Cloud Function deployment ==="
|
||||||
|
|
||||||
|
bash "$(dirname "$0")/00-authcheck.sh"
|
||||||
|
|
||||||
|
gcloud services enable cloudfunctions.googleapis.com pubsub.googleapis.com --project="${PROJECT_ID}" --quiet
|
||||||
|
|
||||||
|
# --- Skriv Cloud Function kildekode ---
|
||||||
|
mkdir -p "${FUNC_DIR}"
|
||||||
|
|
||||||
|
cat > "${FUNC_DIR}/main.py" << 'PYEOF'
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import functions_framework
|
||||||
|
from google.cloud import secretmanager
|
||||||
|
|
||||||
|
|
||||||
|
def get_webhook_url(project_id: str) -> str | None:
|
||||||
|
try:
|
||||||
|
client = secretmanager.SecretManagerServiceClient()
|
||||||
|
name = f"projects/{project_id}/secrets/webhook-url/versions/latest"
|
||||||
|
response = client.access_secret_version(request={"name": name})
|
||||||
|
return response.payload.data.decode("UTF-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def send_webhook(url: str, message: str) -> None:
|
||||||
|
payload = json.dumps({"text": message}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=10):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@functions_framework.cloud_event
|
||||||
|
def billing_alert(cloud_event):
|
||||||
|
project_id = os.environ.get("PROJECT_ID", "")
|
||||||
|
service = os.environ.get("CLOUD_RUN_SERVICE", "")
|
||||||
|
region = os.environ.get("REGION", "")
|
||||||
|
|
||||||
|
# Dekod Pub/Sub-melding
|
||||||
|
data = base64.b64decode(cloud_event.data["message"]["data"]).decode()
|
||||||
|
alert = json.loads(data)
|
||||||
|
|
||||||
|
budget_amount = alert.get("budgetAmount", "?")
|
||||||
|
cost_amount = alert.get("costAmount", "?")
|
||||||
|
alert_threshold = alert.get("alertThresholdExceeded", 0)
|
||||||
|
|
||||||
|
print(f"Billing alert: {cost_amount} / {budget_amount} ({alert_threshold*100:.0f}%)")
|
||||||
|
|
||||||
|
webhook_url = get_webhook_url(project_id)
|
||||||
|
base_msg = (
|
||||||
|
f"🚨 *OSVauco Cost Alert*\n"
|
||||||
|
f"Prosjekt: `{project_id}`\n"
|
||||||
|
f"Forbruk: `{cost_amount}` av budsjett `{budget_amount}`\n"
|
||||||
|
f"Terskel: `{alert_threshold*100:.0f}%`"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Kjør auto-teardown kun ved 100% terskel
|
||||||
|
if alert_threshold >= 1.0:
|
||||||
|
print("100% terskel nådd — starter auto-teardown")
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"gcloud", "run", "services", "delete", service,
|
||||||
|
f"--region={region}",
|
||||||
|
f"--project={project_id}",
|
||||||
|
"--quiet",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
teardown_msg = base_msg + "\n\nℹ️ Auto-teardown utført: Cloud Run stoppet."
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
teardown_msg = base_msg + f"\n\n⚠️ Auto-teardown FEILET: {e.stderr.decode()}"
|
||||||
|
msg = teardown_msg
|
||||||
|
else:
|
||||||
|
msg = base_msg + "\n\n⚠️ Ingen tiltak nå — men pass på forbruket."
|
||||||
|
|
||||||
|
if webhook_url:
|
||||||
|
try:
|
||||||
|
send_webhook(webhook_url, msg)
|
||||||
|
print("Webhook-alert sendt")
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Webhook feilet: {ex}")
|
||||||
|
else:
|
||||||
|
print("Ingen webhook-url i Secret Manager — hopper over varsling")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
cat > "${FUNC_DIR}/requirements.txt" << 'EOF'
|
||||||
|
functions-framework==3.*
|
||||||
|
google-cloud-secret-manager>=2.0.0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✓ Cloud Function kildekode skrevet til ${FUNC_DIR}"
|
||||||
|
|
||||||
|
# --- Deploy Cloud Function ---
|
||||||
|
gcloud functions deploy "${FUNCTION_NAME}" \
|
||||||
|
--gen2 \
|
||||||
|
--runtime=python312 \
|
||||||
|
--region="${REGION}" \
|
||||||
|
--source="${FUNC_DIR}" \
|
||||||
|
--entry-point=billing_alert \
|
||||||
|
--trigger-topic="${PUBSUB_TOPIC}" \
|
||||||
|
--project="${PROJECT_ID}" \
|
||||||
|
--set-env-vars="PROJECT_ID=${PROJECT_ID},CLOUD_RUN_SERVICE=${CLOUD_RUN_SERVICE},REGION=${REGION}" \
|
||||||
|
--service-account="${AGENT_SA:-$(gcloud config get-value account)}" \
|
||||||
|
--quiet
|
||||||
|
|
||||||
|
echo "✓ Cloud Function deployed: ${FUNCTION_NAME}"
|
||||||
|
|
||||||
|
# --- IAM: Cloud Function SA trenger run.admin ---
|
||||||
|
PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
|
||||||
|
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
|
||||||
|
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
|
||||||
|
--role="roles/run.admin" --quiet
|
||||||
|
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
|
||||||
|
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
|
||||||
|
--role="roles/secretmanager.secretAccessor" --quiet
|
||||||
|
echo "✓ IAM: run.admin + secretmanager.secretAccessor gitt til Cloud Function SA"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 10: Cost Guard COMPLETE ==="
|
||||||
|
echo " Legg til webhook URL i Secret Manager:"
|
||||||
|
echo " gcloud secrets create webhook-url --data-file=<(echo -n \"https://DIN-WEBHOOK-URL\")"
|
||||||
|
echo " Kompatibel med: Google Chat, Slack, Discord, Teams"
|
||||||
|
echo " Tester: publiser en test-melding på Pub/Sub:"
|
||||||
|
echo " gcloud pubsub topics publish ${PUBSUB_TOPIC} --message='{\"budgetAmount\":75,\"costAmount\":75,\"alertThresholdExceeded\":1.0}'"
|
||||||
51
infrastructure/notifications/webhook-setup.md
Normal file
51
infrastructure/notifications/webhook-setup.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Webhook / Chat-app oppsett
|
||||||
|
|
||||||
|
Budsjett-alerts og deploy-notifikasjoner sendes til en valgfri chat-app
|
||||||
|
via en enkel webhook-URL lagret i Secret Manager.
|
||||||
|
|
||||||
|
## Kompatible apper
|
||||||
|
|
||||||
|
| App | Webhook-format | Instruksjon |
|
||||||
|
|---|---|---|
|
||||||
|
| Google Chat | `{"text": "..."}` | Rom → Apps → Webhooks → Legg til webhook |
|
||||||
|
| Slack | `{"text": "..."}` | Apps → Incoming Webhooks → Add to Slack |
|
||||||
|
| Discord | `{"content": "..."}` | Kanal-innstillinger → Integrations → Webhooks |
|
||||||
|
| Teams | `{"text": "..."}` | Kanal → Connectors → Incoming Webhook |
|
||||||
|
|
||||||
|
> **Discord-merk:** Discord bruker `content` i stedet for `text`. Oppdater
|
||||||
|
> `main.py` i `10-cost-guard.sh` og `cloudbuild.yaml` notify-steget til
|
||||||
|
> `{"content": "$MSG"}` hvis du bruker Discord.
|
||||||
|
|
||||||
|
## Legge til webhook URL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ny secret
|
||||||
|
gcloud secrets create webhook-url \
|
||||||
|
--data-file=<(echo -n "https://DIN-WEBHOOK-URL") \
|
||||||
|
--project=propane-will-491900-m5
|
||||||
|
|
||||||
|
# Oppdater eksisterende secret
|
||||||
|
gcloud secrets versions add webhook-url \
|
||||||
|
--data-file=<(echo -n "https://NY-WEBHOOK-URL") \
|
||||||
|
--project=propane-will-491900-m5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hva utløser varsler
|
||||||
|
|
||||||
|
| Hendelse | Avsender | Terskel |
|
||||||
|
|---|---|---|
|
||||||
|
| Dev-budsjett 70% nådd | Cloud Function (`10-cost-guard.sh`) | $52.50 av $75 |
|
||||||
|
| Dev-budsjett 90% nådd | Cloud Function | $67.50 av $75 |
|
||||||
|
| Prod-budsjett 50% nådd | Cloud Function | $250 av $500 |
|
||||||
|
| Prod-budsjett 80% nådd | Cloud Function | $400 av $500 |
|
||||||
|
| Budsjett 100% — auto-teardown | Cloud Function | Cloud Run stoppes |
|
||||||
|
| Deploy fullført | `cloudbuild.yaml` steg 5 | Hver push til main |
|
||||||
|
|
||||||
|
## Teste varsling manuelt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simuler 100% budsjett-alert (trigger auto-teardown)
|
||||||
|
gcloud pubsub topics publish billing-alert-auto-teardown \
|
||||||
|
--message='{"budgetAmount":75,"costAmount":75,"alertThresholdExceeded":1.0}' \
|
||||||
|
--project=propane-will-491900-m5
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user