1# -*- coding: utf-8 -*- # 2# Copyright 2014 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Command for adding a path matcher to a URL map.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22import collections 23from apitools.base.py import encoding 24 25from googlecloudsdk.api_lib.compute import base_classes 26from googlecloudsdk.calliope import arg_parsers 27from googlecloudsdk.calliope import base 28from googlecloudsdk.calliope import exceptions 29from googlecloudsdk.command_lib.compute import scope as compute_scope 30from googlecloudsdk.command_lib.compute.backend_buckets import ( 31 flags as backend_bucket_flags) 32from googlecloudsdk.command_lib.compute.backend_services import ( 33 flags as backend_service_flags) 34from googlecloudsdk.command_lib.compute.url_maps import flags 35from googlecloudsdk.command_lib.compute.url_maps import url_maps_utils 36from googlecloudsdk.core import properties 37import six 38 39 40def _DetailedHelp(): 41 # pylint:disable=line-too-long 42 return { 43 'brief': 44 'Add a path matcher to a URL map.', 45 'DESCRIPTION': """ 46*{command}* is used to add a path matcher to a URL map. A path 47matcher maps HTTP request paths to backend services or backend 48buckets. Each path matcher must be referenced by at least one 49host rule. This command can create a new host rule through the 50`--new-hosts` flag or it can reconfigure an existing host rule 51to point to the newly added path matcher using `--existing-host`. 52In the latter case, if a path matcher is orphaned as a result 53of the operation, this command will fail unless 54`--delete-orphaned-path-matcher` is provided. Path matcher 55constraints can be found 56[here](https://cloud.google.com/load-balancing/docs/url-map-concepts#pm-constraints). 57""", 58 'EXAMPLES': """ 59To create a rule for mapping the path ```/search/*``` to the 60hypothetical ```search-service```, ```/static/*``` to the 61```static-bucket``` backend bucket and ```/images/*``` to the 62```images-service``` under the hosts ```example.com``` and 63```*.example.com```, run: 64 65 $ {command} MY-URL-MAP --path-matcher-name=MY-MATCHER --default-service=MY-DEFAULT-SERVICE --backend-service-path-rules='/search/*=search_service,/images/*=images-service' --backend-bucket-path-rules='/static/*=static-bucket' --new-hosts=example.com '*.example.com' 66 67Note that a default service or default backend bucket must be 68provided to handle paths for which there is no mapping. 69""", 70 } 71 # pylint:enable=line-too-long 72 73 74def _Args(parser): 75 """Common arguments to add-path-matcher commands for each release track.""" 76 parser.add_argument( 77 '--description', 78 help='An optional, textual description for the path matcher.') 79 80 parser.add_argument( 81 '--path-matcher-name', 82 required=True, 83 help='The name to assign to the path matcher.') 84 85 parser.add_argument( 86 '--path-rules', 87 type=arg_parsers.ArgDict(min_length=1), 88 default={}, 89 metavar='PATH=SERVICE', 90 help='Rules for mapping request paths to services.') 91 92 host_rule = parser.add_mutually_exclusive_group() 93 host_rule.add_argument( 94 '--new-hosts', 95 type=arg_parsers.ArgList(min_length=1), 96 metavar='NEW_HOST', 97 help=('If specified, a new host rule with the given hosts is created ' 98 'and the path matcher is tied to the new host rule.')) 99 100 host_rule.add_argument( 101 '--existing-host', 102 help="""\ 103 An existing host rule to tie the new path matcher to. Although 104 host rules can contain more than one host, only a single host 105 is needed to uniquely identify the host rule. 106 """) 107 108 parser.add_argument( 109 '--delete-orphaned-path-matcher', 110 action='store_true', 111 default=False, 112 help=('If provided and a path matcher is orphaned as a result of this ' 113 'command, the command removes the orphaned path matcher instead ' 114 'of failing.')) 115 116 group = parser.add_mutually_exclusive_group(required=True) 117 group.add_argument( 118 '--default-service', 119 help=('A backend service that will be used for requests that the path ' 120 'matcher cannot match. Exactly one of --default-service or ' 121 '--default-backend-bucket is required.')) 122 group.add_argument( 123 '--default-backend-bucket', 124 help=('A backend bucket that will be used for requests that the path ' 125 'matcher cannot match. Exactly one of --default-service or ' 126 '--default-backend-bucket is required.')) 127 128 parser.add_argument('--backend-service-path-rules', 129 type=arg_parsers.ArgDict(min_length=1), 130 default={}, 131 metavar='PATH=SERVICE', 132 help='Rules for mapping request paths to services.') 133 parser.add_argument( 134 '--backend-bucket-path-rules', 135 type=arg_parsers.ArgDict(min_length=1), 136 default={}, 137 metavar='PATH=BUCKET', 138 help='Rules for mapping request paths to backend buckets.') 139 140 141def _GetGetRequest(client, url_map_ref): 142 """Returns the request for the existing URL map resource.""" 143 return (client.apitools_client.urlMaps, 'Get', 144 client.messages.ComputeUrlMapsGetRequest( 145 urlMap=url_map_ref.Name(), project=url_map_ref.project)) 146 147 148def _GetSetRequest(client, url_map_ref, replacement): 149 return (client.apitools_client.urlMaps, 'Update', 150 client.messages.ComputeUrlMapsUpdateRequest( 151 urlMap=url_map_ref.Name(), 152 urlMapResource=replacement, 153 project=url_map_ref.project)) 154 155 156def _ModifyBase(client, args, existing): 157 """Modifications to the URL map that are shared between release tracks. 158 159 Args: 160 client: The compute client. 161 args: the argparse arguments that this command was invoked with. 162 existing: the existing URL map message. 163 164 Returns: 165 A modified URL map message. 166 """ 167 replacement = encoding.CopyProtoMessage(existing) 168 169 if not args.new_hosts and not args.existing_host: 170 new_hosts = ['*'] 171 else: 172 new_hosts = args.new_hosts 173 174 # If --new-hosts is given, we check to make sure none of those 175 # hosts already exist and once the check succeeds, we create the 176 # new host rule. 177 if new_hosts: 178 new_hosts = set(new_hosts) 179 for host_rule in existing.hostRules: 180 for host in host_rule.hosts: 181 if host in new_hosts: 182 raise exceptions.ToolException( 183 'Cannot create a new host rule with host [{0}] because the ' 184 'host is already part of a host rule that references the path ' 185 'matcher [{1}].'.format(host, host_rule.pathMatcher)) 186 187 replacement.hostRules.append( 188 client.messages.HostRule( 189 hosts=sorted(new_hosts), pathMatcher=args.path_matcher_name)) 190 191 # If --existing-host is given, we check to make sure that the 192 # corresponding host rule will not render a patch matcher 193 # orphan. If the check succeeds, we change the path matcher of the 194 # host rule. If the check fails, we remove the path matcher if 195 # --delete-orphaned-path-matcher is given otherwise we fail. 196 else: 197 target_host_rule = None 198 for host_rule in existing.hostRules: 199 for host in host_rule.hosts: 200 if host == args.existing_host: 201 target_host_rule = host_rule 202 break 203 if target_host_rule: 204 break 205 206 if not target_host_rule: 207 raise exceptions.ToolException( 208 'No host rule with host [{0}] exists. Check your spelling or ' 209 'use [--new-hosts] to create a new host rule.'.format( 210 args.existing_host)) 211 212 path_matcher_orphaned = True 213 for host_rule in replacement.hostRules: 214 if host_rule == target_host_rule: 215 host_rule.pathMatcher = args.path_matcher_name 216 continue 217 218 if host_rule.pathMatcher == target_host_rule.pathMatcher: 219 path_matcher_orphaned = False 220 break 221 222 if path_matcher_orphaned: 223 # A path matcher will be orphaned, so now we determine whether 224 # we should delete the path matcher or report an error. 225 if args.delete_orphaned_path_matcher: 226 replacement.pathMatchers = [ 227 path_matcher for path_matcher in existing.pathMatchers 228 if path_matcher.name != target_host_rule.pathMatcher 229 ] 230 else: 231 raise exceptions.ToolException( 232 'This operation will orphan the path matcher [{0}]. To ' 233 'delete the orphan path matcher, rerun this command with ' 234 '[--delete-orphaned-path-matcher] or use [gcloud compute ' 235 'url-maps edit] to modify the URL map by hand.'.format( 236 host_rule.pathMatcher)) 237 238 return replacement 239 240 241def _Modify(client, resources, args, url_map, url_map_ref, backend_service_arg, 242 backend_bucket_arg): 243 """Returns a modified URL map message.""" 244 replacement = _ModifyBase(client, args, url_map) 245 246 # Creates PathRule objects from --path-rules, --backend-service-path-rules, 247 # and --backend-bucket-path-rules. 248 service_map = collections.defaultdict(set) 249 bucket_map = collections.defaultdict(set) 250 for path, service in six.iteritems(args.path_rules): 251 service_map[service].add(path) 252 for path, service in six.iteritems(args.backend_service_path_rules): 253 service_map[service].add(path) 254 for path, bucket in six.iteritems(args.backend_bucket_path_rules): 255 bucket_map[bucket].add(path) 256 path_rules = [] 257 for service, paths in sorted(six.iteritems(service_map)): 258 path_rules.append( 259 client.messages.PathRule( 260 paths=sorted(paths), 261 service=resources.Parse( 262 service, 263 params=_GetBackendServiceParamsForUrlMap(url_map, url_map_ref), 264 collection=_GetBackendServiceCollectionForUrlMap( 265 url_map)).SelfLink())) 266 for bucket, paths in sorted(six.iteritems(bucket_map)): 267 path_rules.append( 268 client.messages.PathRule( 269 paths=sorted(paths), 270 service=resources.Parse( 271 bucket, 272 params={ 273 'project': properties.VALUES.core.project.GetOrFail 274 }, 275 collection='compute.backendBuckets').SelfLink())) 276 277 if args.default_service: 278 default_backend_uri = url_maps_utils.ResolveUrlMapDefaultService( 279 args, backend_service_arg, url_map_ref, resources).SelfLink() 280 else: 281 default_backend_uri = backend_bucket_arg.ResolveAsResource( 282 args, resources).SelfLink() 283 284 new_path_matcher = client.messages.PathMatcher( 285 defaultService=default_backend_uri, 286 description=args.description, 287 name=args.path_matcher_name, 288 pathRules=path_rules) 289 290 replacement.pathMatchers.append(new_path_matcher) 291 return replacement 292 293 294def _GetRegionalGetRequest(client, url_map_ref): 295 """Returns the request to get an existing regional URL map resource.""" 296 return (client.apitools_client.regionUrlMaps, 'Get', 297 client.messages.ComputeRegionUrlMapsGetRequest( 298 urlMap=url_map_ref.Name(), 299 project=url_map_ref.project, 300 region=url_map_ref.region)) 301 302 303def _GetRegionalSetRequest(client, url_map_ref, replacement): 304 """Returns the request to update an existing regional URL map resource.""" 305 return (client.apitools_client.regionUrlMaps, 'Update', 306 client.messages.ComputeRegionUrlMapsUpdateRequest( 307 urlMap=url_map_ref.Name(), 308 urlMapResource=replacement, 309 project=url_map_ref.project, 310 region=url_map_ref.region)) 311 312 313def _GetBackendServiceParamsForUrlMap(url_map, url_map_ref): 314 params = {'project': properties.VALUES.core.project.GetOrFail} 315 if hasattr(url_map, 'region') and url_map.region: 316 params['region'] = url_map_ref.region 317 318 return params 319 320 321def _GetBackendServiceCollectionForUrlMap(url_map): 322 if hasattr(url_map, 'region') and url_map.region: 323 return 'compute.regionBackendServices' 324 else: 325 return 'compute.backendServices' 326 327 328def _Run(args, holder, url_map_arg, backend_servie_arg, backend_bucket_arg): 329 """Issues requests necessary to add path matcher to the Url Map.""" 330 client = holder.client 331 332 url_map_ref = url_map_arg.ResolveAsResource( 333 args, holder.resources, default_scope=compute_scope.ScopeEnum.GLOBAL) 334 if url_maps_utils.IsRegionalUrlMapRef(url_map_ref): 335 get_request = _GetRegionalGetRequest(client, url_map_ref) 336 else: 337 get_request = _GetGetRequest(client, url_map_ref) 338 339 url_map = client.MakeRequests([get_request])[0] 340 341 modified_url_map = _Modify(client, holder.resources, args, url_map, 342 url_map_ref, backend_servie_arg, 343 backend_bucket_arg) 344 345 if url_maps_utils.IsRegionalUrlMapRef(url_map_ref): 346 set_request = _GetRegionalSetRequest(client, url_map_ref, modified_url_map) 347 else: 348 set_request = _GetSetRequest(client, url_map_ref, modified_url_map) 349 350 return client.MakeRequests([set_request]) 351 352 353@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA, 354 base.ReleaseTrack.GA) 355class AddPathMatcher(base.UpdateCommand): 356 """Add a path matcher to a URL map.""" 357 358 # TODO(b/144022508): Remove _include_l7_internal_load_balancing 359 _include_l7_internal_load_balancing = True 360 361 detailed_help = _DetailedHelp() 362 BACKEND_SERVICE_ARG = None 363 BACKEND_BUCKET_ARG = None 364 URL_MAP_ARG = None 365 366 @classmethod 367 def Args(cls, parser): 368 cls.BACKEND_BUCKET_ARG = ( 369 backend_bucket_flags.BackendBucketArgumentForUrlMap()) 370 cls.BACKEND_SERVICE_ARG = ( 371 backend_service_flags.BackendServiceArgumentForUrlMap( 372 include_l7_internal_load_balancing=cls 373 ._include_l7_internal_load_balancing)) 374 cls.URL_MAP_ARG = flags.UrlMapArgument( 375 include_l7_internal_load_balancing=cls 376 ._include_l7_internal_load_balancing) 377 cls.URL_MAP_ARG.AddArgument(parser) 378 379 _Args(parser) 380 381 def Run(self, args): 382 holder = base_classes.ComputeApiHolder(self.ReleaseTrack()) 383 return _Run(args, holder, self.URL_MAP_ARG, self.BACKEND_SERVICE_ARG, 384 self.BACKEND_BUCKET_ARG) 385