# -*- 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 cloud deploy rollout resource.""" from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.clouddeploy import client_util from googlecloudsdk.api_lib.clouddeploy import release from googlecloudsdk.api_lib.clouddeploy import rollout from googlecloudsdk.calliope import exceptions from googlecloudsdk.command_lib.deploy import exceptions as cd_exceptions from googlecloudsdk.core import log from googlecloudsdk.core import resources from googlecloudsdk.generated_clients.apis.clouddeploy.v1 import clouddeploy_v1_messages _ROLLOUT_COLLECTION = ( 'clouddeploy.projects.locations.deliveryPipelines.releases.rollouts' ) PENDING_APPROVAL_FILTER_TEMPLATE = ( 'approvalState="NEEDS_APPROVAL" AND ' 'state="PENDING_APPROVAL" AND targetId="{}"' ) DEPLOYED_ROLLOUT_FILTER_TEMPLATE = ( '(approvalState!="REJECTED" AND ' 'approvalState!="NEEDS_APPROVAL") AND state="SUCCEEDED" AND targetId="{}"' ) ROLLOUT_IN_TARGET_FILTER_TEMPLATE = 'targetId="{}"' ROLLOUT_ID_TEMPLATE = '{}-to-{}-{:04d}' WILDCARD_RELEASE_NAME_TEMPLATE = '{}/releases/-' SUCCEED_ROLLOUT_ORDERBY = 'DeployEndTime desc' PENDING_ROLLOUT_ORDERBY = 'CreateTime desc' ENQUEUETIME_ROLLOUT_ORDERBY = 'EnqueueTime desc' def RolloutReferenceFromName(rollout_name): """Returns a rollout reference object from a rollout message. Args: rollout_name: str, full canonical resource name of the rollout Returns: Rollout reference object """ return resources.REGISTRY.ParseRelativeName( rollout_name, collection=_ROLLOUT_COLLECTION ) def RolloutId(rollout_name_or_id): """Returns rollout ID. Args: rollout_name_or_id: str, rollout full name or ID. Returns: Rollout ID. """ rollout_id = rollout_name_or_id if 'projects/' in rollout_name_or_id: rollout_id = resources.REGISTRY.ParseRelativeName( rollout_name_or_id, collection=_ROLLOUT_COLLECTION ).Name() return rollout_id def ListPendingRollouts(target_ref, pipeline_ref): """Lists the rollouts in PENDING_APPROVAL state for the releases associated with the specified target. The rollouts must be approvalState=NEEDS_APPROVAL and state=PENDING_APPROVAL. The returned list is sorted by rollout's create time. Args: target_ref: protorpc.messages.Message, target object. pipeline_ref: protorpc.messages.Message, pipeline object. Returns: a sorted list of rollouts. """ filter_str = PENDING_APPROVAL_FILTER_TEMPLATE.format(target_ref.Name()) parent = WILDCARD_RELEASE_NAME_TEMPLATE.format(pipeline_ref.RelativeName()) return rollout.RolloutClient().List( release_name=parent, filter_str=filter_str, order_by=PENDING_ROLLOUT_ORDERBY, ) def GetFilteredRollouts( target_ref, pipeline_ref, filter_str, order_by, page_size=None, limit=None ): """Gets successfully deployed rollouts for the releases associated with the specified target and index. Args: target_ref: protorpc.messages.Message, target object. pipeline_ref: protorpc.messages.Message, pipeline object. filter_str: Filter string to use when listing rollouts. order_by: order_by field to use when listing rollouts. page_size: int, the maximum number of objects to return per page. limit: int, the maximum number of `Rollout` objects to return. Returns: a rollout object or None if no rollouts in the target. """ parent = WILDCARD_RELEASE_NAME_TEMPLATE.format(pipeline_ref.RelativeName()) return rollout.RolloutClient().List( release_name=parent, filter_str=filter_str.format(target_ref.Name()), order_by=order_by, page_size=page_size, limit=limit, ) def GenerateRolloutId(to_target, release_ref) -> str: filter_str = ROLLOUT_IN_TARGET_FILTER_TEMPLATE.format(to_target) try: rollouts = rollout.RolloutClient().List( release_ref.RelativeName(), filter_str ) return ComputeRolloutID(release_ref.Name(), to_target, rollouts) except apitools_exceptions.HttpError: raise cd_exceptions.ListRolloutsError(release_ref.RelativeName()) def CreateRollout( release_ref, to_target, rollout_id=None, annotations=None, labels=None, description=None, starting_phase_id=None, override_deploy_policies=None, ) -> clouddeploy_v1_messages.Rollout: """Creates a rollout by calling the rollout create API and waits for the operation to finish. Args: release_ref: protorpc.messages.Message, release resource object. to_target: str, the target to create create the rollout in. rollout_id: str, rollout ID. 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, rollout starting phase. override_deploy_policies: List of Deploy Policies to override. Raises: ListRolloutsError: an error occurred calling rollout list API. Returns: The rollout resource created. """ final_rollout_id = rollout_id if not final_rollout_id: final_rollout_id = GenerateRolloutId(to_target, release_ref) resource_dict = release_ref.AsDict() rollout_ref = resources.REGISTRY.Parse( final_rollout_id, collection=_ROLLOUT_COLLECTION, params={ 'projectsId': resource_dict.get('projectsId'), 'locationsId': resource_dict.get('locationsId'), 'deliveryPipelinesId': resource_dict.get('deliveryPipelinesId'), 'releasesId': release_ref.Name(), }, ) rollout_obj = client_util.GetMessagesModule().Rollout( name=rollout_ref.RelativeName(), targetId=to_target, description=description, ) log.status.Print( 'Creating rollout {} in target {}'.format( rollout_ref.RelativeName(), to_target ) ) operation = rollout.RolloutClient().Create( rollout_ref, rollout_obj, annotations, labels, starting_phase_id, override_deploy_policies, ) operation_ref = resources.REGISTRY.ParseRelativeName( operation.name, collection='clouddeploy.projects.locations.operations' ) client_util.OperationsClient().WaitForOperation( operation, operation_ref, 'Waiting for rollout creation operation to complete', ) return rollout.RolloutClient().Get(rollout_ref.RelativeName()) def GetValidRollBackCandidate(target_ref, pipeline_ref): """Gets the currently deployed release and the next valid release that can be rolled back to. Args: target_ref: protorpc.messages.Message, target resource object. pipeline_ref: protorpc.messages.Message, pipeline resource object. Raises: HttpException: an error occurred fetching a resource. Returns: An list containg the currently deployed release and the next valid deployable release. """ iterable = GetFilteredRollouts( target_ref=target_ref, pipeline_ref=pipeline_ref, filter_str=DEPLOYED_ROLLOUT_FILTER_TEMPLATE, order_by=SUCCEED_ROLLOUT_ORDERBY, limit=None, page_size=10, ) rollouts = [] for rollout_obj in iterable: if not rollouts: # Currently deployed rollout in target rollouts.append(rollout_obj) elif not _RolloutIsFromAbandonedRelease(rollout_obj): rollouts.append(rollout_obj) if len(rollouts) >= 2: break return rollouts def _RolloutIsFromAbandonedRelease(rollout_obj): rollout_ref = RolloutReferenceFromName(rollout_obj.name) release_ref = rollout_ref.Parent() try: release_obj = release.ReleaseClient().Get(release_ref.RelativeName()) except apitools_exceptions.HttpError as error: raise exceptions.HttpException(error) return release_obj.abandoned def ComputeRolloutID(release_id, target_id, rollouts) -> str: """Generates a rollout ID. Args: release_id: str, release ID. target_id: str, target ID. rollouts: [apitools.base.protorpclite.messages.Message], list of rollout messages. Returns: rollout ID. Raises: googlecloudsdk.command_lib.deploy.exceptions.RolloutIdExhaustedError: if there are more than 1000 rollouts with auto-generated ID. """ rollout_ids = {RolloutId(r.name) for r in rollouts} for i in range(1, 1001): # If the rollout ID is too long, the resource will fail to be created. # It is up to the user to mitigate this by passing an explicit rollout ID # to use, instead. rollout_id = ROLLOUT_ID_TEMPLATE.format(release_id, target_id, i) if rollout_id not in rollout_ids: return rollout_id raise cd_exceptions.RolloutIDExhaustedError(release_id)