chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# Remote Integration Tests for `/apps/{app_name}/trigger/*` Endpoints
|
||||
|
||||
End-to-end tests that verify Pub/Sub push, and
|
||||
Eventarc triggers against a real ADK agent deployed to Cloud Run.
|
||||
|
||||
## Workflow
|
||||
|
||||
This workflow is split into three independent phases.
|
||||
|
||||
### Phase 1: Deploy Agent
|
||||
|
||||
Build the ADK package and deploy the echo agent to Cloud Run. This ensures that
|
||||
the service and its identity exist before any infrastructure wiring occurs.
|
||||
|
||||
```bash
|
||||
export GCP_PROJECT_ID=your-project-id
|
||||
export SUFFIX=ea786 # Pick a unique suffix
|
||||
export SERVICE_NAME=adk-trigger-test-$SUFFIX
|
||||
|
||||
# 1. Build local ADK wheel
|
||||
uv build --wheel --out-dir tests/remote/triggers/test_agent/wheels/
|
||||
|
||||
# 2. Deploy Agent
|
||||
gcloud run deploy $SERVICE_NAME \
|
||||
--source=tests/remote/triggers/test_agent \
|
||||
--project="$GCP_PROJECT_ID" \
|
||||
--region="us-central1" \
|
||||
--port=8080 \
|
||||
--quiet
|
||||
```
|
||||
|
||||
### Phase 2: Wire Infrastructure (Terraform)
|
||||
|
||||
Run Terraform to create the supporting infrastructure (IAM roles, Pub/Sub
|
||||
topics).
|
||||
|
||||
```bash
|
||||
cd tests/remote/triggers/terraform
|
||||
terraform init
|
||||
terraform apply \
|
||||
-var=project_id=$GCP_PROJECT_ID \
|
||||
-var=service_name=$SERVICE_NAME \
|
||||
-var=suffix=$SUFFIX
|
||||
```
|
||||
|
||||
### Phase 3: Run Tests (Pytest)
|
||||
|
||||
```bash
|
||||
# Run Tests from the project root
|
||||
export GCP_PROJECT_ID=your-project-id
|
||||
export SUFFIX=ea786
|
||||
pytest tests/remote/triggers/ -v -s
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
# 1. Destroy infrastructure
|
||||
cd tests/remote/triggers/terraform
|
||||
terraform destroy \
|
||||
-var=project_id=$GCP_PROJECT_ID \
|
||||
-var=service_name=$SERVICE_NAME \
|
||||
-var=suffix=$SUFFIX
|
||||
|
||||
# 2. Delete Cloud Run service
|
||||
gcloud run services delete $SERVICE_NAME --project=$GCP_PROJECT_ID --region=us-central1 --quiet
|
||||
```
|
||||
@@ -0,0 +1,169 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Shared pytest fixtures for remote trigger integration tests.
|
||||
|
||||
Exposes GCP resource references by reading current Terraform state.
|
||||
|
||||
Environment variables:
|
||||
GCP_PROJECT_ID : GCP project.
|
||||
GCP_REGION : GCP region (default: ``us-central1``).
|
||||
ADK_TERRAFORM_BIN : Path to terraform binary (default: ``terraform``).
|
||||
ADK_TERRAFORM_CWD : Directory to run terraform from (default:
|
||||
``tests/remote/terraform``).
|
||||
ADK_TERRAFORM_ARGS: Extra arguments for terraform commands.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
TERRAFORM_DIR = os.path.join(os.path.dirname(__file__), "terraform")
|
||||
|
||||
|
||||
def _get_project_id() -> str | None:
|
||||
"""Return GCP project ID from env or gcloud config."""
|
||||
project = os.environ.get("GCP_PROJECT_ID")
|
||||
if project:
|
||||
return project
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gcloud", "config", "get-value", "project"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return result.stdout.strip() or None
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def _get_identity_token(audience: str) -> str | None:
|
||||
"""Fetch an identity token for the given audience via gcloud."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gcloud",
|
||||
"auth",
|
||||
"print-identity-token",
|
||||
f"--audiences={audience}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Infrastructure Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def terraform_outputs():
|
||||
"""Read Terraform outputs from the current state."""
|
||||
project = _get_project_id()
|
||||
if not project:
|
||||
pytest.skip(
|
||||
"GCP_PROJECT_ID not set and no gcloud default project configured"
|
||||
)
|
||||
|
||||
tf_bin = os.environ.get("ADK_TERRAFORM_BIN", "terraform")
|
||||
tf_cwd = os.environ.get("ADK_TERRAFORM_CWD", TERRAFORM_DIR)
|
||||
tf_args = shlex.split(os.environ.get("ADK_TERRAFORM_ARGS", ""))
|
||||
|
||||
try:
|
||||
# Build the command using provided overrides
|
||||
cmd = [tf_bin] + tf_args + ["output", "-json"]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=tf_cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
raw = json.loads(result.stdout)
|
||||
return {k: v["value"] for k, v in raw.items()}
|
||||
except (
|
||||
subprocess.CalledProcessError,
|
||||
FileNotFoundError,
|
||||
json.JSONDecodeError,
|
||||
) as e:
|
||||
pytest.fail(
|
||||
"Failed to read Terraform outputs. Ensure 'terraform apply' has been"
|
||||
f" run successfully.\nCommand: {' '.join(cmd)}\nCWD:"
|
||||
f" {tf_cwd}\nError: {e}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def cloud_run_url(terraform_outputs) -> str:
|
||||
"""Base URL of the deployed Cloud Run service."""
|
||||
return terraform_outputs["cloud_run_url"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def pubsub_topic(terraform_outputs) -> str:
|
||||
"""Fully qualified Pub/Sub topic name."""
|
||||
return terraform_outputs["pubsub_topic"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def pubsub_topic_short(terraform_outputs) -> str:
|
||||
"""Short Pub/Sub topic name."""
|
||||
return terraform_outputs["pubsub_topic_short"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def eventarc_topic(terraform_outputs) -> str:
|
||||
"""Fully qualified Eventarc source topic name."""
|
||||
return terraform_outputs["eventarc_topic"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def eventarc_topic_short(terraform_outputs) -> str:
|
||||
"""Short Eventarc source topic name."""
|
||||
return terraform_outputs["eventarc_topic_short"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def project_id(terraform_outputs) -> str:
|
||||
"""GCP project ID."""
|
||||
return terraform_outputs["project_id"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def auth_headers(cloud_run_url) -> dict[str, str]:
|
||||
"""Authorization headers for direct HTTP calls to Cloud Run."""
|
||||
try:
|
||||
resp = requests.get(cloud_run_url, timeout=10)
|
||||
if resp.status_code != 403:
|
||||
return {}
|
||||
except requests.RequestException:
|
||||
pass
|
||||
|
||||
token = _get_identity_token(cloud_run_url)
|
||||
if token:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
return {}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADK Agent for trigger testing.
|
||||
#
|
||||
# This configuration references a Cloud Run service that has been deployed
|
||||
# manually (e.g. via `gcloud run deploy`) before running Terraform.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
locals {
|
||||
service_name = var.service_name != null ? var.service_name : "${local.name_prefix}-${local.suffix}"
|
||||
}
|
||||
|
||||
# Read the service back as a data source so other resources can reference
|
||||
# its URL and attributes.
|
||||
data "google_cloud_run_v2_service" "trigger_agent" {
|
||||
name = local.service_name
|
||||
location = var.region
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
# No longer using resource "google_cloud_run_v2_service" to avoid
|
||||
# deployment conflicts with local tools.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Eventarc: separate Pub/Sub topic as event source → Cloud Run /trigger/eventarc
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# A dedicated topic that acts as the Eventarc event source.
|
||||
# Publishing to this topic triggers the Eventarc → Cloud Run pipeline.
|
||||
resource "google_pubsub_topic" "eventarc_source" {
|
||||
name = "${local.name_prefix}-eventarc-${local.suffix}"
|
||||
project = var.project_id
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
resource "google_eventarc_trigger" "trigger_test" {
|
||||
name = "${local.name_prefix}-${local.suffix}"
|
||||
location = var.region
|
||||
project = var.project_id
|
||||
|
||||
matching_criteria {
|
||||
attribute = "type"
|
||||
value = "google.cloud.pubsub.topic.v1.messagePublished"
|
||||
}
|
||||
|
||||
transport {
|
||||
pubsub {
|
||||
topic = google_pubsub_topic.eventarc_source.id
|
||||
}
|
||||
}
|
||||
|
||||
destination {
|
||||
cloud_run_service {
|
||||
service = data.google_cloud_run_v2_service.trigger_agent.name
|
||||
path = "/apps/trigger_echo_agent/trigger/eventarc"
|
||||
region = var.region
|
||||
}
|
||||
}
|
||||
|
||||
service_account = google_service_account.eventarc_invoker.email
|
||||
|
||||
depends_on = [
|
||||
google_project_iam_member.eventarc_event_receiver,
|
||||
google_project_service.apis,
|
||||
]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GCS Trigger Test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "random_id" "bucket_suffix" {
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
resource "google_storage_bucket" "trigger_test_bucket" {
|
||||
name = "${local.name_prefix}-bucket-${local.suffix}-${random_id.bucket_suffix.hex}"
|
||||
location = var.region
|
||||
project = var.project_id
|
||||
force_destroy = true
|
||||
|
||||
uniform_bucket_level_access = true
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
data "google_storage_project_service_account" "gcs_account" {
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
resource "google_project_iam_member" "gcs_pubsub_publisher" {
|
||||
project = var.project_id
|
||||
role = "roles/pubsub.publisher"
|
||||
member = "serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}"
|
||||
|
||||
depends_on = [data.google_storage_project_service_account.gcs_account]
|
||||
}
|
||||
|
||||
resource "google_eventarc_trigger" "gcs_trigger" {
|
||||
name = "${local.name_prefix}-gcs-${local.suffix}"
|
||||
location = var.region
|
||||
project = var.project_id
|
||||
|
||||
matching_criteria {
|
||||
attribute = "type"
|
||||
value = "google.cloud.storage.object.v1.finalized"
|
||||
}
|
||||
matching_criteria {
|
||||
attribute = "bucket"
|
||||
value = google_storage_bucket.trigger_test_bucket.name
|
||||
}
|
||||
|
||||
destination {
|
||||
cloud_run_service {
|
||||
service = data.google_cloud_run_v2_service.trigger_agent.name
|
||||
path = "/apps/trigger_echo_agent/trigger/eventarc"
|
||||
region = var.region
|
||||
}
|
||||
}
|
||||
|
||||
service_account = google_service_account.eventarc_invoker.email
|
||||
|
||||
depends_on = [
|
||||
google_project_iam_member.eventarc_event_receiver,
|
||||
google_project_iam_member.gcs_pubsub_publisher,
|
||||
google_project_service.apis,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IAM bindings and Service Accounts for Cloud Run invokers.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Service account for the Cloud Run agent itself.
|
||||
resource "google_service_account" "cloud_run" {
|
||||
account_id = "${local.name_prefix}-run-${local.suffix}"
|
||||
display_name = "ADK Trigger Test - Cloud Run Agent"
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
# Service account for Pub/Sub to invoke Cloud Run.
|
||||
resource "google_service_account" "pubsub_invoker" {
|
||||
account_id = "${local.name_prefix}-ps-${local.suffix}"
|
||||
display_name = "ADK Trigger Test - Pub/Sub Invoker"
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
# Service account for Eventarc to invoke Cloud Run.
|
||||
resource "google_service_account" "eventarc_invoker" {
|
||||
account_id = "${local.name_prefix}-ea-${local.suffix}"
|
||||
display_name = "ADK Trigger Test - Eventarc Invoker"
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "pubsub_invoker" {
|
||||
name = data.google_cloud_run_v2_service.trigger_agent.name
|
||||
location = var.region
|
||||
project = var.project_id
|
||||
role = "roles/run.invoker"
|
||||
member = "serviceAccount:${google_service_account.pubsub_invoker.email}"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "eventarc_invoker" {
|
||||
name = data.google_cloud_run_v2_service.trigger_agent.name
|
||||
location = var.region
|
||||
project = var.project_id
|
||||
role = "roles/run.invoker"
|
||||
member = "serviceAccount:${google_service_account.eventarc_invoker.email}"
|
||||
}
|
||||
|
||||
|
||||
# Eventarc requires the receiver role on the invoker service account.
|
||||
resource "google_project_iam_member" "eventarc_event_receiver" {
|
||||
project = var.project_id
|
||||
role = "roles/eventarc.eventReceiver"
|
||||
member = "serviceAccount:${google_service_account.eventarc_invoker.email}"
|
||||
}
|
||||
|
||||
data "google_project" "project" {
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
# Grant the Pub/Sub service agent permission to create OIDC tokens for the pubsub_invoker SA
|
||||
resource "google_service_account_iam_member" "pubsub_token_creator" {
|
||||
service_account_id = google_service_account.pubsub_invoker.name
|
||||
role = "roles/iam.serviceAccountTokenCreator"
|
||||
member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
|
||||
}
|
||||
|
||||
# Grant the Pub/Sub service agent permission to create OIDC tokens for the eventarc_invoker SA
|
||||
resource "google_service_account_iam_member" "eventarc_token_creator" {
|
||||
service_account_id = google_service_account.eventarc_invoker.name
|
||||
role = "roles/iam.serviceAccountTokenCreator"
|
||||
member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0.0"
|
||||
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = ">= 5.0.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
|
||||
default_labels = {
|
||||
goog-terraform-provisioned = "true"
|
||||
}
|
||||
}
|
||||
|
||||
# Random suffix to avoid resource name collisions across parallel test runs.
|
||||
resource "random_id" "suffix" {
|
||||
byte_length = 4
|
||||
}
|
||||
|
||||
locals {
|
||||
suffix = var.suffix != null ? var.suffix : random_id.suffix.hex
|
||||
name_prefix = "adk-trigger-test"
|
||||
|
||||
required_apis = [
|
||||
"run.googleapis.com",
|
||||
"pubsub.googleapis.com",
|
||||
"cloudbuild.googleapis.com",
|
||||
"eventarc.googleapis.com",
|
||||
"logging.googleapis.com",
|
||||
]
|
||||
}
|
||||
|
||||
resource "google_project_service" "apis" {
|
||||
for_each = toset(local.required_apis)
|
||||
|
||||
project = var.project_id
|
||||
service = each.value
|
||||
|
||||
disable_on_destroy = false
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
output "cloud_run_url" {
|
||||
description = "Base URL of the deployed Cloud Run trigger-echo-agent service."
|
||||
value = data.google_cloud_run_v2_service.trigger_agent.uri
|
||||
}
|
||||
|
||||
output "pubsub_topic" {
|
||||
description = "Fully qualified Pub/Sub topic name for direct-push tests."
|
||||
value = google_pubsub_topic.trigger_test.id
|
||||
}
|
||||
|
||||
output "pubsub_topic_short" {
|
||||
description = "Short Pub/Sub topic name."
|
||||
value = google_pubsub_topic.trigger_test.name
|
||||
}
|
||||
|
||||
output "eventarc_topic" {
|
||||
description = "Fully qualified Pub/Sub topic name that fires the Eventarc trigger."
|
||||
value = google_pubsub_topic.eventarc_source.id
|
||||
}
|
||||
|
||||
output "eventarc_topic_short" {
|
||||
description = "Short Eventarc source topic name."
|
||||
value = google_pubsub_topic.eventarc_source.name
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
output "project_id" {
|
||||
description = "GCP project ID used for test resources."
|
||||
value = var.project_id
|
||||
}
|
||||
|
||||
output "region" {
|
||||
description = "GCP region used for test resources."
|
||||
value = var.region
|
||||
}
|
||||
|
||||
output "suffix" {
|
||||
description = "The random suffix used for resource naming."
|
||||
value = local.suffix
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pub/Sub topic + push subscription pointing to /trigger/pubsub
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "google_pubsub_topic" "trigger_test" {
|
||||
name = "${local.name_prefix}-${local.suffix}"
|
||||
project = var.project_id
|
||||
|
||||
depends_on = [google_project_service.apis]
|
||||
}
|
||||
|
||||
resource "google_pubsub_subscription" "trigger_push" {
|
||||
name = "${local.name_prefix}-push-${local.suffix}"
|
||||
project = var.project_id
|
||||
topic = google_pubsub_topic.trigger_test.id
|
||||
|
||||
push_config {
|
||||
push_endpoint = "${data.google_cloud_run_v2_service.trigger_agent.uri}/apps/trigger_echo_agent/trigger/pubsub"
|
||||
|
||||
oidc_token {
|
||||
service_account_email = google_service_account.pubsub_invoker.email
|
||||
audience = data.google_cloud_run_v2_service.trigger_agent.uri
|
||||
}
|
||||
}
|
||||
|
||||
# Short ack deadline for faster test feedback.
|
||||
ack_deadline_seconds = 30
|
||||
|
||||
# Retry policy for failed pushes.
|
||||
retry_policy {
|
||||
minimum_backoff = "10s"
|
||||
maximum_backoff = "60s"
|
||||
}
|
||||
|
||||
# Let the subscription persist until Terraform destroys it.
|
||||
# (default retention of 604800s is fine for tests)
|
||||
expiration_policy {
|
||||
ttl = ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
variable "project_id" {
|
||||
description = "GCP project ID for test resources."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "GCP region for Cloud Run and related resources."
|
||||
type = string
|
||||
default = "us-central1"
|
||||
}
|
||||
|
||||
variable "simulate_429_count" {
|
||||
description = "Number of 429 errors the echo agent simulates before succeeding (0 = disabled)."
|
||||
type = number
|
||||
default = 0
|
||||
}
|
||||
|
||||
variable "service_name" {
|
||||
description = "Optional name of a pre-deployed Cloud Run service. If provided, Terraform will use it instead of generating a name from the suffix."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "suffix" {
|
||||
description = "Optional suffix for resource naming. If not provided, a random one will be generated."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install the local ADK wheel (built from the repo under test).
|
||||
# Install the .whl file directly so pip uses it instead of PyPI.
|
||||
# Dependencies are resolved from PyPI automatically.
|
||||
COPY wheels/ /tmp/wheels/
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
RUN pip install --no-cache-dir /tmp/wheels/google_adk-*.whl \
|
||||
&& rm -rf /tmp/wheels
|
||||
|
||||
# Copy the agent into the expected adk layout:
|
||||
# /app/agents/trigger_echo_agent/__init__.py
|
||||
# /app/agents/trigger_echo_agent/agent.py
|
||||
COPY __init__.py agents/trigger_echo_agent/__init__.py
|
||||
COPY agent.py agents/trigger_echo_agent/agent.py
|
||||
|
||||
ENV PORT=8080
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["adk", "api_server", "--host=0.0.0.0", "--port=8080", "--trigger_sources=pubsub,eventarc", "/app/agents"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Echo agent for remote trigger integration tests.
|
||||
|
||||
Uses a before_model_callback to echo user input without calling the LLM,
|
||||
making tests fast, deterministic, and free of LLM model quota usage.
|
||||
|
||||
Supports optional 429 simulation via the SIMULATE_429_COUNT environment
|
||||
variable: when set to N > 0, the first N invocations per session will raise
|
||||
a RuntimeError containing "429 RESOURCE_EXHAUSTED" before succeeding.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models.llm_request import LlmRequest
|
||||
from google.adk.models.llm_response import LlmResponse
|
||||
from google.genai import types
|
||||
|
||||
# Track 429 simulation state across invocations within a process.
|
||||
# Keyed by session_id to allow per-session failure counts.
|
||||
_429_counter: dict[str, int] = {}
|
||||
|
||||
|
||||
def before_model_callback(
|
||||
callback_context: CallbackContext,
|
||||
llm_request: LlmRequest,
|
||||
) -> LlmResponse:
|
||||
"""Echo user input back without calling the LLM.
|
||||
|
||||
If SIMULATE_429_COUNT is set, raises a transient error for the first N
|
||||
invocations (per session) to exercise the retry-with-backoff logic in
|
||||
the trigger endpoints.
|
||||
"""
|
||||
fail_count = int(os.environ.get("SIMULATE_429_COUNT", "0"))
|
||||
session = callback_context.session
|
||||
session_id = session.id if session else "default"
|
||||
|
||||
if fail_count > 0:
|
||||
current = _429_counter.get(session_id, 0)
|
||||
if current < fail_count:
|
||||
_429_counter[session_id] = current + 1
|
||||
raise RuntimeError("429 RESOURCE_EXHAUSTED: simulated quota exceeded")
|
||||
|
||||
# Extract the most recent user message from the session events.
|
||||
user_text = ""
|
||||
if session and session.events:
|
||||
for event in reversed(session.events):
|
||||
if event.content and event.content.role == "user" and event.content.parts:
|
||||
user_text = event.content.parts[0].text or ""
|
||||
break
|
||||
|
||||
# Fall back to the current LLM request contents if no session event found.
|
||||
if not user_text and llm_request.contents:
|
||||
for content in reversed(llm_request.contents):
|
||||
if content.role == "user" and content.parts:
|
||||
user_text = content.parts[0].text or ""
|
||||
break
|
||||
|
||||
return LlmResponse(
|
||||
content=types.Content(
|
||||
role="model",
|
||||
parts=[types.Part(text=f"ECHO: {user_text}")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.5-flash",
|
||||
name="trigger_echo_agent",
|
||||
instruction="Echo agent for trigger testing.",
|
||||
before_model_callback=before_model_callback,
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Remote integration tests for the /apps/trigger_echo_agent/trigger/eventarc endpoint.
|
||||
|
||||
Tests cover:
|
||||
|
||||
/apps/trigger_echo_agent/trigger/eventarc on Cloud Run.
|
||||
2. Full pipeline tests — publish to the Eventarc source topic and
|
||||
verify the request reached Cloud Run via Cloud Logging.
|
||||
|
||||
Prerequisites:
|
||||
- GCP project with Eventarc, Pub/Sub, Cloud Run, and Cloud Logging
|
||||
APIs enabled.
|
||||
- ``gcloud`` CLI authenticated.
|
||||
- Terraform >= 1.5 installed.
|
||||
|
||||
Run:
|
||||
GCP_PROJECT_ID=my-project pytest tests/remote/test_trigger_eventarc.py -v -s
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from google.cloud import logging as cloud_logging
|
||||
from google.cloud import pubsub_v1
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Full Eventarc pipeline tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEventarcPipeline:
|
||||
"""Test the full Pub/Sub → Eventarc → Cloud Run pipeline.
|
||||
|
||||
Verification is done by checking Cloud Run request logs for successful
|
||||
POST requests to /apps/trigger_echo_agent/trigger/eventarc.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, eventarc_topic, project_id):
|
||||
self.publisher = pubsub_v1.PublisherClient()
|
||||
self.topic_path = eventarc_topic
|
||||
self.project_id = project_id
|
||||
self.logging_client = cloud_logging.Client(project=project_id)
|
||||
|
||||
def _publish_event(self, data: str, wait_seconds: int = 45) -> None:
|
||||
"""Publish a message to the Eventarc source topic and wait."""
|
||||
future = self.publisher.publish(
|
||||
self.topic_path,
|
||||
data.encode("utf-8"),
|
||||
)
|
||||
future.result(timeout=30)
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
def _count_successful_requests(
|
||||
self,
|
||||
path: str,
|
||||
since: datetime.datetime,
|
||||
) -> int:
|
||||
"""Count successful HTTP requests to the given path in Cloud Run logs."""
|
||||
timestamp = since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
filter_str = (
|
||||
'resource.type="cloud_run_revision" '
|
||||
f'httpRequest.requestUrl:"{path}" '
|
||||
"httpRequest.status=200 "
|
||||
f'timestamp>="{timestamp}"'
|
||||
)
|
||||
entries = list(
|
||||
self.logging_client.list_entries(
|
||||
filter_=filter_str,
|
||||
page_size=50,
|
||||
)
|
||||
)
|
||||
return len(entries)
|
||||
|
||||
def test_eventarc_pubsub_trigger(self):
|
||||
"""Publish to Eventarc source topic and verify Cloud Run processes it."""
|
||||
start_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
self._publish_event(json.dumps({"test": "eventarc-pipeline"}))
|
||||
count = self._count_successful_requests(
|
||||
"/apps/trigger_echo_agent/trigger/eventarc", start_time
|
||||
)
|
||||
assert count >= 1, (
|
||||
"Expected at least 1 successful request to"
|
||||
f" /apps/trigger_echo_agent/trigger/eventarc, found {count}"
|
||||
)
|
||||
|
||||
def test_eventarc_multiple_events(self):
|
||||
"""Publish multiple events and verify they are processed."""
|
||||
start_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
for i in range(3):
|
||||
future = self.publisher.publish(
|
||||
self.topic_path,
|
||||
json.dumps({"seq": i}).encode("utf-8"),
|
||||
)
|
||||
future.result(timeout=30)
|
||||
|
||||
# Eventarc delivery + log ingestion can be slow; wait longer.
|
||||
time.sleep(90)
|
||||
|
||||
count = self._count_successful_requests(
|
||||
"/apps/trigger_echo_agent/trigger/eventarc", start_time
|
||||
)
|
||||
assert count >= 1, (
|
||||
"Expected at least 1 successful request to"
|
||||
f" /apps/trigger_echo_agent/trigger/eventarc, found {count}"
|
||||
)
|
||||
@@ -0,0 +1,166 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Remote integration tests for the /apps/trigger_echo_agent/trigger/pubsub endpoint.
|
||||
|
||||
Tests cover:
|
||||
|
||||
2. Full pipeline tests — publish to a Pub/Sub topic with a push
|
||||
subscription pointing at the Cloud Run service, then verify via
|
||||
Cloud Logging that the requests reached the service.
|
||||
|
||||
Prerequisites:
|
||||
- GCP project with Pub/Sub, Cloud Run, and Cloud Logging APIs enabled.
|
||||
- ``gcloud`` CLI authenticated.
|
||||
- Terraform >= 1.5 installed.
|
||||
|
||||
Run:
|
||||
GCP_PROJECT_ID=my-project pytest tests/remote/test_trigger_pubsub.py -v -s
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
|
||||
from google.cloud import logging as cloud_logging
|
||||
from google.cloud import pubsub_v1
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Full Pub/Sub pipeline tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPubSubPipeline:
|
||||
"""Test the full Pub/Sub → push subscription → Cloud Run pipeline.
|
||||
|
||||
Verification is done by checking Cloud Run request logs for successful
|
||||
POST requests to /apps/trigger_echo_agent/trigger/pubsub. We look for
|
||||
httpRequest entries with
|
||||
status 200 that arrived after we published.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, pubsub_topic, project_id, cloud_run_url):
|
||||
self.publisher = pubsub_v1.PublisherClient()
|
||||
self.topic_path = pubsub_topic
|
||||
self.project_id = project_id
|
||||
self.cloud_run_url = cloud_run_url
|
||||
self.logging_client = cloud_logging.Client(project=project_id)
|
||||
|
||||
def _publish_and_wait(
|
||||
self,
|
||||
data: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
wait_seconds: int = 30,
|
||||
) -> None:
|
||||
"""Publish a message and wait for it to be delivered."""
|
||||
future = self.publisher.publish(
|
||||
self.topic_path,
|
||||
data.encode("utf-8"),
|
||||
**(attributes or {}),
|
||||
)
|
||||
future.result(timeout=30)
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
def _count_successful_requests(
|
||||
self,
|
||||
path: str,
|
||||
since: datetime.datetime,
|
||||
) -> int:
|
||||
"""Count successful HTTP requests to the given path in Cloud Run logs."""
|
||||
timestamp = since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
filter_str = (
|
||||
'resource.type="cloud_run_revision" '
|
||||
f'httpRequest.requestUrl:"{path}" '
|
||||
"httpRequest.status=200 "
|
||||
f'timestamp>="{timestamp}"'
|
||||
)
|
||||
entries = list(
|
||||
self.logging_client.list_entries(
|
||||
filter_=filter_str,
|
||||
page_size=200,
|
||||
)
|
||||
)
|
||||
return len(entries)
|
||||
|
||||
def test_publish_text_message(self):
|
||||
"""Publish a text message and verify it reaches Cloud Run."""
|
||||
start_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
self._publish_and_wait("hello-pipeline-test")
|
||||
count = self._count_successful_requests(
|
||||
"/apps/trigger_echo_agent/trigger/pubsub", start_time
|
||||
)
|
||||
assert count >= 1, (
|
||||
"Expected at least 1 successful request to"
|
||||
f" /apps/trigger_echo_agent/trigger/pubsub, found {count}"
|
||||
)
|
||||
|
||||
def test_publish_json_message(self):
|
||||
"""Publish a JSON message and verify processing."""
|
||||
start_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
data = json.dumps({"type": "json", "value": 42})
|
||||
self._publish_and_wait(data)
|
||||
count = self._count_successful_requests(
|
||||
"/apps/trigger_echo_agent/trigger/pubsub", start_time
|
||||
)
|
||||
assert count >= 1, (
|
||||
"Expected at least 1 successful request to"
|
||||
f" /apps/trigger_echo_agent/trigger/pubsub, found {count}"
|
||||
)
|
||||
|
||||
def test_publish_with_attributes(self):
|
||||
"""Publish a message with attributes and verify processing."""
|
||||
start_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
self._publish_and_wait(
|
||||
"attr-pipeline-test",
|
||||
attributes={"test_attr": "pytest"},
|
||||
)
|
||||
count = self._count_successful_requests(
|
||||
"/apps/trigger_echo_agent/trigger/pubsub", start_time
|
||||
)
|
||||
assert count >= 1, (
|
||||
"Expected at least 1 successful request to"
|
||||
f" /apps/trigger_echo_agent/trigger/pubsub, found {count}"
|
||||
)
|
||||
|
||||
def test_high_volume(self):
|
||||
"""Publish 20 messages and verify they are processed."""
|
||||
start_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
n = 20
|
||||
futures = []
|
||||
for i in range(n):
|
||||
future = self.publisher.publish(
|
||||
self.topic_path,
|
||||
f"volume-test-{i}".encode("utf-8"),
|
||||
)
|
||||
futures.append(future)
|
||||
|
||||
for f in futures:
|
||||
f.result(timeout=30)
|
||||
|
||||
# Wait for push delivery.
|
||||
time.sleep(15)
|
||||
|
||||
count = self._count_successful_requests(
|
||||
"/apps/trigger_echo_agent/trigger/pubsub", start_time
|
||||
)
|
||||
# Allow some tolerance — at least 80% should arrive.
|
||||
assert (
|
||||
count >= n * 0.8
|
||||
), f"Expected at least {int(n * 0.8)} successful requests, found {count}"
|
||||
Reference in New Issue
Block a user