288 lines
9.3 KiB
Python
288 lines
9.3 KiB
Python
# -*- 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)
|