@@ -124,6 +124,9 @@ class ComputeEngineInsertInstanceOperator(ComputeEngineBaseOperator):
124124 :param timeout: The amount of time, in seconds, to wait for the request to complete.
125125 Note that if `retry` is specified, the timeout applies to each individual attempt.
126126 :param metadata: Additional metadata that is provided to the method.
127+ :param recreate_if_machine_type_different: When True, delete and recreate the instance if
128+ the existing machine type differs from the requested body. Defaults to
129+ False, in which case differences are only logged.
127130 """
128131
129132 operator_extra_links = (ComputeInstanceDetailsLink (),)
@@ -156,6 +159,7 @@ def __init__(
156159 api_version : str = "v1" ,
157160 validate_body : bool = True ,
158161 impersonation_chain : str | Sequence [str ] | None = None ,
162+ recreate_if_machine_type_different : bool = False ,
159163 ** kwargs ,
160164 ) -> None :
161165 self .body = body
@@ -167,6 +171,7 @@ def __init__(
167171 self .retry = retry
168172 self .timeout = timeout
169173 self .metadata = metadata
174+ self .recreate_if_machine_type_different = recreate_if_machine_type_different
170175
171176 if validate_body :
172177 self ._field_validator = GcpBodyFieldValidator (
@@ -206,54 +211,123 @@ def _validate_all_body_fields(self) -> None:
206211 if self ._field_validator :
207212 self ._field_validator .validate (self .body )
208213
214+ def _extract_machine_type (self , value : str | None ) -> str | None :
215+ if not value :
216+ return None
217+ return value .split ("/" )[- 1 ]
218+
219+ def _detect_instance_drift (self , existing : Instance ) -> dict [str , Any ]:
220+ """Detect machine type differences between the existing instance and the requested body."""
221+ diffs = {}
222+
223+ # Compare machine_type.
224+ requested_machine_type = self .body .get ("machine_type" )
225+ existing_machine_type = getattr (existing , "machine_type" , None )
226+
227+ requested_name = self ._extract_machine_type (requested_machine_type )
228+ existing_name = self ._extract_machine_type (existing_machine_type )
229+
230+ if requested_name and existing_name and requested_name != existing_name :
231+ diffs ["machine_type" ] = {
232+ "existing" : existing_name ,
233+ "requested" : requested_name ,
234+ }
235+
236+ return diffs
237+
238+ def _create_instance (self , hook : ComputeEngineHook , context : Context ) -> dict :
239+ """Create the instance using the current body and return the created instance as dict."""
240+ self ._field_sanitizer .sanitize (self .body )
241+
242+ self .log .info ("Creating Instance with specified body: %s" , self .body )
243+
244+ hook .insert_instance (
245+ body = self .body ,
246+ request_id = self .request_id ,
247+ project_id = self .project_id ,
248+ zone = self .zone ,
249+ )
250+
251+ self .log .info ("The specified Instance has been created SUCCESSFULLY" )
252+
253+ new_instance = hook .get_instance (
254+ resource_id = self .resource_id ,
255+ project_id = self .project_id ,
256+ zone = self .zone ,
257+ )
258+
259+ ComputeInstanceDetailsLink .persist (
260+ context = context ,
261+ project_id = self .project_id or hook .project_id ,
262+ )
263+
264+ return Instance .to_dict (new_instance )
265+
209266 def execute (self , context : Context ) -> dict :
267+ """
268+ Ensure that a Compute Engine instance with the given name exists.
269+
270+ If the instance does not exist, it is created. If it already exists,
271+ presence is treated as success (presence-based idempotence).
272+
273+ If machine type drift is detected and ``recreate_if_machine_type_different=True``,
274+ the existing instance is deleted and recreated using the requested body.
275+ """
210276 hook = ComputeEngineHook (
211277 gcp_conn_id = self .gcp_conn_id ,
212278 api_version = self .api_version ,
213279 impersonation_chain = self .impersonation_chain ,
214280 )
215281 self ._validate_all_body_fields ()
216282 self .check_body_fields ()
283+
217284 try :
218- # Idempotence check (sort of) - we want to check if the new Instance
219- # is already created and if is, then we assume it was created previously - we do
220- # not check if content of the Instance is as expected.
221- # We assume success if the Instance is simply present.
222285 existing_instance = hook .get_instance (
223286 resource_id = self .resource_id ,
224287 project_id = self .project_id ,
225288 zone = self .zone ,
226289 )
227290 except exceptions .NotFound as e :
228- # We actually expect to get 404 / Not Found here as the should not yet exist
291+ # We expect a 404 here if the instance does not yet exist.
229292 if e .code != 404 :
230293 raise e
231- else :
232- self .log .info ("The %s Instance already exists" , self .resource_id )
233- ComputeInstanceDetailsLink .persist (
234- context = context ,
235- project_id = self .project_id or hook .project_id ,
294+
295+ # Create instance if it does not exist.
296+ return self ._create_instance (hook , context )
297+
298+ # Instance already exists.
299+ self .log .info ("The %s Instance already exists" , self .resource_id )
300+
301+ # Detect drift.
302+ diffs = self ._detect_instance_drift (existing_instance )
303+ if diffs :
304+ self .log .warning (
305+ "Existing instance '%s' differs from requested configuration: %s" ,
306+ self .resource_id ,
307+ diffs ,
236308 )
237- return Instance .to_dict (existing_instance )
238- self ._field_sanitizer .sanitize (self .body )
239- self .log .info ("Creating Instance with specified body: %s" , self .body )
240- hook .insert_instance (
241- body = self .body ,
242- request_id = self .request_id ,
243- project_id = self .project_id ,
244- zone = self .zone ,
245- )
246- self .log .info ("The specified Instance has been created SUCCESSFULLY" )
247- new_instance = hook .get_instance (
248- resource_id = self .resource_id ,
249- project_id = self .project_id ,
250- zone = self .zone ,
251- )
309+
310+ if self .recreate_if_machine_type_different :
311+ self .log .info (
312+ "Recreating instance '%s' because recreate_if_machine_type_different=True" ,
313+ self .resource_id ,
314+ )
315+
316+ hook .delete_instance (
317+ resource_id = self .resource_id ,
318+ project_id = self .project_id ,
319+ request_id = self .request_id ,
320+ zone = self .zone ,
321+ )
322+
323+ return self ._create_instance (hook , context )
324+
252325 ComputeInstanceDetailsLink .persist (
253326 context = context ,
254327 project_id = self .project_id or hook .project_id ,
255328 )
256- return Instance .to_dict (new_instance )
329+
330+ return Instance .to_dict (existing_instance )
257331
258332
259333class ComputeEngineInsertInstanceFromTemplateOperator (ComputeEngineBaseOperator ):
0 commit comments