# -*- coding: utf-8 -*- # # Copyright 2014 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. """Command for modifying backend services.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import io from apitools.base.protorpclite import messages from apitools.base.py import encoding from googlecloudsdk.api_lib.compute import base_classes from googlecloudsdk.api_lib.compute import property_selector from googlecloudsdk.calliope import base from googlecloudsdk.calliope import exceptions as calliope_exceptions from googlecloudsdk.command_lib.compute import exceptions from googlecloudsdk.command_lib.compute import flags as compute_flags from googlecloudsdk.command_lib.compute.backend_services import backend_services_utils from googlecloudsdk.command_lib.compute.backend_services import flags from googlecloudsdk.core import resources from googlecloudsdk.core import yaml from googlecloudsdk.core.console import console_io from googlecloudsdk.core.util import edit import six class InvalidResourceError(calliope_exceptions.ToolException): # Normally we'd want to subclass core.exceptions.Error, but base_classes.Edit # abuses ToolException to classify errors when displaying messages to users, # and we should continue to fit in that framework for now. pass class Edit(base.Command): """Modify a backend service. *{command}* modifies a backend service of a Google Cloud load balancer or Traffic Director. The backend service resource is fetched from the server and presented in a text editor that displays the configurable fields. The specific editor is defined by the ``EDITOR'' environment variable. The name of each backend corresponds to the name of an instance group, zonal NEG, serverless NEG, or internet NEG. To add, remove, or swap backends, use the `gcloud compute backend-services remove-backend` and `gcloud compute backend-services add-backend` commands. """ DEFAULT_FORMAT = 'yaml' _BACKEND_SERVICE_ARG = flags.GLOBAL_REGIONAL_BACKEND_SERVICE_ARG @classmethod def Args(cls, parser): cls._BACKEND_SERVICE_ARG.AddArgument(parser) def _ProcessEditedResource(self, holder, backend_service_ref, file_contents, original_object, original_record, modifiable_record, args): """Returns an updated resource that was edited by the user.""" # It's very important that we replace the characters of comment # lines with spaces instead of removing the comment lines # entirely. JSON and YAML deserialization give error messages # containing line, column, and the character offset of where the # error occurred. If the deserialization fails; we want to make # sure those numbers map back to what the user actually had in # front of him or her otherwise the errors will not be very # useful. non_comment_lines = '\n'.join( ' ' * len(line) if line.startswith('#') else line for line in file_contents.splitlines()) modified_record = base_classes.DeserializeValue( non_comment_lines, args.format or Edit.DEFAULT_FORMAT) # Normalizes all of the fields that refer to other # resource. (i.e., translates short names to URIs) reference_normalizer = property_selector.PropertySelector( transformations=self.GetReferenceNormalizers(holder.resources)) modified_record = reference_normalizer.Apply(modified_record) if modifiable_record == modified_record: new_object = None else: modified_record['name'] = original_record['name'] fingerprint = original_record.get('fingerprint') if fingerprint: modified_record['fingerprint'] = fingerprint new_object = encoding.DictToMessage(modified_record, holder.client.messages.BackendService) # If existing object is equal to the proposed object or if # there is no new object, then there is no work to be done, so we # return the original object. if not new_object or original_object == new_object: return [original_object] return holder.client.MakeRequests( [self.GetSetRequest(holder.client, backend_service_ref, new_object)]) def Run(self, args): holder = base_classes.ComputeApiHolder(self.ReleaseTrack()) client = holder.client backend_service_ref = self._BACKEND_SERVICE_ARG.ResolveAsResource( args, holder.resources, default_scope=backend_services_utils.GetDefaultScope(), scope_lister=compute_flags.GetDefaultScopeLister(client)) get_request = self.GetGetRequest(client, backend_service_ref) objects = client.MakeRequests([get_request]) original_object = objects[0] original_record = encoding.MessageToDict(original_object) # Selects only the fields that can be modified. field_selector = property_selector.PropertySelector(properties=[ 'backends', 'customRequestHeaders', 'customResponseHeaders', 'description', 'enableCDN', 'healthChecks', 'iap.enabled', 'iap.oauth2ClientId', 'iap.oauth2ClientSecret', 'port', 'portName', 'protocol', 'timeoutSec', ]) modifiable_record = field_selector.Apply(original_record) file_contents = self.BuildFileContents(args, client, original_record, modifiable_record) resource_list = self.EditResource(args, backend_service_ref, file_contents, holder, modifiable_record, original_object, original_record) for resource in resource_list: yield resource def BuildFileContents(self, args, client, original_record, modifiable_record): buf = io.StringIO() for line in base_classes.HELP.splitlines(): buf.write('#') if line: buf.write(' ') buf.write(line) buf.write('\n') buf.write('\n') buf.write(base_classes.SerializeDict(modifiable_record, args.format or Edit.DEFAULT_FORMAT)) buf.write('\n') example = base_classes.SerializeDict( encoding.MessageToDict(self.GetExampleResource(client)), args.format or Edit.DEFAULT_FORMAT) base_classes.WriteResourceInCommentBlock(example, 'Example resource:', buf) buf.write('#\n') original = base_classes.SerializeDict(original_record, args.format or Edit.DEFAULT_FORMAT) base_classes.WriteResourceInCommentBlock(original, 'Original resource:', buf) return buf.getvalue() def EditResource(self, args, backend_service_ref, file_contents, holder, modifiable_record, original_object, original_record): while True: try: file_contents = edit.OnlineEdit(file_contents) except edit.NoSaveException: raise exceptions.AbortedError('Edit aborted by user.') try: resource_list = self._ProcessEditedResource(holder, backend_service_ref, file_contents, original_object, original_record, modifiable_record, args) break except (ValueError, yaml.YAMLParseError, messages.ValidationError, calliope_exceptions.ToolException) as e: message = getattr(e, 'message', six.text_type(e)) if isinstance(e, calliope_exceptions.ToolException): problem_type = 'applying' else: problem_type = 'parsing' message = ('There was a problem {0} your changes: {1}' .format(problem_type, message)) if not console_io.PromptContinue( message=message, prompt_string='Would you like to edit the resource again?'): raise exceptions.AbortedError('Edit aborted by user.') return resource_list def GetExampleResource(self, client): uri_prefix = ('https://compute.googleapis.com/compute/v1/projects/' 'my-project/') instance_groups_uri_prefix = ( 'https://compute.googleapis.com/compute/v1/projects/' 'my-project/zones/') return client.messages.BackendService( backends=[ client.messages.Backend( balancingMode=( client.messages.Backend.BalancingModeValueValuesEnum.RATE), group=(instance_groups_uri_prefix + 'us-central1-a/instanceGroups/group-1'), maxRate=100), client.messages.Backend( balancingMode=( client.messages.Backend.BalancingModeValueValuesEnum.RATE), group=(instance_groups_uri_prefix + 'europe-west1-a/instanceGroups/group-2'), maxRate=150), ], customRequestHeaders=['X-Forwarded-Port:443'], customResponseHeaders=['X-Client-Geo-Location:US,Mountain View'], description='My backend service', healthChecks=[ uri_prefix + 'global/httpHealthChecks/my-health-check-1', uri_prefix + 'global/httpHealthChecks/my-health-check-2' ], name='backend-service', port=80, portName='http', protocol=client.messages.BackendService.ProtocolValueValuesEnum.HTTP, selfLink=uri_prefix + 'global/backendServices/backend-service', timeoutSec=30, ) def GetReferenceNormalizers(self, resource_registry): def MakeReferenceNormalizer(field_name, allowed_collections): """Returns a function to normalize resource references.""" def NormalizeReference(reference): """Returns normalized URI for field_name.""" try: value_ref = resource_registry.Parse(reference) except resources.UnknownCollectionException: raise InvalidResourceError( '[{field_name}] must be referenced using URIs.'.format( field_name=field_name)) if value_ref.Collection() not in allowed_collections: raise InvalidResourceError( 'Invalid [{field_name}] reference: [{value}].'. format( field_name=field_name, value=reference)) return value_ref.SelfLink() return NormalizeReference # Ensure group is a uri or full collection path representing an instance # group. Full uris/paths are required because if the user gives us less, we # don't want to be in the business of guessing health checks. return [ ('healthChecks[]', MakeReferenceNormalizer( 'healthChecks', ('compute.httpHealthChecks', 'compute.httpsHealthChecks', 'compute.healthChecks', 'compute.regionHealthChecks'))), ('backends[].group', MakeReferenceNormalizer( 'group', ('compute.instanceGroups', 'compute.regionInstanceGroups'))), ] def GetGetRequest(self, client, backend_service_ref): if backend_service_ref.Collection() == 'compute.regionBackendServices': return (client.apitools_client.regionBackendServices, 'Get', client.messages.ComputeRegionBackendServicesGetRequest( **backend_service_ref.AsDict())) return (client.apitools_client.backendServices, 'Get', client.messages.ComputeBackendServicesGetRequest( **backend_service_ref.AsDict())) def GetSetRequest(self, client, backend_service_ref, replacement): if backend_service_ref.Collection() == 'compute.regionBackendServices': return (client.apitools_client.regionBackendServices, 'Update', client.messages.ComputeRegionBackendServicesUpdateRequest( backendServiceResource=replacement, **backend_service_ref.AsDict())) return (client.apitools_client.backendServices, 'Update', client.messages.ComputeBackendServicesUpdateRequest( backendServiceResource=replacement, **backend_service_ref.AsDict()))