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

Side by Side Diff: appengine/auth_service/config.py

Issue 2840053003: auth_service: Fetch revisions of all configs (for UI) at once. (Closed)
Patch Set: comments Created 3 years, 7 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
« no previous file with comments | « no previous file | appengine/auth_service/config_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2015 The LUCI Authors. All rights reserved. 1 # Copyright 2015 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 """Adapter between config service client and the rest of auth_service. 5 """Adapter between config service client and the rest of auth_service.
6 6
7 Basically a cron job that each minute refetches config files from config service 7 Basically a cron job that each minute refetches config files from config service
8 and modifies auth service datastore state if anything changed. 8 and modifies auth service datastore state if anything changed.
9 9
10 Following files are fetched: 10 Following files are fetched:
(...skipping 16 matching lines...) Expand all
27 from google import protobuf 27 from google import protobuf
28 from google.appengine.ext import ndb 28 from google.appengine.ext import ndb
29 29
30 from components import config 30 from components import config
31 from components import datastore_utils 31 from components import datastore_utils
32 from components import gitiles 32 from components import gitiles
33 from components import utils 33 from components import utils
34 from components.auth import ipaddr 34 from components.auth import ipaddr
35 from components.auth import model 35 from components.auth import model
36 from components.config import validation 36 from components.config import validation
37 from components.config import validation_context
38 37
39 from proto import config_pb2 38 from proto import config_pb2
40 import importer 39 import importer
41 40
42 41
43 # Config file revision number and where it came from. 42 # Config file revision number and where it came from.
44 Revision = collections.namedtuple('Revision', ['revision', 'url']) 43 Revision = collections.namedtuple('Revision', ['revision', 'url'])
45 44
46 45
47 class CannotLoadConfigError(Exception): 46 class CannotLoadConfigError(Exception):
(...skipping 13 matching lines...) Expand all
61 60
62 61
63 def get_remote_url(): 62 def get_remote_url():
64 """Returns URL of a config service if configured, to display in UI.""" 63 """Returns URL of a config service if configured, to display in UI."""
65 settings = config.ConfigSettings.cached() 64 settings = config.ConfigSettings.cached()
66 if settings and settings.service_hostname: 65 if settings and settings.service_hostname:
67 return 'https://%s' % settings.service_hostname 66 return 'https://%s' % settings.service_hostname
68 return None 67 return None
69 68
70 69
71 def get_config_revision(path): 70 def get_revisions():
71 """Returns a mapping {config file name => Revision instance or None}."""
72 futures = {p: _get_config_revision_async(p) for p in _CONFIG_SCHEMAS}
73 return {p: f.get_result() for p, f in futures.iteritems()}
74
75
76 def _get_config_revision_async(path):
72 """Returns tuple with info about last imported config revision.""" 77 """Returns tuple with info about last imported config revision."""
78 assert path in _CONFIG_SCHEMAS, path
73 schema = _CONFIG_SCHEMAS.get(path) 79 schema = _CONFIG_SCHEMAS.get(path)
74 return schema['revision_getter']() if schema else None 80 return schema['revision_getter']()
75 81
76 82
77 @utils.cache_with_expiration(expiration_sec=60) 83 @utils.cache_with_expiration(expiration_sec=60)
78 def get_settings(): 84 def get_settings():
79 """Returns auth service own settings (from settings.cfg) as SettingsCfg proto. 85 """Returns auth service own settings (from settings.cfg) as SettingsCfg proto.
80 86
81 Returns default settings if the ones in the datastore are no longer valid. 87 Returns default settings if the ones in the datastore are no longer valid.
82 """ 88 """
83 text = _get_service_config('settings.cfg') 89 text = _get_service_config('settings.cfg')
84 if not text: 90 if not text:
(...skipping 23 matching lines...) Expand all
108 configs = _fetch_configs(_CONFIG_SCHEMAS) 114 configs = _fetch_configs(_CONFIG_SCHEMAS)
109 except CannotLoadConfigError as exc: 115 except CannotLoadConfigError as exc:
110 logging.error('Failed to fetch configs\n%s', exc) 116 logging.error('Failed to fetch configs\n%s', exc)
111 return 117 return
112 118
113 # Figure out what needs to be updated. 119 # Figure out what needs to be updated.
114 dirty = {} 120 dirty = {}
115 dirty_in_authdb = {} 121 dirty_in_authdb = {}
116 for path, (new_rev, conf) in sorted(configs.iteritems()): 122 for path, (new_rev, conf) in sorted(configs.iteritems()):
117 assert path in _CONFIG_SCHEMAS, path 123 assert path in _CONFIG_SCHEMAS, path
118 cur_rev = get_config_revision(path) 124 cur_rev = _get_config_revision_async(path).get_result()
119 if cur_rev != new_rev or force: 125 if cur_rev != new_rev or force:
120 if _CONFIG_SCHEMAS[path]['use_authdb_transaction']: 126 if _CONFIG_SCHEMAS[path]['use_authdb_transaction']:
121 dirty_in_authdb[path] = (new_rev, conf) 127 dirty_in_authdb[path] = (new_rev, conf)
122 else: 128 else:
123 dirty[path] = (new_rev, conf) 129 dirty[path] = (new_rev, conf)
124 else: 130 else:
125 logging.info('Config %s is up-to-date at rev %s', path, cur_rev.revision) 131 logging.info('Config %s is up-to-date at rev %s', path, cur_rev.revision)
126 132
127 # First update configs that do not touch AuthDB, one by one. 133 # First update configs that do not touch AuthDB, one by one.
128 for path, (rev, conf) in sorted(dirty.iteritems()): 134 for path, (rev, conf) in sorted(dirty.iteritems()):
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
186 Root entity. Key ID is config file name. 192 Root entity. Key ID is config file name.
187 """ 193 """
188 # The body of the config itself. 194 # The body of the config itself.
189 config = ndb.TextProperty() 195 config = ndb.TextProperty()
190 # Last imported SHA1 revision of the config. 196 # Last imported SHA1 revision of the config.
191 revision = ndb.StringProperty(indexed=False) 197 revision = ndb.StringProperty(indexed=False)
192 # URL the config was imported from. 198 # URL the config was imported from.
193 url = ndb.StringProperty(indexed=False) 199 url = ndb.StringProperty(indexed=False)
194 200
195 201
196 def _get_service_config_rev(cfg_name): 202 @ndb.tasklet
203 def _get_service_config_rev_async(cfg_name):
197 """Returns last processed Revision of given config.""" 204 """Returns last processed Revision of given config."""
198 e = _AuthServiceConfig.get_by_id(cfg_name) 205 e = yield _AuthServiceConfig.get_by_id_async(cfg_name)
199 return Revision(e.revision, e.url) if e else None 206 raise ndb.Return(Revision(e.revision, e.url) if e else None)
200 207
201 208
202 def _get_service_config(cfg_name): 209 def _get_service_config(cfg_name):
203 """Returns text of given config file or None if missing.""" 210 """Returns text of given config file or None if missing."""
204 e = _AuthServiceConfig.get_by_id(cfg_name) 211 e = _AuthServiceConfig.get_by_id(cfg_name)
205 return e.config if e else None 212 return e.config if e else None
206 213
207 214
208 @ndb.transactional 215 @ndb.transactional
209 def _update_service_config(cfg_name, rev, conf): 216 def _update_service_config(cfg_name, rev, conf):
210 """Stores new config (and its revision). 217 """Stores new config (and its revision).
211 218
212 This function is called only if config has already been validated. 219 This function is called only if config has already been validated.
213 """ 220 """
214 assert isinstance(conf, basestring) 221 assert isinstance(conf, basestring)
215 e = _AuthServiceConfig.get_by_id(cfg_name) or _AuthServiceConfig(id=cfg_name) 222 e = _AuthServiceConfig.get_by_id(cfg_name) or _AuthServiceConfig(id=cfg_name)
216 old = e.config 223 old = e.config
217 e.populate(config=conf, revision=rev.revision, url=rev.url) 224 e.populate(config=conf, revision=rev.revision, url=rev.url)
218 e.put() 225 e.put()
219 return old != conf 226 return old != conf
220 227
221 228
222 ### Group importer config implementation details. 229 ### Group importer config implementation details.
223 230
224 231
225 def _get_imports_config_revision(): 232 @ndb.tasklet
233 def _get_imports_config_revision_async():
226 """Returns Revision of last processed imports.cfg config.""" 234 """Returns Revision of last processed imports.cfg config."""
227 e = importer.config_key().get() 235 e = yield importer.config_key().get_async()
228 if not e or not isinstance(e.config_revision, dict): 236 if not e or not isinstance(e.config_revision, dict):
229 return None 237 raise ndb.Return(None)
230 desc = e.config_revision 238 desc = e.config_revision
231 return Revision(desc.get('rev'), desc.get('url')) 239 raise ndb.Return(Revision(desc.get('rev'), desc.get('url')))
232 240
233 241
234 def _update_imports_config(rev, conf): 242 def _update_imports_config(rev, conf):
235 """Applies imports.cfg config.""" 243 """Applies imports.cfg config."""
236 # Rewrite existing config even if it is the same (to update 'rev'). 244 # Rewrite existing config even if it is the same (to update 'rev').
237 cur = importer.read_config() 245 cur = importer.read_config()
238 importer.write_config(conf, {'rev': rev.revision, 'url': rev.url}) 246 importer.write_config(conf, {'rev': rev.revision, 'url': rev.url})
239 return cur != conf 247 return cur != conf
240 248
241 249
242 ### Implementation of configs expanded to AuthDB entities. 250 ### Implementation of configs expanded to AuthDB entities.
243 251
244 252
245 class _ImportedConfigRevisions(ndb.Model): 253 class _ImportedConfigRevisions(ndb.Model):
246 """Stores mapping config path -> {'rev': SHA1, 'url': URL}. 254 """Stores mapping config path -> {'rev': SHA1, 'url': URL}.
247 255
248 Parent entity is AuthDB root (auth.model.root_key()). Updated in a transaction 256 Parent entity is AuthDB root (auth.model.root_key()). Updated in a transaction
249 when importing configs. 257 when importing configs.
250 """ 258 """
251 revisions = ndb.JsonProperty() 259 revisions = ndb.JsonProperty()
252 260
253 261
254 def _imported_config_revisions_key(): 262 def _imported_config_revisions_key():
255 return ndb.Key(_ImportedConfigRevisions, 'self', parent=model.root_key()) 263 return ndb.Key(_ImportedConfigRevisions, 'self', parent=model.root_key())
256 264
257 265
258 def _get_authdb_config_rev(path): 266 @ndb.tasklet
267 def _get_authdb_config_rev_async(path):
259 """Returns Revision of last processed config given its name.""" 268 """Returns Revision of last processed config given its name."""
260 mapping = _imported_config_revisions_key().get() 269 mapping = yield _imported_config_revisions_key().get_async()
261 if not mapping or not isinstance(mapping.revisions, dict): 270 if not mapping or not isinstance(mapping.revisions, dict):
262 return None 271 raise ndb.Return(None)
263 desc = mapping.revisions.get(path) 272 desc = mapping.revisions.get(path)
264 if not isinstance(desc, dict): 273 if not isinstance(desc, dict):
265 return None 274 raise ndb.Return(None)
266 return Revision(desc.get('rev'), desc.get('url')) 275 raise ndb.Return(Revision(desc.get('rev'), desc.get('url')))
267 276
268 277
269 @datastore_utils.transactional 278 @datastore_utils.transactional
270 def _update_authdb_configs(configs): 279 def _update_authdb_configs(configs):
271 """Pushes new configs to AuthDB entity group. 280 """Pushes new configs to AuthDB entity group.
272 281
273 Args: 282 Args:
274 configs: dict {config path -> (Revision tuple, <config>)}. 283 configs: dict {config path -> (Revision tuple, <config>)}.
275 """ 284 """
276 revs = _imported_config_revisions_key().get() 285 revs = _imported_config_revisions_key().get()
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
463 modified_ts=utils.utcnow(), 472 modified_ts=utils.utcnow(),
464 comment='Importing oauth.cfg at rev %s' % rev.revision) 473 comment='Importing oauth.cfg at rev %s' % rev.revision)
465 existing.put() 474 existing.put()
466 return True 475 return True
467 476
468 477
469 ### Description of all known config files: how to validate and import them. 478 ### Description of all known config files: how to validate and import them.
470 479
471 # Config file name -> { 480 # Config file name -> {
472 # 'proto_class': protobuf class of the config or None to keep it as text, 481 # 'proto_class': protobuf class of the config or None to keep it as text,
473 # 'revision_getter': lambda: <latest imported Revision>, 482 # 'revision_getter': lambda: ndb.Future with <latest imported Revision>
474 # 'validator': lambda config: <raises ValueError on invalid format> 483 # 'validator': lambda config: <raises ValueError on invalid format>
475 # 'updater': lambda rev, config: True if applied, False if not. 484 # 'updater': lambda rev, config: True if applied, False if not.
476 # 'use_authdb_transaction': True to call 'updater' in AuthDB transaction. 485 # 'use_authdb_transaction': True to call 'updater' in AuthDB transaction.
477 # 'default': Default config value to use if the config file is missing. 486 # 'default': Default config value to use if the config file is missing.
478 # } 487 # }
479 _CONFIG_SCHEMAS = { 488 _CONFIG_SCHEMAS = {
480 'imports.cfg': { 489 'imports.cfg': {
481 'proto_class': None, # importer configs are stored as text 490 'proto_class': None, # importer configs are stored as text
482 'revision_getter': _get_imports_config_revision, 491 'revision_getter': _get_imports_config_revision_async,
483 'updater': _update_imports_config, 492 'updater': _update_imports_config,
484 'use_authdb_transaction': False, 493 'use_authdb_transaction': False,
485 }, 494 },
486 'ip_whitelist.cfg': { 495 'ip_whitelist.cfg': {
487 'proto_class': config_pb2.IPWhitelistConfig, 496 'proto_class': config_pb2.IPWhitelistConfig,
488 'revision_getter': lambda: _get_authdb_config_rev('ip_whitelist.cfg'), 497 'revision_getter': lambda: _get_authdb_config_rev_async('ip_whitelist.cfg'),
489 'updater': _update_ip_whitelist_config, 498 'updater': _update_ip_whitelist_config,
490 'use_authdb_transaction': True, 499 'use_authdb_transaction': True,
491 }, 500 },
492 'oauth.cfg': { 501 'oauth.cfg': {
493 'proto_class': config_pb2.OAuthConfig, 502 'proto_class': config_pb2.OAuthConfig,
494 'revision_getter': lambda: _get_authdb_config_rev('oauth.cfg'), 503 'revision_getter': lambda: _get_authdb_config_rev_async('oauth.cfg'),
495 'updater': _update_oauth_config, 504 'updater': _update_oauth_config,
496 'use_authdb_transaction': True, 505 'use_authdb_transaction': True,
497 }, 506 },
498 'settings.cfg': { 507 'settings.cfg': {
499 'proto_class': None, # settings are stored as text in datastore 508 'proto_class': None, # settings are stored as text in datastore
500 'default': '', # it's fine if config file is not there 509 'default': '', # it's fine if config file is not there
501 'revision_getter': lambda: _get_service_config_rev('settings.cfg'), 510 'revision_getter': lambda: _get_service_config_rev_async('settings.cfg'),
502 'updater': lambda rev, c: _update_service_config('settings.cfg', rev, c), 511 'updater': lambda rev, c: _update_service_config('settings.cfg', rev, c),
503 'use_authdb_transaction': False, 512 'use_authdb_transaction': False,
504 }, 513 },
505 } 514 }
506 515
507 516
508 @utils.memcache('auth_service:get_configs_url', time=300) 517 @utils.memcache('auth_service:get_configs_url', time=300)
509 def _get_configs_url(): 518 def _get_configs_url():
510 """Returns URL where luci-config fetches configs from.""" 519 """Returns URL where luci-config fetches configs from."""
511 url = config.get_config_set_location(config.self_config_set()) 520 url = config.get_config_set_location(config.self_config_set())
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
552 try: 561 try:
553 location = gitiles.Location.parse(configs_url) 562 location = gitiles.Location.parse(configs_url)
554 return str(gitiles.Location( 563 return str(gitiles.Location(
555 hostname=location.hostname, 564 hostname=location.hostname,
556 project=location.project, 565 project=location.project,
557 treeish=rev, 566 treeish=rev,
558 path=posixpath.join(location.path, path))) 567 path=posixpath.join(location.path, path)))
559 except ValueError: 568 except ValueError:
560 # Not a gitiles URL, return as is. 569 # Not a gitiles URL, return as is.
561 return configs_url 570 return configs_url
OLDNEW
« no previous file with comments | « no previous file | appengine/auth_service/config_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698