Skip to content

Commit 48f54a7

Browse files
committed
fix(cloudrun): PoC Replace gcloud commands with a Client Library
1 parent 48023d8 commit 48f54a7

File tree

7 files changed

+321
-1
lines changed

7 files changed

+321
-1
lines changed

run/service-auth/build_image.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import os
2+
import tarfile
3+
import tempfile
4+
import time
5+
6+
from google.cloud.devtools import cloudbuild_v1
7+
from google.cloud.devtools.cloudbuild_v1.types import Build, BuildStep, BuildOptions, Source, StorageSource
8+
from google.cloud import storage
9+
10+
# from google.api_core.exceptions import NotFound
11+
12+
from deploy import deploy_cloud_run_service
13+
14+
PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
15+
16+
def build_image_from_source(
17+
project_id: str,
18+
region: str,
19+
service_name: str,
20+
source_directory: str = ".",
21+
gcs_bucket_name: str = None,
22+
image_tag: str = "latest",
23+
) -> str | None:
24+
"""Builds a container image from local source using Google Cloud Build
25+
and Google Cloud Buildpacks."""
26+
27+
build_client = cloudbuild_v1.CloudBuildClient()
28+
storage_client = storage.Client(project=project_id)
29+
30+
if not gcs_bucket_name:
31+
gcs_bucket_name = f"{project_id}-cloud-build-source"
32+
print(f"GCS bucket name not provided, using {gcs_bucket_name}. Ensure it exists.")
33+
34+
# TODO: Add bucket creation logic here
35+
36+
timestamp = int(time.time())
37+
gcs_source_object = f"source/{service_name}-{timestamp}.tar.gz"
38+
39+
# Use a temporary directory for the archive
40+
with tempfile.TemporaryDirectory() as tmpdir:
41+
source_archive = os.path.join(tmpdir, f"{service_name}-{timestamp}.tar.gz")
42+
43+
print(f"Packaging source from {source_directory} to {source_archive}")
44+
with tarfile.open(source_archive, "w:gz") as tar:
45+
# Add files from source_directory directly, arcname='.' means they are at the root of the tar
46+
tar.add(source_directory, arcname='.')
47+
48+
bucket = storage_client.bucket(gcs_bucket_name)
49+
blob = bucket.blob(gcs_source_object)
50+
print(f"Uploading {source_archive} to gs://{gcs_bucket_name}/{gcs_source_object}")
51+
blob.upload_from_filename(source_archive)
52+
print("Source uploaded.")
53+
54+
artifact_registry_repo = "cloud-run-source-deploy"
55+
image_name = f"{region}-docker.pkg.dev/{project_id}/{artifact_registry_repo}/{service_name}"
56+
full_image_uri = f"{image_name}:{image_tag}"
57+
58+
# Construct the Build request using the directly imported types
59+
build_request = Build(
60+
source=Source(
61+
storage_source=StorageSource(
62+
bucket=gcs_bucket_name,
63+
object_=gcs_source_object,
64+
)
65+
),
66+
images=[full_image_uri],
67+
steps=[
68+
BuildStep(
69+
name="gcr.io/k8s-skaffold/pack",
70+
args=[
71+
"build",
72+
full_image_uri,
73+
"--builder", "gcr.io/buildpacks/builder:v1",
74+
"--path", ".",
75+
],
76+
)
77+
],
78+
timeout={"seconds": 1200},
79+
options=BuildOptions(
80+
# Example: machine_type=BuildOptions.MachineType.E2_MEDIUM
81+
)
82+
)
83+
84+
print(f"Starting Cloud Build for image {full_image_uri}...")
85+
operation = build_client.create_build(project_id=project_id, build=build_request)
86+
87+
# The operation returned by create_build for google-cloud-build v1.x.x
88+
# is actually a google.api_core.operation.Operation, which has a `result()` method
89+
# or you can poll the build resource itself using build_client.get_build.
90+
# The `operation.metadata.build.id` pattern is more for long-running operations from other APIs.
91+
# Let's get the build ID from the name which is usually in the format projects/{project_id}/builds/{build_id}
92+
build_id = operation.name.split("/")[-1] # Extract build ID from operation.name
93+
print(f"Build operation created. Build ID: {build_id}")
94+
95+
# Get the initial build details to find the log URL
96+
initial_build_info = build_client.get_build(project_id=project_id, id=build_id)
97+
print(f"Logs URL: {initial_build_info.log_url}")
98+
print("Waiting for build to complete...")
99+
100+
while True:
101+
build_info = build_client.get_build(project_id=project_id, id=build_id)
102+
103+
if build_info.status == Build.Status.SUCCESS:
104+
print(f"Build {build_info.id} completed successfully.")
105+
print(f"Built image: {full_image_uri}")
106+
return full_image_uri
107+
elif build_info.status in [
108+
Build.Status.FAILURE,
109+
Build.Status.INTERNAL_ERROR,
110+
Build.Status.TIMEOUT,
111+
Build.Status.CANCELLED,
112+
]:
113+
print(f"Build {build_info.id} failed with status: {build_info.status.name}")
114+
print(f"Logs URL: {build_info.log_url}")
115+
return None
116+
117+
time.sleep(10)
118+
119+
120+
if __name__ == "__main__":
121+
REGION = "us-central1"
122+
SERVICE_NAME = "my-app-from-source"
123+
SOURCE_DIRECTORY = "./source/"
124+
GCS_BUILD_BUCKET = f"{PROJECT_ID}_cloudbuild_sources"
125+
126+
ENVIRONMENT_VARIABLES = {
127+
# "APP_MESSAGE": "Deployed from source via Python!",
128+
}
129+
130+
built_image_uri = None
131+
try:
132+
print(f"--- Step 1: Building image for {SERVICE_NAME} from source {SOURCE_DIRECTORY} ---")
133+
built_image_uri = build_image_from_source(
134+
project_id=PROJECT_ID,
135+
region=REGION,
136+
service_name=SERVICE_NAME,
137+
source_directory=SOURCE_DIRECTORY,
138+
gcs_bucket_name=GCS_BUILD_BUCKET
139+
)
140+
141+
if built_image_uri:
142+
print(f"\n--- Step 2: Deploying image {built_image_uri} to Cloud Run service {SERVICE_NAME} ---")
143+
deploy_cloud_run_service(
144+
project_id=PROJECT_ID,
145+
region=REGION,
146+
service_name=SERVICE_NAME,
147+
image_uri=built_image_uri,
148+
env_vars=ENVIRONMENT_VARIABLES,
149+
allow_unauthenticated=True,
150+
)
151+
print(f"\n--- Deployment process for {SERVICE_NAME} finished ---")
152+
else:
153+
print(f"Image build failed for {SERVICE_NAME}. Deployment aborted.")
154+
155+
except Exception as e:
156+
print(f"An error occurred during the build or deployment process: {e}")
157+
import traceback
158+
traceback.print_exc()

