def WaitForCondition(poller, error_class): """Wait for a configuration to be ready in latest revision. Args: poller: A serverless_operations.ConditionPoller object. error_class: Error to raise on timeout failure Returns: A googlecloudsdk.command_lib.run.condition.Conditions object. Raises: error_class: Max retry limit exceeded. """ try: return waiter.PollUntilDone( poller, None, max_wait_ms=_POLLING_TIMEOUT_MS, wait_ceiling_ms=_RETRY_TIMEOUT_MS) except retry.RetryException: conditions = poller.GetConditions() # err.message already indicates timeout. Check ready_cond_type for more # information. msg = conditions.DescriptiveMessage() if conditions else None raise error_class(msg)
def WaitForCondition(self, poller): """Wait for a configuration to be ready in latest revision. Args: poller: A ConditionPoller object. Returns: A condition.Conditions object. Raises: RetryException: Max retry limit exceeded. ConfigurationError: configuration failed to """ try: return waiter.PollUntilDone(poller, None, wait_ceiling_ms=1000) except retry.RetryException as err: conditions = poller.GetConditions() # err.message already indicates timeout. Check ready_cond_type for more # information. msg = conditions.DescriptiveMessage() if conditions else None if msg: log.error('Still waiting: {}'.format(msg)) raise err if not conditions.IsReady(): raise serverless_exceptions.ConfigurationError( conditions.DescriptiveMessage())
def WaitForCondition(self, getter): """Wait for a configuration to be ready in latest revision.""" stages = _ServiceStages() with progress_tracker.StagedProgressTracker( 'Deploying...', stages.values(), failure_message='Deployment failed') as tracker: config_poller = ConditionPoller(getter, tracker, stages, dependencies={ 'RoutesReady': {'ConfigurationsReady'}, }) try: conditions = waiter.PollUntilDone( config_poller, None, wait_ceiling_ms=1000) except retry.RetryException as err: conditions = config_poller.GetConditions() # err.message already indicates timeout. Check ready_cond_type for more # information. msg = conditions.DescriptiveMessage() if conditions else None if msg: log.error('Still waiting: {}'.format(msg)) raise err if not conditions.IsReady(): raise serverless_exceptions.ConfigurationError( conditions.DescriptiveMessage())
def _WaitForOperation(client, operation, resource_type): """Waits for an operation to complete. Args: client: GAPIC API client, client used to make requests. operation: run_apps.v1alpha1.operation, object to wait for. resource_type: type, the expected type of resource response Returns: The resulting resource of input paramater resource_type. """ poller = waiter.CloudOperationPoller(resource_type, client.projects_locations_operations) operation_ref = resources.REGISTRY.ParseRelativeName( operation.name, collection='{}.projects.locations.operations'.format(API_NAME)) try: return poller.GetResult(waiter.PollUntilDone( poller, operation_ref, max_wait_ms=_POLLING_TIMEOUT_MS, wait_ceiling_ms=_RETRY_TIMEOUT_MS)) except waiter.OperationError: operation = poller.Poll(operation_ref) raise exceptions.IntegrationsOperationError( 'OperationError: code={0}, message={1}'.format( operation.error.code, encoding.Decode(operation.error.message)))
def CreateDomainMapping(self, domain_mapping_ref, service_name, config_changes, force_override=False): """Create a domain mapping. Args: domain_mapping_ref: Resource, domainmapping resource. service_name: str, the service to which to map domain. config_changes: list of ConfigChanger to modify the domainmapping with force_override: bool, override an existing mapping of this domain. Returns: A domain_mapping.DomainMapping object. """ messages = self.messages_module new_mapping = domain_mapping.DomainMapping.New( self._client, domain_mapping_ref.namespacesId) new_mapping.name = domain_mapping_ref.domainmappingsId new_mapping.route_name = service_name new_mapping.force_override = force_override for config_change in config_changes: new_mapping = config_change.Adjust(new_mapping) request = messages.RunNamespacesDomainmappingsCreateRequest( domainMapping=new_mapping.Message(), parent=domain_mapping_ref.Parent().RelativeName()) with metrics.RecordDuration(metric_names.CREATE_DOMAIN_MAPPING): try: response = self._client.namespaces_domainmappings.Create(request) except api_exceptions.HttpConflictError: raise serverless_exceptions.DomainMappingCreationError( 'Domain mapping to [{}] for service [{}] already exists.'.format( domain_mapping_ref.Name(), service_name)) # 'run domain-mappings create' is synchronous. Poll for its completion.x with progress_tracker.ProgressTracker('Creating...'): mapping = waiter.PollUntilDone( DomainMappingResourceRecordPoller(self), domain_mapping_ref) ready = mapping.conditions.get('Ready') message = None if ready and ready.get('message'): message = ready['message'] if not mapping.records: if (mapping.ready_condition['reason'] == domain_mapping.MAPPING_ALREADY_EXISTS_CONDITION_REASON): raise serverless_exceptions.DomainMappingAlreadyExistsError( 'Domain mapping to [{}] is already in use elsewhere.'.format( domain_mapping_ref.Name())) raise serverless_exceptions.DomainMappingCreationError( message or 'Could not create domain mapping.') if message: log.status.Print(message) return mapping return domain_mapping.DomainMapping(response, messages)
def _GetBaseRevision(self, config, metadata, status): """Return a Revision for use as the "base revision" for a change. When making a change that should not affect the code running, the "base revision" is the revision that we should lock the code to - it's where we get the digest for the image to run. Getting this revision: * If there's a nonce in the revisonTemplate metadata, use that * If that query produces >1 or produces 0 after a short timeout, use the latestCreatedRevision in status. Arguments: config: Configuration, the configuration to get the base revision of. May have been derived from a Service. metadata: ObjectMeta, the metadata from the top-level object status: Union[ConfigurationStatus, ServiceStatus], the status of the top- level object. Returns: The base revision of the configuration. """ # Or returns None if not available by nonce & the control plane has not # implemented latestCreatedRevisionName on the Service object yet. base_revision_nonce = config.revision_labels.get(NONCE_LABEL, None) base_revision = None if base_revision_nonce: try: namespace_ref = self._registry.Parse( metadata.namespace, collection='run.namespaces') poller = NonceBasedRevisionPoller(self, namespace_ref) base_revision = poller.GetResult( waiter.PollUntilDone(poller, base_revision_nonce, sleep_ms=500, max_wait_ms=2000)) except retry.WaitException: pass # Nonce polling didn't work, because some client didn't post one or didn't # change one. Fall back to the (slightly racy) `latestCreatedRevisionName`. if not base_revision: # TODO(b/117663680) Getattr -> normal access. if getattr(status, 'latestCreatedRevisionName', None): # Get by latestCreatedRevisionName revision_ref = self._registry.Parse( status.latestCreatedRevisionName, params={'namespacesId': metadata.namespace}, collection='run.namespaces.revisions') base_revision = self.GetRevision(revision_ref) return base_revision
def CreateDomainMapping(self, domain_mapping_ref, service_name): """Create a domain mapping. Args: domain_mapping_ref: Resource, domainmapping resource. service_name: str, the service to which to map domain. Returns: A domain_mapping.DomainMapping object. """ messages = self._messages_module new_mapping = domain_mapping.DomainMapping.New( self._client, domain_mapping_ref.namespacesId) new_mapping.name = domain_mapping_ref.domainmappingsId new_mapping.route_name = service_name request = messages.RunNamespacesDomainmappingsCreateRequest( domainMapping=new_mapping.Message(), parent=domain_mapping_ref.Parent().RelativeName()) with metrics.RecordDuration(metric_names.CREATE_DOMAIN_MAPPING): try: response = self._client.namespaces_domainmappings.Create( request) except api_exceptions.HttpConflictError: raise serverless_exceptions.DomainMappingCreationError( 'Domain mapping to [{}] for service [{}] already exists.'. format(domain_mapping_ref.Name(), service_name)) # 'run domain-mappings create' is synchronous. Poll for its completion.x with progress_tracker.ProgressTracker('Creating...'): mapping = waiter.PollUntilDone( DomainMappingResourceRecordPoller(self), domain_mapping_ref) ready = mapping.conditions.get('Ready') records = getattr(mapping.status, 'resourceRecords', None) message = None if ready and ready.get('message'): message = ready['message'] if not records: raise serverless_exceptions.DomainMappingCreationError( message or 'Could not create domain mapping.') if message: log.status.Print(message) return records return domain_mapping.DomainMapping(response, messages)
def WaitForOperation(operation): """Silently waits for the given google.longrunning.Operation to complete. Args: operation: The operation to poll. Raises: apitools.base.py.HttpError: if the request returns an HTTP error Returns: The response field of the completed operation. """ op_service = ods_util.GetClient().projects_locations_operations op_resource = resources.REGISTRY.ParseRelativeName( operation.name, collection='ondemandscanning.projects.locations.operations') poller = waiter.CloudOperationPollerNoResources(op_service) return waiter.PollUntilDone(poller, op_resource)
def Delete(ref, getter, deleter, async_): """Deletes a resource for a surface, including a pretty progress tracker.""" if async_ is None: async_ = platforms.GetPlatform() != platforms.PLATFORM_MANAGED if async_: deleter(ref) return poller = DeletionPoller(getter) with progress_tracker.ProgressTracker( message='Deleting [{}]'.format(ref.Name()), detail_message_callback=poller.GetMessage): deleter(ref) res = waiter.PollUntilDone(poller, ref) if res: if poller.GetMessage(): raise serverless_exceptions.DeletionFailedError( 'Failed to delete [{}]: {}.'.format( ref.Name(), poller.GetMessage())) else: raise serverless_exceptions.DeletionFailedError( 'Failed to delete [{}].'.format(ref.Name()))
def WaitForCondition(self, getter): """Wait for a configuration to be ready in latest revision.""" with progress_tracker.StagedProgressTracker( 'Deploying...', _CONDITION_TO_STAGE.values(), failure_message='Deployment failed') as tracker: for stage in _CONDITION_TO_STAGE.values(): tracker.StartStage(stage) config_poller = ConditionPoller(getter, tracker) try: conditions = waiter.PollUntilDone(config_poller, None) except retry.RetryException as err: conditions = config_poller.GetConditions() # err.message already indicates timeout. Check ready_cond_type for more # information. msg = conditions.DescriptiveMessage() if conditions else None if msg: log.error('Still waiting: {}'.format(msg)) raise err if not conditions.IsReady(): raise serverless_exceptions.ConfigurationError( conditions.DescriptiveMessage())
def ReleaseService(self, service_ref, config_changes, tracker=None, asyn=False, allow_unauthenticated=None, for_replace=False, prefetch=False, build_op_ref=None, build_log_url=None): """Change the given service in prod using the given config_changes. Ensures a new revision is always created, even if the spec of the revision has not changed. Arguments: service_ref: Resource, the service to release. config_changes: list, objects that implement Adjust(). tracker: StagedProgressTracker, to report on the progress of releasing. asyn: bool, if True, return without waiting for the service to be updated. allow_unauthenticated: bool, True if creating a hosted Cloud Run service which should also have its IAM policy set to allow unauthenticated access. False if removing the IAM policy to allow unauthenticated access from a service. for_replace: bool, If the change is for a replacing the service from a YAML specification. prefetch: the service, pre-fetched for ReleaseService. `False` indicates the caller did not perform a prefetch; `None` indicates a nonexistant service. build_op_ref: The reference to the build. build_log_url: The log url of the build result. """ if tracker is None: tracker = progress_tracker.NoOpStagedProgressTracker( stages.ServiceStages(allow_unauthenticated is not None), interruptable=True, aborted_message='aborted') if build_op_ref is not None: tracker.StartStage(stages.BUILD_READY) tracker.UpdateHeaderMessage('Building Container.') tracker.UpdateStage( stages.BUILD_READY, 'Logs are available at [{build_log_url}].'.format( build_log_url=build_log_url)) client = cloudbuild_util.GetClientInstance() poller = waiter.CloudOperationPoller(client.projects_builds, client.operations) operation = waiter.PollUntilDone(poller, build_op_ref) response_dict = encoding.MessageToPyValue(operation.response) if response_dict and response_dict['status'] != 'SUCCESS': tracker.FailStage( stages.BUILD_READY, None, message='Container build failed and ' 'logs are available at [{build_log_url}].'.format( build_log_url=build_log_url)) return else: tracker.CompleteStage(stages.BUILD_READY) if prefetch is None: serv = None else: serv = prefetch or self.GetService(service_ref) if for_replace: with_image = True else: with_image = any( isinstance(c, config_changes_mod.ImageChange) for c in config_changes) self._AddRevisionForcingChange(serv, config_changes) if serv and not with_image: # Avoid changing the running code by making the new revision by digest self._EnsureImageDigest(serv, config_changes) config_changes = [_SetClientNameAndVersion()] + config_changes self._UpdateOrCreateService( service_ref, config_changes, with_image, serv) if allow_unauthenticated is not None: try: tracker.StartStage(stages.SERVICE_IAM_POLICY_SET) tracker.UpdateStage(stages.SERVICE_IAM_POLICY_SET, '') self.AddOrRemoveIamPolicyBinding(service_ref, allow_unauthenticated, ALLOW_UNAUTH_POLICY_BINDING_MEMBER, ALLOW_UNAUTH_POLICY_BINDING_ROLE) tracker.CompleteStage(stages.SERVICE_IAM_POLICY_SET) except api_exceptions.HttpError: warning_message = ( 'Setting IAM policy failed, try "gcloud beta run services ' '{}-iam-policy-binding --region={region} --member=allUsers ' '--role=roles/run.invoker {service}"'.format( 'add' if allow_unauthenticated else 'remove', region=self._region, service=service_ref.servicesId)) tracker.CompleteStageWithWarning( stages.SERVICE_IAM_POLICY_SET, warning_message=warning_message) if not asyn: getter = functools.partial(self.GetService, service_ref) poller = ServiceConditionPoller( getter, tracker, dependencies=stages.ServiceDependencies(), serv=serv) self.WaitForCondition(poller) for msg in run_condition.GetNonTerminalMessages(poller.GetConditions()): tracker.AddWarning(msg)
def _GetBaseRevision(self, template, metadata, status): """Return a Revision for use as the "base revision" for a change. When making a change that should not affect the code running, the "base revision" is the revision that we should lock the code to - it's where we get the digest for the image to run. Getting this revision: * If there's a name in the template metadata, use that * If there's a nonce in the revisonTemplate metadata, use that * If that query produces >1 or 0 after a short timeout, use the latestCreatedRevision in status. Arguments: template: Revision, the revision template to get the base revision of. May have been derived from a Service. metadata: ObjectMeta, the metadata from the top-level object status: Union[ConfigurationStatus, ServiceStatus], the status of the top- level object. Returns: The base revision of the configuration or None if not found by revision name nor nonce and latestCreatedRevisionName does not exist on the Service object. """ base_revision = None # Try to find by revision name base_revision_name = template.name if base_revision_name: try: revision_ref_getter = functools.partial( self._registry.Parse, params={'namespacesId': metadata.namespace}, collection='run.namespaces.revisions') poller = RevisionNameBasedPoller(self, revision_ref_getter) base_revision = poller.GetResult( waiter.PollUntilDone( poller, base_revision_name, sleep_ms=500, max_wait_ms=2000)) except retry.RetryException: pass # Name polling didn't work. Fall back to nonce polling if not base_revision: base_revision_nonce = template.labels.get(revision.NONCE_LABEL, None) if base_revision_nonce: try: # TODO(b/150322097): Remove this when the api has been split. # This try/except block is needed because the v1alpha1 and v1 run apis # have different collection names for the namespaces. try: namespace_ref = self._registry.Parse( metadata.namespace, collection='run.namespaces') except resources.InvalidCollectionException: namespace_ref = self._registry.Parse( metadata.namespace, collection='run.api.v1.namespaces') poller = NonceBasedRevisionPoller(self, namespace_ref) base_revision = poller.GetResult(waiter.PollUntilDone( poller, base_revision_nonce, sleep_ms=500, max_wait_ms=2000)) except retry.RetryException: pass # Nonce polling didn't work, because some client didn't post one or didn't # change one. Fall back to the (slightly racy) `latestCreatedRevisionName`. if not base_revision: # TODO(b/117663680) Getattr -> normal access. if getattr(status, 'latestCreatedRevisionName', None): # Get by latestCreatedRevisionName revision_ref = self._registry.Parse( status.latestCreatedRevisionName, params={'namespacesId': metadata.namespace}, collection='run.namespaces.revisions') base_revision = self.GetRevision(revision_ref) return base_revision
def testPollUntilDone(self): waiter.PollUntilDone(poller=OperationPoller(10), operation_ref='operation-X') self.AssertOutputEquals('') self.AssertErrEquals('')
def WaitForApplyLROWithStagedTracker(poller, operation_ref, message, preview=False): """Waits for an "apply" deployment/preview LRO using a StagedProgressTracker. This function is a wrapper around waiter.PollUntilDone that uses a progress_tracker.StagedProgressTracker to display the individual steps of an apply deployment or preview LRO. Args: poller: a waiter.Poller instance operation_ref: Reference to the operation to poll on. message: string containing the main progress message to display. preview: bool, True if it's a preview LRO, False if it's a deployment LRO. Returns: A response object message from the LRO (i.e. a messages.Preview or messages.Deployment). """ messages = GetMessagesModule() stages = [] progress_stages = ApplyProgressStages(preview) for key, msg in progress_stages.items(): stages.append(progress_tracker.Stage(msg, key)) with progress_tracker.StagedProgressTracker( message=message, stages=stages, tracker_id='meta.deployment_progress') as tracker: def _StatusUpdate(result, status): """Updates poller.detailed_message on every tick with an appropriate message. Args: result: the latest messages.Operation object. status: unused. """ del status # Unused by this logic # Need to encode to JSON and then decode to Message to be able to # reasonably access attributes. json = encoding.MessageToJson(result.metadata) deployment_metadata = encoding.JsonToMessage( messages.OperationMetadata, json).deploymentMetadata if deployment_metadata and deployment_metadata.step and progress_stages.get( deployment_metadata.step.name) is not None: tracker.StartStage(deployment_metadata.step.name) # Complete all previous stages. ordered_stages = list(progress_stages.keys()) current_index = ordered_stages.index( deployment_metadata.step.name) for i in range(current_index): if not tracker.IsComplete(ordered_stages[i]): tracker.CompleteStage(ordered_stages[i]) try: operation = waiter.PollUntilDone(poller, operation_ref, status_update=_StatusUpdate, max_wait_ms=_MAX_WAIT_TIME_MS, wait_ceiling_ms=_WAIT_CEILING_MS) except retry.WaitException: # Operation timed out. raise waiter.TimeoutError( '{0} timed out after {1} seconds. Please retry this operation.' .format(operation_ref.Name(), _MAX_WAIT_TIME_MS / 1000)) result = poller.GetResult(operation) if preview: # Preview result from the LRO needs to be converted to Message since # there is no Get method for preview and poller doesn't automatically # do this. json = encoding.MessageToJson(result) result = encoding.JsonToMessage(messages.Preview, json) is_complete = (result.state == messages.Preview.StateValueValuesEnum.COMPLETED) else: is_complete = (result.state == messages.Deployment.StateValueValuesEnum.ACTIVE) if result is not None and is_complete: for stage in stages: if not tracker.IsComplete(stage.key): tracker.CompleteStage(stage.key) return result