Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 48f54a7

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

File tree

Expand file treeCollapse file tree

7 files changed

+321
-1
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+321
-1
lines changed

‎run/service-auth/build_image.py

Copy file name to clipboard
+158Lines 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

Copy file name to clipboard
+159Lines 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

Copy file name to clipboardExpand all lines: run/service-auth/receive.py
+1-1Lines 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

Copy file name to clipboardExpand all lines: run/service-auth/receive_auth_requests_test.py
+2Lines 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

Copy file name to clipboard
+1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.