410 lines
14 KiB
Python
410 lines
14 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2023 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.
|
|
|
|
# pylint: disable=raise-missing-from
|
|
"""Pollers for Serverless operations."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
from googlecloudsdk.api_lib.util import waiter
|
|
from googlecloudsdk.command_lib.run import exceptions as serverless_exceptions
|
|
from googlecloudsdk.core import exceptions
|
|
|
|
|
|
class DomainMappingResourceRecordPoller(waiter.OperationPoller):
|
|
"""Poll for when a DomainMapping first has resourceRecords."""
|
|
|
|
def __init__(self, ops):
|
|
self._ops = ops
|
|
|
|
def IsDone(self, mapping):
|
|
if getattr(mapping.status, 'resourceRecords', None):
|
|
return True
|
|
conditions = mapping.conditions
|
|
# pylint: disable=g-bool-id-comparison
|
|
# False (indicating failure) as distinct from None (indicating not sure yet)
|
|
if conditions and conditions['Ready']['status'] is False:
|
|
return True
|
|
# pylint: enable=g-bool-id-comparison
|
|
return False
|
|
|
|
def GetResult(self, mapping):
|
|
return mapping
|
|
|
|
def Poll(self, domain_mapping_ref):
|
|
return self._ops.GetDomainMapping(domain_mapping_ref)
|
|
|
|
|
|
class ConditionPoller(waiter.OperationPoller):
|
|
"""A poller for CloudRun resource creation or update.
|
|
|
|
Takes in a reference to a StagedProgressTracker, and updates it with progress.
|
|
"""
|
|
|
|
def __init__(
|
|
self, resource_getter, tracker, dependencies=None, ready_message='Done.'
|
|
):
|
|
"""Initialize the ConditionPoller.
|
|
|
|
Start any unblocked stages in the tracker immediately.
|
|
|
|
Arguments:
|
|
resource_getter: function, returns a resource with conditions.
|
|
tracker: a StagedProgressTracker to keep updated. It must contain a stage
|
|
for each condition in the dependencies map, if the dependencies map is
|
|
provided. The stage represented by each key can only start when the set
|
|
of conditions in the corresponding value have all completed. If a
|
|
condition should be managed by this ConditionPoller but depends on
|
|
nothing, it should map to an empty set. Conditions in the tracker but
|
|
*not* managed by the ConditionPoller should not appear in the dict.
|
|
dependencies: Dict[str, Set[str]], The dependencies between conditions
|
|
that are managed by this ConditionPoller. The values are the set of
|
|
conditions that must become true before the key begins being worked on
|
|
by the server. If the entire dependencies dict is None, the poller will
|
|
assume that all keys in the tracker are relevant and none have
|
|
dependencies.
|
|
ready_message: str, message to display in header of tracker when
|
|
conditions are ready.
|
|
"""
|
|
# _dependencies is a map of condition -> {preceding conditions}
|
|
# It is meant to be checked off as we finish things.
|
|
self._dependencies = {k: set() for k in tracker}
|
|
if dependencies is not None:
|
|
for k in dependencies:
|
|
# Add dependencies, only if they're still not complete. If a stage isn't
|
|
# in the tracker. consider it "already complete".
|
|
self._dependencies[k] = {
|
|
c
|
|
for c in dependencies[k]
|
|
if c in tracker and not tracker.IsComplete(c)
|
|
}
|
|
self._resource_getter = resource_getter
|
|
self._tracker = tracker
|
|
self._resource_fail_type = exceptions.Error
|
|
self._ready_message = ready_message
|
|
self._StartUnblocked()
|
|
|
|
def _IsBlocked(self, condition):
|
|
return condition in self._dependencies and self._dependencies[condition]
|
|
|
|
def IsDone(self, conditions):
|
|
"""Overrides.
|
|
|
|
Args:
|
|
conditions: A condition.Conditions object.
|
|
|
|
Returns:
|
|
a bool indicates whether `conditions` is terminal.
|
|
"""
|
|
if conditions is None:
|
|
return False
|
|
return conditions.IsTerminal()
|
|
|
|
def _PollTerminalSubconditions(self, conditions, conditions_message):
|
|
for condition in conditions.TerminalSubconditions():
|
|
if condition not in self._dependencies:
|
|
continue
|
|
message = conditions[condition]['message']
|
|
status = conditions[condition]['status']
|
|
self._PossiblyUpdateMessage(condition, message, conditions_message)
|
|
if status is None:
|
|
continue
|
|
elif status:
|
|
if self._PossiblyCompleteStage(condition, message):
|
|
# Check all terminal subconditions again to ensure any stages that
|
|
# were unblocked by this stage completing are re-checked before we
|
|
# check the ready condition
|
|
self._PollTerminalSubconditions(conditions, conditions_message)
|
|
break
|
|
else:
|
|
self._PossiblyFailStage(condition, message)
|
|
|
|
def Poll(self, unused_ref):
|
|
"""Overrides.
|
|
|
|
Args:
|
|
unused_ref: A string representing the operation reference. Currently it
|
|
must be 'deploy'.
|
|
|
|
Returns:
|
|
A condition.Conditions object.
|
|
"""
|
|
conditions = self.GetConditions()
|
|
|
|
if conditions is None or not conditions.IsFresh():
|
|
return None
|
|
|
|
conditions_message = conditions.DescriptiveMessage()
|
|
self._tracker.UpdateHeaderMessage(conditions_message)
|
|
|
|
self._PollTerminalSubconditions(conditions, conditions_message)
|
|
|
|
terminal_condition = conditions.TerminalCondition()
|
|
if conditions.IsReady():
|
|
self._tracker.UpdateHeaderMessage(self._ready_message)
|
|
if terminal_condition in self._dependencies:
|
|
self._PossiblyCompleteStage(terminal_condition, None)
|
|
self._tracker.Tick()
|
|
elif conditions.IsFailed():
|
|
if terminal_condition in self._dependencies:
|
|
self._PossiblyFailStage(terminal_condition, None)
|
|
raise self._resource_fail_type(conditions_message)
|
|
|
|
return conditions
|
|
|
|
def GetResource(self):
|
|
return self._resource_getter()
|
|
|
|
def _PossiblyUpdateMessage(self, condition, message, conditions_message):
|
|
"""Update the stage message.
|
|
|
|
Args:
|
|
condition: str, The name of the status condition.
|
|
message: str, The new message to display
|
|
conditions_message: str, The message from the conditions object we're
|
|
displaying..
|
|
"""
|
|
if condition not in self._tracker or self._tracker.IsComplete(condition):
|
|
return
|
|
|
|
if self._IsBlocked(condition):
|
|
return
|
|
|
|
if message != conditions_message:
|
|
self._tracker.UpdateStage(condition, message)
|
|
|
|
def _RecordConditionComplete(self, condition):
|
|
"""Take care of the internal-to-this-class bookkeeping stage complete."""
|
|
# Unblock anything that was blocked on this.
|
|
|
|
# Strategy: "check off" each dependency as we complete it by removing from
|
|
# the set in the value.
|
|
for requirements in self._dependencies.values():
|
|
requirements.discard(condition)
|
|
|
|
def _PossiblyCompleteStage(self, condition, message):
|
|
"""Complete the stage if it's not already complete.
|
|
|
|
Make sure the necessary internal bookkeeping is done.
|
|
|
|
Args:
|
|
condition: str, The name of the condition whose stage should be completed.
|
|
message: str, The detailed message for the condition.
|
|
|
|
Returns:
|
|
bool: True if stage was completed, False if no action taken
|
|
"""
|
|
if condition not in self._tracker or self._tracker.IsComplete(condition):
|
|
return False
|
|
# A blocked condition is likely to remain True (indicating the previous
|
|
# operation concerning it was successful) until the blocking condition(s)
|
|
# finish and it's time to switch to Unknown (the current operation
|
|
# concerning it is in progress). Don't mark those done before they switch to
|
|
# Unknown.
|
|
if not self._tracker.IsRunning(condition):
|
|
return False
|
|
self._RecordConditionComplete(condition)
|
|
self._StartUnblocked()
|
|
self._tracker.CompleteStage(condition, message)
|
|
return True
|
|
|
|
def _StartUnblocked(self):
|
|
"""Call StartStage in the tracker for any not-started not-blocked tasks.
|
|
|
|
Record the fact that they're started in our internal bookkeeping.
|
|
"""
|
|
# The set of stages that aren't marked started and don't have unsatisfied
|
|
# dependencies are newly unblocked.
|
|
for c in self._dependencies:
|
|
if c not in self._tracker:
|
|
continue
|
|
if self._tracker.IsWaiting(c) and not self._IsBlocked(c):
|
|
self._tracker.StartStage(c)
|
|
# TODO(b/120679874): Should not have to manually call Tick()
|
|
self._tracker.Tick()
|
|
|
|
def _PossiblyFailStage(self, condition, message):
|
|
"""Possibly fail the stage.
|
|
|
|
Args:
|
|
condition: str, The name of the status whose stage failed.
|
|
message: str, The detailed message for the condition.
|
|
|
|
Raises:
|
|
DeploymentFailedError: If the 'Ready' condition failed.
|
|
"""
|
|
# Don't fail an already failed stage.
|
|
if condition not in self._tracker or self._tracker.IsComplete(condition):
|
|
return
|
|
|
|
self._tracker.FailStage(
|
|
condition, self._resource_fail_type(message), message
|
|
)
|
|
|
|
def GetResult(self, conditions):
|
|
"""Overrides.
|
|
|
|
Get terminal conditions as the polling result.
|
|
|
|
Args:
|
|
conditions: A condition.Conditions object.
|
|
|
|
Returns:
|
|
A condition.Conditions object.
|
|
"""
|
|
return conditions
|
|
|
|
def GetConditions(self):
|
|
"""Returns the resource conditions wrapped in condition.Conditions.
|
|
|
|
Returns:
|
|
A condition.Conditions object.
|
|
"""
|
|
resource = self._resource_getter()
|
|
|
|
if resource is None:
|
|
return None
|
|
return resource.conditions
|
|
|
|
|
|
class ServiceConditionPoller(ConditionPoller):
|
|
"""A ConditionPoller for services."""
|
|
|
|
def __init__(self, getter, tracker, dependencies=None, serv=None):
|
|
super().__init__(getter, tracker, dependencies)
|
|
self._resource_fail_type = serverless_exceptions.DeploymentFailedError
|
|
|
|
|
|
class WorkerPoolConditionPoller(ConditionPoller):
|
|
"""A ConditionPoller for worker pools."""
|
|
|
|
def __init__(self, getter, tracker, dependencies=None, worker_pool=None):
|
|
super().__init__(getter, tracker, dependencies)
|
|
self._resource_fail_type = serverless_exceptions.DeploymentFailedError
|
|
|
|
|
|
class RevisionNameBasedPoller(waiter.OperationPoller):
|
|
"""Poll for the revision with the given name to exist."""
|
|
|
|
def __init__(self, operations, revision_ref_getter):
|
|
self._operations = operations
|
|
self._revision_ref_getter = revision_ref_getter
|
|
|
|
def IsDone(self, revision_obj):
|
|
return bool(revision_obj)
|
|
|
|
def Poll(self, revision_name):
|
|
revision_ref = self._revision_ref_getter(revision_name)
|
|
return self._operations.GetRevision(revision_ref)
|
|
|
|
def GetResult(self, revision_obj):
|
|
return revision_obj
|
|
|
|
|
|
class NonceBasedRevisionPoller(waiter.OperationPoller):
|
|
"""To poll for exactly one revision with the given nonce to appear."""
|
|
|
|
def __init__(self, operations, namespace_ref):
|
|
self._operations = operations
|
|
self._namespace = namespace_ref
|
|
|
|
def IsDone(self, revisions):
|
|
return bool(revisions)
|
|
|
|
def Poll(self, nonce):
|
|
return self._operations.GetRevisionsByNonce(self._namespace, nonce)
|
|
|
|
def GetResult(self, revisions):
|
|
if len(revisions) == 1:
|
|
return revisions[0]
|
|
return None
|
|
|
|
|
|
class ExecutionConditionPoller(ConditionPoller):
|
|
"""A ConditionPoller for jobs."""
|
|
|
|
def __init__(self, getter, tracker, terminal_condition, dependencies=None):
|
|
super().__init__(getter, tracker, dependencies)
|
|
self._resource_fail_type = serverless_exceptions.ExecutionFailedError
|
|
self._terminal_condition = terminal_condition
|
|
|
|
def _PotentiallyUpdateInstanceCompletions(self, job_obj, conditions):
|
|
"""Maybe update the terminal condition stage message with number of completions."""
|
|
terminal_condition = conditions.TerminalCondition()
|
|
if terminal_condition not in self._tracker or self._IsBlocked(
|
|
terminal_condition
|
|
):
|
|
return
|
|
|
|
self._tracker.UpdateStage(
|
|
terminal_condition,
|
|
'{} / {} complete'.format(
|
|
job_obj.status.succeededCount or 0, job_obj.task_count
|
|
),
|
|
)
|
|
|
|
def GetConditions(self):
|
|
"""Returns the resource conditions wrapped in condition.Conditions.
|
|
|
|
Returns:
|
|
A condition.Conditions object.
|
|
"""
|
|
job_obj = self._resource_getter()
|
|
|
|
if job_obj is None:
|
|
return None
|
|
|
|
conditions = job_obj.GetConditions(self._terminal_condition)
|
|
|
|
# This is a bit of a cheat to hook into the polling method. This is done
|
|
# because this is only place where the resource is gotten from the server,
|
|
# so reusing it saves an api call. This is also simpler than attempting to
|
|
# override the Poll method which would likely lead to duplicate code and/or
|
|
# complicated error handling.
|
|
self._PotentiallyUpdateInstanceCompletions(job_obj, conditions)
|
|
|
|
return conditions
|
|
|
|
|
|
class WaitOperationPoller(waiter.CloudOperationPoller):
|
|
"""Poll for a long running operation using Wait instead of Get."""
|
|
|
|
def __init__(self, messages_module, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._messages_module = messages_module
|
|
|
|
def Poll(self, operation_ref):
|
|
"""Overrides.
|
|
|
|
Args:
|
|
operation_ref: googlecloudsdk.core.resources.Resource.
|
|
|
|
Returns:
|
|
fetched operation message.
|
|
"""
|
|
request_type = self.operation_service.GetRequestType('Wait')
|
|
wait_req = self._messages_module.GoogleLongrunningWaitOperationRequest(
|
|
timeout='10s'
|
|
)
|
|
return self.operation_service.Wait(
|
|
request_type(
|
|
name=operation_ref.RelativeName(),
|
|
googleLongrunningWaitOperationRequest=wait_req,
|
|
)
|
|
)
|