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 ()
0 commit comments