# -*- coding: utf-8 -*- # # Copyright 2021 Google LLC. All Rights Reserved. # # 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. """Utilities for the promote operation.""" from googlecloudsdk.api_lib.clouddeploy import rollout from googlecloudsdk.command_lib.deploy import exceptions from googlecloudsdk.command_lib.deploy import release_util from googlecloudsdk.command_lib.deploy import rollout_util from googlecloudsdk.command_lib.deploy import target_util from googlecloudsdk.core import log from googlecloudsdk.core import resources _LAST_TARGET_IN_SEQUENCE = ( 'Release {} is already deployed to the last target ' '({}) in the promotion sequence.\n- Release: {}\n- Target: {}\n' ) # In progress List filter IN_PROGRESS_FILTER_TEMPLATE = 'state="IN_PROGRESS"' def Promote( release_ref, release_obj, to_target, is_create, rollout_id=None, annotations=None, labels=None, description=None, starting_phase_id=None, override_deploy_policies=None, ): """Creates a rollout for the given release in the destination target. If to_target is not specified and there is a rollout in progress, the promote will fail. Otherwise this computes the destination target based on the promotion sequence. Args: release_ref: protorpc.messages.Message, release resource object. release_obj: apitools.base.protorpclite.messages.Message, release message object. to_target: str, the target to promote the release to. is_create: bool, if creates a rollout during release creation. rollout_id: str, ID to assign to the generated rollout. annotations: dict[str,str], a dict of annotation (key,value) pairs that allow clients to store small amounts of arbitrary data in cloud deploy resources. labels: dict[str,str], a dict of label (key,value) pairs that can be used to select cloud deploy resources and to find collections of cloud deploy resources that satisfy certain conditions. description: str, rollout description. starting_phase_id: str, the starting phase for the rollout. override_deploy_policies: List of Deploy Policies to override. Raises: googlecloudsdk.command_lib.deploy.exceptions.RolloutIdExhausted googlecloudsdk.command_lib.deploy.exceptions.ReleaseInactiveError Returns: The rollout resource created. """ dest_target = to_target if not dest_target: dest_target = GetToTargetID(release_obj, is_create) rollout_resource = rollout_util.CreateRollout( release_ref, dest_target, rollout_id, annotations, labels, description, starting_phase_id, override_deploy_policies, ) # Check if it requires approval. target_obj = release_util.GetSnappedTarget(release_obj, dest_target) if target_obj and target_obj.requireApproval: log.status.Print('The rollout is pending approval.') return rollout_resource def GetToTargetID(release_obj, is_create): """Get the to_target parameter for promote API. This checks the promotion sequence to get the next stage to promote the release to. Args: release_obj: apitools.base.protorpclite.messages.Message, release message. is_create: bool, if getting the target for release creation. Returns: the target ID. Raises: NoStagesError: if no pipeline stages exist in the release. ReleaseInactiveError: if this is not called during release creation and the specified release has no rollouts. """ if not release_obj.deliveryPipelineSnapshot.serialPipeline.stages: raise exceptions.NoStagesError(release_obj.name) # Use release short name to avoid the issue by mixed use of # the project number and id. release_ref = resources.REGISTRY.ParseRelativeName( release_obj.name, collection='clouddeploy.projects.locations.deliveryPipelines.releases', ) to_target = release_obj.deliveryPipelineSnapshot.serialPipeline.stages[ 0 ].targetId # The order of pipeline stages represents the promotion sequence. # E.g. test->stage->prod. Here we start with the last stage. reversed_stages = list( reversed(release_obj.deliveryPipelineSnapshot.serialPipeline.stages) ) release_dict = release_ref.AsDict() for i, stage in enumerate(reversed_stages): target_ref = target_util.TargetReference( stage.targetId, release_dict['projectsId'], release_dict['locationsId'] ) # Starting with the last target in the promotion sequence per above, find # the last successfully deployed rollout to that target. current_rollout = target_util.GetCurrentRollout( target_ref, resources.REGISTRY.Parse( None, collection='clouddeploy.projects.locations.deliveryPipelines', params={ 'projectsId': release_dict['projectsId'], 'locationsId': release_dict['locationsId'], 'deliveryPipelinesId': release_dict['deliveryPipelinesId'], }, ), ) if current_rollout: current_rollout_ref = resources.REGISTRY.Parse( current_rollout.name, collection='clouddeploy.projects.locations.deliveryPipelines.releases.rollouts', ) # Promotes the release from the target that is farthest along in the # promotion sequence to its next stage in the promotion sequence. if current_rollout_ref.Parent().Name() == release_ref.Name(): if i > 0: to_target = reversed_stages[i - 1].targetId else: log.status.Print( _LAST_TARGET_IN_SEQUENCE.format( release_ref.Name(), target_ref.Name(), release_ref.RelativeName(), target_ref.RelativeName(), ) ) to_target = target_ref.RelativeName() # Once a target to promote to is found break out of the loop break # This means the release is not deployed to any target, # to_target flag is required in this case. if ( to_target == release_obj.deliveryPipelineSnapshot.serialPipeline.stages[0].targetId and not is_create ): raise exceptions.ReleaseInactiveError() return target_util.TargetId(to_target) def CheckIfInProgressRollout(release_ref, release_obj, to_target_id): """Checks if there are any rollouts in progress for the given release. Args: release_ref: protorpc.messages.Message, release resource object. release_obj: apitools.base.protorpclite.messages.Message, release message object. to_target_id: string, target id (e.g. 'prod') Raises: googlecloudsdk.command_lib.deploy.exceptions.RolloutInProgressError """ rollouts = list( rollout.RolloutClient().List( release_ref.RelativeName(), IN_PROGRESS_FILTER_TEMPLATE, limit=1 ) ) if rollouts: raise exceptions.RolloutInProgressError(release_obj.name, to_target_id)