run/service-auth/deploy.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import os
2+
from google.cloud import run_v2
3+
from google.cloud.run_v2 import types
4+
from google.cloud.run_v2.types import revision_template as revision_template_types
5+
from google.cloud.run_v2.types import k8s_min as k8s_min_types
6+
from google.protobuf import field_mask_pb2
7+
8+
from google.api_core.exceptions import NotFound
9+
10+
from google.iam.v1 import policy_pb2, iam_policy_pb2
11+
12+
PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
13+
14+
15+
def deploy_cloud_run_service(
16+
project_id: str,
17+
region: str,
18+
service_name: str,
19+
image_uri: str,
20+
env_vars: dict = None,
21+
memory_limit: str = "512Mi",
22+
cpu_limit: str = "1",
23+
port: int = 8080,
24+
allow_unauthenticated: bool = True,
25+
):
26+
"""Deploys or updates a Cloud Run service using the Python client library."""
27+
client = run_v2.ServicesClient()
28+
29+
parent = f"projects/{project_id}/locations/{region}"
30+
full_service_name = f"{parent}/services/{service_name}"
31+
32+
container = types.Container(
33+
image=image_uri,
34+
ports=[k8s_min_types.ContainerPort(container_port=port)],
35+
resources=k8s_min_types.ResourceRequirements(
36+
limits={"cpu": cpu_limit, "memory": memory_limit}
37+
),
38+
)
39+
40+
if env_vars:
41+
container.env = [
42+
k8s_min_types.EnvVar(name=key, value=value) for key, value in env_vars.items()
43+
]
44+
45+
service_config = types.service.Service(
46+
name=full_service_name,
47+
template=revision_template_types.RevisionTemplate(
48+
containers=[container],
49+
scaling=types.RevisionScaling(
50+
min_instance_count=0,
51+
max_instance_count=1,
52+
),
53+
),
54+
traffic=[
55+
types.TrafficTarget(
56+
type_=types.TrafficTargetAllocationType.TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST,
57+
percent=100,
58+
)
59+
]
60+
)
61+
62+
deployed_service = None
63+
try:
64+
client.get_service(name=full_service_name)
65+
print(f"Service {service_name} found. Attempting to update...")
66+
67+
update_mask = field_mask_pb2.FieldMask()
68+
update_mask.paths.append("template")
69+
update_mask.paths.append("traffic")
70+
71+
operation = client.update_service(service=service_config, field_mask=update_mask)
72+
print(f"Update operation started for service {service_name}: {operation.operation.name}")
73+
deployed_service = operation.result()
74+
print(f"Service {service_name} updated successfully: {deployed_service.uri}")
75+
76+
except NotFound as e:
77+
print(f"Service {service_name} not found. Attempting to create...")
78+
service_config.name = "" # API will construct the full name based on service_id
79+
80+
operation = client.create_service(
81+
parent=parent,
82+
service=service_config,
83+
service_id=service_name,
84+
)
85+
print(f"Create operation started for service {service_name}: {operation.operation.name}")
86+
deployed_service = operation.result()
87+
print(f"Service {service_name} created successfully: {deployed_service.uri}")
88+
except Exception as e:
89+
print(f"An error occurred during service deployment: {e}")
90+
raise
91+
92+
# --- Allow unauthenticated requests if requested ---
93+
if deployed_service and allow_unauthenticated:
94+
try:
95+
print(f"Attempting to allow unauthenticated access for {service_name}...")
96+
# Get the current IAM policy
97+
# The resource path for IAM is the full service name
98+
policy_request = iam_policy_pb2.GetIamPolicyRequest(resource=deployed_service.name)
99+
current_policy = client.get_iam_policy(request=policy_request)
100+
101+
# Create a new binding for allUsers
102+
new_binding = policy_pb2.Binding(
103+
role="roles/run.invoker",
104+
members=["allUsers"],
105+
)
106+
107+
# Add the new binding to the policy
108+
# Be careful not to remove existing bindings.
109+
# Check if this binding already exists to avoid duplicates (optional but good practice)
110+
binding_exists = False
111+
for binding in current_policy.bindings:
112+
if binding.role == new_binding.role and "allUsers" in binding.members:
113+
print(f"Binding for allUsers with role {new_binding.role} already exists.")
114+
binding_exists = True
115+
break
116+
117+
if not binding_exists:
118+
current_policy.bindings.append(new_binding)
119+
120+
set_policy_request = iam_policy_pb2.SetIamPolicyRequest(
121+
resource=deployed_service.name,
122+
policy=current_policy,
123+
)
124+
client.set_iam_policy(request=set_policy_request)
125+
print(f"Successfully allowed unauthenticated access for {service_name}.")
126+
else:
127+
# If you want to ensure the policy is set even if only other bindings change,
128+
# you might still call set_iam_policy here, but for just adding allUsers,
129+
# this check avoids an unnecessary API call if it's already public.
130+
pass
131+
132+
133+
except Exception as e:
134+
print(f"Failed to set IAM policy for unauthenticated access: {e}")
135+
# Depending on requirements, you might want to raise this error or just log it.
136+
137+
return deployed_service
138+
139+
140+
if __name__ == "__main__":
141+
REGION = "us-central1"
142+
SERVICE_NAME = "my-python-deployed-service"
143+
IMAGE_URI = "gcr.io/cloudrun/hello"
144+
145+
# Optional environment variables
146+
ENVIRONMENT_VARIABLES = {}
147+
148+
deploy_cloud_run_service(
149+
project_id=PROJECT_ID,
150+
region=REGION,
151+
service_name=SERVICE_NAME,
152+
image_uri=IMAGE_URI,
153+
env_vars=ENVIRONMENT_VARIABLES,
154+
)
155+
156+
try:
157+
pass
158+
except Exception as e:
159+
print(f"Deployment failed: {e}")

run/service-auth/receive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
For example for Cloud Run or Cloud Functions.
1818
"""
1919

20-
# This sample will be migrated to app.py
20+
# This sample is marked to be migrated to app.py
2121

2222
# [START auth_validate_and_decode_bearer_token_on_flask]
2323
# [START cloudrun_service_to_service_receive]

run/service-auth/receive_auth_requests_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import backoff
2424

2525
from google.auth.transport import requests as transport_requests
26+
from google.cloud import run_v2
2627
from google.oauth2 import id_token
2728

2829
import pytest
@@ -32,6 +33,7 @@
3233
from requests.packages.urllib3.util.retry import Retry
3334
from requests.sessions import Session
3435

36+
3537
PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
3638

3739
STATUS_FORCELIST = [

run/service-auth/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
google-auth==2.40.1
22
google-cloud-run==0.10.18
3+
google-cloud-build==3.31.1
34
requests==2.32.3
45
Flask==3.1.0
56
gunicorn==23.0.0
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)