# -*- 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 URL maps.""" 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 from googlecloudsdk.command_lib.compute import exceptions as compute_exceptions from googlecloudsdk.command_lib.compute import scope as compute_scope from googlecloudsdk.command_lib.compute.url_maps import flags from googlecloudsdk.command_lib.compute.url_maps import url_maps_utils 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 def _DetailedHelp(): return { 'brief': 'Modify URL maps', 'DESCRIPTION': """\ *{command}* can be used to modify a URL map. The URL map resource is fetched from the server and presented in a text editor. After the file is saved and closed, this command will update the resource. Only fields that can be modified are displayed in the editor. The editor used to modify the resource is chosen by inspecting the ``EDITOR'' environment variable. """, } def _ProcessEditedResource( holder, url_map_ref, file_contents, original_object, original_record, modifiable_record, args, enable_regional_backend_buckets=False, ): """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 ) reference_normalizer = property_selector.PropertySelector( transformations=_GetReferenceNormalizers( holder.resources, enable_regional_backend_buckets ) ) 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.UrlMap ) # 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( [_GetSetRequest(holder.client, url_map_ref, new_object)] ) def _EditResource( args, client, holder, original_object, url_map_ref, track, enable_regional_backend_buckets=False, ): """Allows user to edit the URL Map.""" original_record = encoding.MessageToDict(original_object) # Selects only the fields that can be modified. field_selector = property_selector.PropertySelector( properties=[ 'defaultService', 'description', 'hostRules', 'pathMatchers', 'tests', 'defaultCustomErrorResponsePolicy', ] ) modifiable_record = field_selector.Apply(original_record) buf = _BuildFileContents( args, client, modifiable_record, original_record, track ) file_contents = buf.getvalue() while True: try: file_contents = edit.OnlineEdit(file_contents) except edit.NoSaveException: raise compute_exceptions.AbortedError('Edit aborted by user.') try: enable_regional_backend_buckets = track in ['alpha'] resource_list = _ProcessEditedResource( holder, url_map_ref, file_contents, original_object, original_record, modifiable_record, args, enable_regional_backend_buckets, ) break except ( ValueError, yaml.YAMLParseError, messages.ValidationError, exceptions.ToolException, ) as e: message = getattr(e, 'message', six.text_type(e)) if isinstance(e, 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 compute_exceptions.AbortedError('Edit aborted by user.') return resource_list def _BuildFileContents(args, client, modifiable_record, original_record, track): """Builds the initial editable file.""" 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(_GetExampleResource(client, track)), 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 def _GetExampleResource(client, track): """Gets an example URL Map.""" backend_service_uri_prefix = ( 'https://compute.googleapis.com/compute/%(track)s/projects/' 'my-project/global/backendServices/' % {'track': track} ) backend_bucket_uri_prefix = ( 'https://compute.googleapis.com/compute/%(track)s/projects/' 'my-project/global/backendBuckets/' % {'track': track} ) return client.messages.UrlMap( name='site-map', defaultService=backend_service_uri_prefix + 'default-service', defaultCustomErrorResponsePolicy=client.messages.CustomErrorResponsePolicy( errorService=backend_service_uri_prefix + 'error-service', errorResponseRules=[ client.messages.CustomErrorResponsePolicyCustomErrorResponseRule( matchResponseCodes=['4xx'], path='/errors/4xx/not-found.html', overrideResponseCode=404, ), ], ), hostRules=[ client.messages.HostRule( hosts=['*.google.com', 'google.com'], pathMatcher='www' ), client.messages.HostRule( hosts=['*.youtube.com', 'youtube.com', '*-youtube.com'], pathMatcher='youtube', ), ], pathMatchers=[ client.messages.PathMatcher( name='www', defaultService=backend_service_uri_prefix + 'www-default', pathRules=[ client.messages.PathRule( paths=['/search', '/search/*'], service=backend_service_uri_prefix + 'search', ), client.messages.PathRule( paths=['/search/ads', '/search/ads/*'], service=backend_service_uri_prefix + 'ads', ), client.messages.PathRule( paths=['/images/*'], service=backend_bucket_uri_prefix + 'images', ), ], ), client.messages.PathMatcher( name='youtube', defaultService=backend_service_uri_prefix + 'youtube-default', pathRules=[ client.messages.PathRule( paths=['/search', '/search/*'], service=backend_service_uri_prefix + 'youtube-search', ), client.messages.PathRule( paths=['/watch', '/view', '/preview'], service=backend_service_uri_prefix + 'youtube-watch', ), ], ), ], tests=[ client.messages.UrlMapTest( host='www.google.com', path='/search/ads/inline?q=flowers', service=backend_service_uri_prefix + 'ads', ), client.messages.UrlMapTest( host='youtube.com', path='/watch/this', service=backend_service_uri_prefix + 'youtube-default', ), client.messages.UrlMapTest( host='youtube.com', path='/images/logo.png', service=backend_bucket_uri_prefix + 'images', ), ], ) def _GetReferenceNormalizers( resource_registry, enable_regional_backend_buckets=False ): """Gets normalizers that translate short names to URIs.""" 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 if enable_regional_backend_buckets: allowed_collections = [ 'compute.backendServices', 'compute.backendBuckets', 'compute.regionBackendServices', 'compute.regionBackendBuckets', ] else: allowed_collections = [ 'compute.backendServices', 'compute.backendBuckets', 'compute.regionBackendServices', ] return [ ( 'defaultService', MakeReferenceNormalizer('defaultService', allowed_collections), ), ( 'pathMatchers[].defaultService', MakeReferenceNormalizer('defaultService', allowed_collections), ), ( 'pathMatchers[].pathRules[].service', MakeReferenceNormalizer('service', allowed_collections), ), ( 'CustomErrorResponsePolicy.errorService', MakeReferenceNormalizer('errorService', allowed_collections), ), ( 'tests[].service', MakeReferenceNormalizer('service', allowed_collections), ), ] def _GetGetRequest(client, url_map_ref): """Gets the request for getting the URL Map.""" if url_maps_utils.IsRegionalUrlMapRef(url_map_ref): return ( client.apitools_client.regionUrlMaps, 'Get', client.messages.ComputeRegionUrlMapsGetRequest( urlMap=url_map_ref.Name(), project=url_map_ref.project, region=url_map_ref.region, ), ) return ( client.apitools_client.urlMaps, 'Get', client.messages.ComputeUrlMapsGetRequest(**url_map_ref.AsDict()), ) def _GetSetRequest(client, url_map_ref, replacement): """Gets the request for setting the URL Map.""" if url_maps_utils.IsRegionalUrlMapRef(url_map_ref): return ( client.apitools_client.regionUrlMaps, 'Update', client.messages.ComputeRegionUrlMapsUpdateRequest( urlMap=url_map_ref.Name(), urlMapResource=replacement, project=url_map_ref.project, region=url_map_ref.region, ), ) return ( client.apitools_client.urlMaps, 'Update', client.messages.ComputeUrlMapsUpdateRequest( urlMapResource=replacement, **url_map_ref.AsDict() ), ) def _Run(args, holder, track, url_map_arg): """Issues requests necessary to edit URL maps.""" client = holder.client url_map_ref = url_map_arg.ResolveAsResource( args, holder.resources, default_scope=compute_scope.ScopeEnum.GLOBAL ) get_request = _GetGetRequest(client, url_map_ref) objects = client.MakeRequests([get_request]) resource_list = _EditResource( args, client, holder, objects[0], url_map_ref, track ) for resource in resource_list: yield resource class InvalidResourceError(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 @base.ReleaseTracks(base.ReleaseTrack.GA) @base.UniverseCompatible class Edit(base.Command): """Modify URL maps.""" detailed_help = _DetailedHelp() DEFAULT_FORMAT = 'yaml' URL_MAP_ARG = None TRACK = 'v1' @classmethod def Args(cls, parser): cls.URL_MAP_ARG = flags.UrlMapArgument() cls.URL_MAP_ARG.AddArgument(parser) def Run(self, args): holder = base_classes.ComputeApiHolder(self.ReleaseTrack()) return _Run(args, holder, self.TRACK, self.URL_MAP_ARG) @base.ReleaseTracks(base.ReleaseTrack.BETA) class EditBeta(Edit): TRACK = 'beta' @base.ReleaseTracks(base.ReleaseTrack.ALPHA) class EditAlpha(EditBeta): TRACK = 'alpha'