Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(8)

Side by Side Diff: third_party/google-endpoints/google/api/control/service.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 Google Inc. All Rights Reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """service provides funcs for working with ``Service`` instances.
16
17 :func:`extract_report_spec` obtains objects used to determine what metrics,
18 labels and logs are included in a report request.
19
20 :class:`MethodRegistry` obtains a registry of `MethodInfo` instances from the
21 data within a `Service` which can then be used to determine which methods get
22 tracked.
23
24 :class:`Loaders` enumerates the different ways in which to obtain a usable
25 ``Service`` instance
26
27 """
28
29 from __future__ import absolute_import
30
31 import collections
32 import logging
33 import os
34
35
36 from apitools.base.py import encoding
37 from enum import Enum
38
39 from . import label_descriptor, metric_descriptor, messages, path_template
40 from google.api.config import service_config
41
42
43 logger = logging.getLogger(__name__)
44
45
46 CONFIG_VAR = 'ENDPOINTS_SERVICE_CONFIG_FILE'
47
48
49 def _load_from_well_known_env():
50 if CONFIG_VAR not in os.environ:
51 logger.info('did not load service; no environ var %s', CONFIG_VAR)
52 return None
53 config_file = os.environ[CONFIG_VAR]
54 if not os.path.exists(os.environ[CONFIG_VAR]):
55 logger.warn('did not load service; missing config file %s', config_file)
56 return None
57 try:
58 with open(config_file) as f:
59 return encoding.JsonToMessage(messages.Service, f.read())
60 except ValueError:
61 logger.warn('did not load service; bad json config file %s', config_file )
62 return None
63
64
65 _SIMPLE_CONFIG = """
66 {
67 "name": "allow-all",
68 "http": {
69 "rules": [{
70 "selector": "allow-all.GET",
71 "get": "**"
72 }, {
73 "selector": "allow-all.POST",
74 "post": "**"
75 }]
76 },
77 "usage": {
78 "rules": [{
79 "selector" : "allow-all.GET",
80 "allowUnregisteredCalls" : true
81 }, {
82 "selector" : "allow-all.POST",
83 "allowUnregisteredCalls" : true
84 }]
85 }
86 }
87 """
88 _SIMPLE_CORE = encoding.JsonToMessage(messages.Service, _SIMPLE_CONFIG)
89
90
91 def _load_simple():
92 return encoding.CopyProtoMessage(_SIMPLE_CORE)
93
94
95 class Loaders(Enum):
96 """Enumerates the functions used to load service configs."""
97 # pylint: disable=too-few-public-methods
98 ENVIRONMENT = (_load_from_well_known_env,)
99 SIMPLE = (_load_simple,)
100 FROM_SERVICE_MANAGEMENT = (service_config.fetch_service_config,)
101
102 def __init__(self, load_func):
103 """Constructor.
104
105 load_func is used to load a service config
106 """
107 self._load_func = load_func
108
109 def load(self, **kw):
110 return self._load_func(**kw)
111
112
113 class MethodRegistry(object):
114 """Provides a registry of the api methods defined by a ``Service``.
115
116 During construction, ``MethodInfo`` instances are extracted from a
117 ``Service``. The are subsequently accessible via the :func:`lookup` method.
118
119 """
120 # pylint: disable=too-few-public-methods
121 _OPTIONS = 'OPTIONS'
122
123 def __init__(self, service):
124 """Constructor.
125
126 Args:
127 service (:class:`google.api.gen.servicecontrol_v1_messages.Service`):
128 a service instance
129 """
130 if not isinstance(service, messages.Service):
131 raise ValueError('service should be an instance of Service')
132 if not service.name:
133 raise ValueError('Bad service: the name is missing')
134
135 self._service = service # the service that provides the methods
136 self._extracted_methods = {} # tracks all extracted_methods by selector
137
138 self._auth_infos = self._extract_auth_config()
139
140 # tracks urls templates
141 self._templates_method_infos = collections.defaultdict(list)
142 self._extract_methods()
143
144 def lookup(self, http_method, path):
145 http_method = http_method.lower()
146 if path.startswith('/'):
147 path = path[1:]
148 tmi = self._templates_method_infos.get(http_method)
149 if not tmi:
150 logger.debug('No methods for http method %s in %s',
151 http_method,
152 self._templates_method_infos.keys())
153 return None
154
155 # pylint: disable=fixme
156 # TODO: speed this up if it proves to be bottleneck.
157 #
158 # There is sophisticated trie-based solution in esp, something similar
159 # could be built around the path_template implementation
160 for template, method_info in tmi:
161 logger.debug('trying %s with template %s', path, template)
162 try:
163 template.match(path)
164 logger.debug('%s matched template %s', path, template)
165 return method_info
166 except path_template.ValidationException:
167 logger.debug('%s did not match template %s', path, template)
168 continue
169
170 return None
171
172 def _extract_auth_config(self):
173 """Obtains the authentication configurations."""
174
175 service = self._service
176 if not service.authentication:
177 return {}
178
179 auth_infos = {}
180 for auth_rule in service.authentication.rules:
181 selector = auth_rule.selector
182 provider_ids_to_audiences = {}
183 for requirement in auth_rule.requirements:
184 provider_id = requirement.providerId
185 if provider_id and requirement.audiences:
186 audiences = requirement.audiences.split(",")
187 provider_ids_to_audiences[provider_id] = audiences
188 auth_infos[selector] = AuthInfo(provider_ids_to_audiences)
189 return auth_infos
190
191 def _extract_methods(self):
192 """Obtains the methods used in the service."""
193 service = self._service
194 all_urls = set()
195 urls_with_options = set()
196 if not service.http:
197 return
198 for rule in service.http.rules:
199 http_method, url = _detect_pattern_option(rule)
200 if not url or not http_method or not rule.selector:
201 logger.error('invalid HTTP binding encountered')
202 continue
203
204 # Obtain the method info
205 method_info = self._get_or_create_method_info(rule.selector)
206 if rule.body:
207 method_info.body_field_path = rule.body
208 if not self._register(http_method, url, method_info):
209 continue # detected an invalid url
210 all_urls.add(url)
211
212 if http_method == self._OPTIONS:
213 urls_with_options.add(url)
214
215 self._add_cors_options_selectors(all_urls - urls_with_options)
216 self._update_usage()
217 self._update_system_parameters()
218
219 def _register(self, http_method, url, method_info):
220 try:
221 http_method = http_method.lower()
222 template = path_template.PathTemplate(url)
223 self._templates_method_infos[http_method].append((template, method_i nfo))
224 logger.debug('Registered template %s under method %s',
225 template,
226 http_method)
227 return True
228 except path_template.ValidationException:
229 logger.error('invalid HTTP template provided: %s', url)
230 return False
231
232 def _update_usage(self):
233 extracted_methods = self._extracted_methods
234 service = self._service
235 if not service.usage:
236 return
237 for rule in service.usage.rules:
238 selector = rule.selector
239 method = extracted_methods.get(selector)
240 if method:
241 method.allow_unregistered_calls = rule.allowUnregisteredCalls
242 else:
243 logger.error('bad usage selector: No HTTP rule for %s', selector )
244
245 def _get_or_create_method_info(self, selector):
246 extracted_methods = self._extracted_methods
247 info = self._extracted_methods.get(selector)
248 if info:
249 return info
250
251 auth_infos = self._auth_infos
252 auth_info = auth_infos[selector] if selector in auth_infos else None
253
254 info = MethodInfo(selector, auth_info)
255 extracted_methods[selector] = info
256 return info
257
258 def _add_cors_options_selectors(self, urls):
259 extracted_methods = self._extracted_methods
260 base_selector = '%s.%s' % (self._service.name, self._OPTIONS)
261
262 # ensure that no existing options selector is being used
263 options_selector = base_selector
264 n = 0
265 while extracted_methods.get(options_selector) is not None:
266 n += 1
267 options_selector = '%s.%d' % (base_selector, n)
268 method_info = self._get_or_create_method_info(options_selector)
269 method_info.allow_unregistered_calls = True
270 for u in urls:
271 self._register(self._OPTIONS, u, method_info)
272
273 def _update_system_parameters(self):
274 extracted_methods = self._extracted_methods
275 service = self._service
276 if not service.systemParameters:
277 return
278 rules = service.systemParameters.rules
279 for rule in rules:
280 selector = rule.selector
281 method = extracted_methods.get(selector)
282 if not method:
283 logger.error('bad system parameter: No HTTP rule for %s',
284 selector)
285 continue
286
287 for parameter in rule.parameters:
288 name = parameter.name
289 if not name:
290 logger.error('bad system parameter: no parameter name %s',
291 selector)
292 continue
293
294 if parameter.httpHeader:
295 method.add_header_param(name, parameter.httpHeader)
296 if parameter.urlQueryParameter:
297 method.add_url_query_param(name, parameter.urlQueryParameter )
298
299
300 class AuthInfo(object):
301 """Consolidates auth information about methods defined in a ``Service``."""
302
303 def __init__(self, provider_ids_to_audiences):
304 """Construct an AuthInfo instance.
305
306 Args:
307 provider_ids_to_audiences: a dictionary that maps from provider ids
308 to allowed audiences.
309 """
310 self._provider_ids_to_audiences = provider_ids_to_audiences
311
312 def is_provider_allowed(self, provider_id):
313 return provider_id in self._provider_ids_to_audiences
314
315 def get_allowed_audiences(self, provider_id):
316 return self._provider_ids_to_audiences.get(provider_id, [])
317
318
319 class MethodInfo(object):
320 """Consolidates information about methods defined in a ``Service``."""
321 API_KEY_NAME = 'api_key'
322 # pylint: disable=too-many-instance-attributes
323
324 def __init__(self, selector, auth_info):
325 self.selector = selector
326 self.auth_info = auth_info
327 self.allow_unregistered_calls = False
328 self.backend_address = ''
329 self.body_field_path = ''
330 self._url_query_parameters = collections.defaultdict(list)
331 self._header_parameters = collections.defaultdict(list)
332
333 def add_url_query_param(self, name, parameter):
334 self._url_query_parameters[name].append(parameter)
335
336 def add_header_param(self, name, parameter):
337 self._header_parameters[name].append(parameter)
338
339 def url_query_param(self, name):
340 return tuple(self._url_query_parameters[name])
341
342 def header_param(self, name):
343 return tuple(self._header_parameters[name])
344
345 @property
346 def api_key_http_header(self):
347 return self.header_param(self.API_KEY_NAME)
348
349 @property
350 def api_key_url_query_params(self):
351 return self.url_query_param(self.API_KEY_NAME)
352
353
354 def extract_report_spec(
355 service,
356 label_is_supported=label_descriptor.KnownLabels.is_supported,
357 metric_is_supported=metric_descriptor.KnownMetrics.is_supported):
358 """Obtains the used logs, metrics and labels from a service.
359
360 label_is_supported and metric_is_supported are filter functions used to
361 determine if label_descriptors or metric_descriptors found in the service
362 are supported.
363
364 Args:
365 service (:class:`google.api.gen.servicecontrol_v1_messages.Service`):
366 a service instance
367 label_is_supported (:func): determines if a given label is supported
368 metric_is_supported (:func): determines if a given metric is supported
369
370 Return:
371 tuple: (
372 logs (set[string}), # the logs to report to
373 metrics (list[string]), # the metrics to use
374 labels (list[string]) # the labels to add
375 )
376 """
377 resource_descs = service.monitoredResources
378 labels_dict = {}
379 logs = set()
380 if service.logging:
381 logs = _add_logging_destinations(
382 service.logging.producerDestinations,
383 resource_descs,
384 service.logs,
385 labels_dict,
386 label_is_supported
387 )
388 metrics_dict = {}
389 monitoring = service.monitoring
390 if monitoring:
391 for destinations in (monitoring.consumerDestinations,
392 monitoring.producerDestinations):
393 _add_monitoring_destinations(destinations,
394 resource_descs,
395 service.metrics,
396 metrics_dict,
397 metric_is_supported,
398 labels_dict,
399 label_is_supported)
400 return logs, metrics_dict.keys(), labels_dict.keys()
401
402
403 def _add_logging_destinations(destinations,
404 resource_descs,
405 log_descs,
406 labels_dict,
407 is_supported):
408 all_logs = set()
409 for d in destinations:
410 if not _add_labels_for_a_monitored_resource(resource_descs,
411 d.monitoredResource,
412 labels_dict,
413 is_supported):
414 continue # skip bad monitored resources
415 for log in d.logs:
416 if _add_labels_for_a_log(log_descs, log, labels_dict, is_supported):
417 all_logs.add(log) # only add correctly configured logs
418 return all_logs
419
420
421 def _add_monitoring_destinations(destinations,
422 resource_descs,
423 metric_descs,
424 metrics_dict,
425 metric_is_supported,
426 labels_dict,
427 label_is_supported):
428 # pylint: disable=too-many-arguments
429 for d in destinations:
430 if not _add_labels_for_a_monitored_resource(resource_descs,
431 d.monitoredResource,
432 labels_dict,
433 label_is_supported):
434 continue # skip bad monitored resources
435 for metric_name in d.metrics:
436 metric_desc = _find_metric_descriptor(metric_descs, metric_name,
437 metric_is_supported)
438 if not metric_desc:
439 continue # skip unrecognized or unsupported metric
440 if not _add_labels_from_descriptors(metric_desc.labels, labels_dict,
441 label_is_supported):
442 continue # skip metrics with bad labels
443 metrics_dict[metric_name] = metric_desc
444
445
446 def _add_labels_from_descriptors(descs, labels_dict, is_supported):
447 # only add labels if there are no conflicts
448 for desc in descs:
449 existing = labels_dict.get(desc.key)
450 if existing and existing.valueType != desc.valueType:
451 logger.warn('halted label scan: conflicting label in %s', desc.key)
452 return False
453 # Update labels_dict
454 for desc in descs:
455 if is_supported(desc):
456 labels_dict[desc.key] = desc
457 return True
458
459
460 def _add_labels_for_a_log(logging_descs, log_name, labels_dict, is_supported):
461 for d in logging_descs:
462 if d.name == log_name:
463 _add_labels_from_descriptors(d.labels, labels_dict, is_supported)
464 return True
465 logger.warn('bad log label scan: log not found %s', log_name)
466 return False
467
468
469 def _add_labels_for_a_monitored_resource(resource_descs,
470 resource_name,
471 labels_dict,
472 is_supported):
473 for d in resource_descs:
474 if d.type == resource_name:
475 _add_labels_from_descriptors(d.labels, labels_dict, is_supported)
476 return True
477 logger.warn('bad monitored resource label scan: resource not found %s',
478 resource_name)
479 return False
480
481
482 def _find_metric_descriptor(metric_descs, name, metric_is_supported):
483 for d in metric_descs:
484 if name != d.name:
485 continue
486 if metric_is_supported(d):
487 return d
488 else:
489 return None
490 return None
491
492
493 # This is derived from the oneof choices of the HttpRule message's pattern
494 # field in google/api/http.proto, and should be kept in sync with that
495 _HTTP_RULE_ONE_OF_FIELDS = (
496 'get', 'put', 'post', 'delete', 'patch', 'custom')
497
498
499 def _detect_pattern_option(http_rule):
500 for f in _HTTP_RULE_ONE_OF_FIELDS:
501 value = http_rule.get_assigned_value(f)
502 if value is not None:
503 if f == 'custom':
504 return value.kind, value.path
505 else:
506 return f, value
507 return None, None
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698