auth_service: Fetch revisions of all configs (for UI) at once.
Also, do it in parallel. This will simplify the switch to client-side Single
Page Application by removing the need for per-tab callback (info for all tabs
will be fetched at once).
R=nodir@chromium.org
BUG=714821
Review-Url: https://codereview.chromium.org/2840053003
diff --git a/appengine/auth_service/config.py b/appengine/auth_service/config.py
index d387f20..f07b352 100644
--- a/appengine/auth_service/config.py
+++ b/appengine/auth_service/config.py
@@ -34,7 +34,6 @@
from components.auth import ipaddr
from components.auth import model
from components.config import validation
-from components.config import validation_context
from proto import config_pb2
import importer
@@ -68,10 +67,17 @@
return None
-def get_config_revision(path):
+def get_revisions():
+ """Returns a mapping {config file name => Revision instance or None}."""
+ futures = {p: _get_config_revision_async(p) for p in _CONFIG_SCHEMAS}
+ return {p: f.get_result() for p, f in futures.iteritems()}
+
+
+def _get_config_revision_async(path):
"""Returns tuple with info about last imported config revision."""
+ assert path in _CONFIG_SCHEMAS, path
schema = _CONFIG_SCHEMAS.get(path)
- return schema['revision_getter']() if schema else None
+ return schema['revision_getter']()
@utils.cache_with_expiration(expiration_sec=60)
@@ -115,7 +121,7 @@
dirty_in_authdb = {}
for path, (new_rev, conf) in sorted(configs.iteritems()):
assert path in _CONFIG_SCHEMAS, path
- cur_rev = get_config_revision(path)
+ cur_rev = _get_config_revision_async(path).get_result()
if cur_rev != new_rev or force:
if _CONFIG_SCHEMAS[path]['use_authdb_transaction']:
dirty_in_authdb[path] = (new_rev, conf)
@@ -193,10 +199,11 @@
url = ndb.StringProperty(indexed=False)
-def _get_service_config_rev(cfg_name):
+@ndb.tasklet
+def _get_service_config_rev_async(cfg_name):
"""Returns last processed Revision of given config."""
- e = _AuthServiceConfig.get_by_id(cfg_name)
- return Revision(e.revision, e.url) if e else None
+ e = yield _AuthServiceConfig.get_by_id_async(cfg_name)
+ raise ndb.Return(Revision(e.revision, e.url) if e else None)
def _get_service_config(cfg_name):
@@ -222,13 +229,14 @@
### Group importer config implementation details.
-def _get_imports_config_revision():
+@ndb.tasklet
+def _get_imports_config_revision_async():
"""Returns Revision of last processed imports.cfg config."""
- e = importer.config_key().get()
+ e = yield importer.config_key().get_async()
if not e or not isinstance(e.config_revision, dict):
- return None
+ raise ndb.Return(None)
desc = e.config_revision
- return Revision(desc.get('rev'), desc.get('url'))
+ raise ndb.Return(Revision(desc.get('rev'), desc.get('url')))
def _update_imports_config(rev, conf):
@@ -255,15 +263,16 @@
return ndb.Key(_ImportedConfigRevisions, 'self', parent=model.root_key())
-def _get_authdb_config_rev(path):
+@ndb.tasklet
+def _get_authdb_config_rev_async(path):
"""Returns Revision of last processed config given its name."""
- mapping = _imported_config_revisions_key().get()
+ mapping = yield _imported_config_revisions_key().get_async()
if not mapping or not isinstance(mapping.revisions, dict):
- return None
+ raise ndb.Return(None)
desc = mapping.revisions.get(path)
if not isinstance(desc, dict):
- return None
- return Revision(desc.get('rev'), desc.get('url'))
+ raise ndb.Return(None)
+ raise ndb.Return(Revision(desc.get('rev'), desc.get('url')))
@datastore_utils.transactional
@@ -470,7 +479,7 @@
# Config file name -> {
# 'proto_class': protobuf class of the config or None to keep it as text,
-# 'revision_getter': lambda: <latest imported Revision>,
+# 'revision_getter': lambda: ndb.Future with <latest imported Revision>
# 'validator': lambda config: <raises ValueError on invalid format>
# 'updater': lambda rev, config: True if applied, False if not.
# 'use_authdb_transaction': True to call 'updater' in AuthDB transaction.
@@ -479,26 +488,26 @@
_CONFIG_SCHEMAS = {
'imports.cfg': {
'proto_class': None, # importer configs are stored as text
- 'revision_getter': _get_imports_config_revision,
+ 'revision_getter': _get_imports_config_revision_async,
'updater': _update_imports_config,
'use_authdb_transaction': False,
},
'ip_whitelist.cfg': {
'proto_class': config_pb2.IPWhitelistConfig,
- 'revision_getter': lambda: _get_authdb_config_rev('ip_whitelist.cfg'),
+ 'revision_getter': lambda: _get_authdb_config_rev_async('ip_whitelist.cfg'),
'updater': _update_ip_whitelist_config,
'use_authdb_transaction': True,
},
'oauth.cfg': {
'proto_class': config_pb2.OAuthConfig,
- 'revision_getter': lambda: _get_authdb_config_rev('oauth.cfg'),
+ 'revision_getter': lambda: _get_authdb_config_rev_async('oauth.cfg'),
'updater': _update_oauth_config,
'use_authdb_transaction': True,
},
'settings.cfg': {
'proto_class': None, # settings are stored as text in datastore
'default': '', # it's fine if config file is not there
- 'revision_getter': lambda: _get_service_config_rev('settings.cfg'),
+ 'revision_getter': lambda: _get_service_config_rev_async('settings.cfg'),
'updater': lambda rev, c: _update_service_config('settings.cfg', rev, c),
'use_authdb_transaction': False,
},
diff --git a/appengine/auth_service/config_test.py b/appengine/auth_service/config_test.py
index 0a4eefb..368fdb1 100755
--- a/appengine/auth_service/config_test.py
+++ b/appengine/auth_service/config_test.py
@@ -16,7 +16,6 @@
from components import config as config_component
from components import utils
from components.auth import model
-from components.config import validation_context
from test_support import test_case
from proto import config_pb2
@@ -28,25 +27,30 @@
super(ConfigTest, self).setUp()
def test_refetch_config(self):
- # Mock of "current known revision" state.
- revs = {
+ initial_revs = {
'a.cfg': config.Revision('old_a_rev', 'urla'),
'b.cfg': config.Revision('old_b_rev', 'urlb'),
'c.cfg': config.Revision('old_c_rev', 'urlc'),
}
+ revs = initial_revs.copy()
bumps = []
+
def bump_rev(pkg, rev, conf):
revs[pkg] = rev
bumps.append((pkg, rev, conf, ndb.in_transaction()))
return True
+ @ndb.tasklet
+ def get_rev_async(pkg):
+ raise ndb.Return(revs[pkg])
+
self.mock(config, 'is_remote_configured', lambda: True)
self.mock(config, '_CONFIG_SCHEMAS', {
# Will be updated outside of auth db transaction.
'a.cfg': {
'proto_class': None,
- 'revision_getter': lambda: revs['a.cfg'],
+ 'revision_getter': lambda: get_rev_async('a.cfg'),
'validator': lambda body: self.assertEqual(body, 'new a body'),
'updater': lambda rev, conf: bump_rev('a.cfg', rev, conf),
'use_authdb_transaction': False,
@@ -54,7 +58,7 @@
# Will not be changed.
'b.cfg': {
'proto_class': None,
- 'revision_getter': lambda: revs['b.cfg'],
+ 'revision_getter': lambda: get_rev_async('b.cfg'),
'validator': lambda _body: True,
'updater': lambda rev, conf: bump_rev('b.cfg', rev, conf),
'use_authdb_transaction': False,
@@ -62,17 +66,23 @@
# Will be updated inside auth db transaction.
'c.cfg': {
'proto_class': None,
- 'revision_getter': lambda: revs['c.cfg'],
+ 'revision_getter': lambda: get_rev_async('c.cfg'),
'validator': lambda body: self.assertEqual(body, 'new c body'),
'updater': lambda rev, conf: bump_rev('c.cfg', rev, conf),
'use_authdb_transaction': True,
},
})
- self.mock(config, '_fetch_configs', lambda _: {
+
+ # _fetch_configs is called by config.refetch_config().
+ configs_to_fetch = {
'a.cfg': (config.Revision('new_a_rev', 'urla'), 'new a body'),
'b.cfg': (config.Revision('old_b_rev', 'urlb'), 'old b body'),
'c.cfg': (config.Revision('new_c_rev', 'urlc'), 'new c body'),
- })
+ }
+ self.mock(config, '_fetch_configs', lambda _: configs_to_fetch)
+
+ # Old revisions initially.
+ self.assertEqual(initial_revs, config.get_revisions())
# Initial update.
config.refetch_config()
@@ -82,6 +92,11 @@
], bumps)
del bumps[:]
+ # Updated revisions now.
+ self.assertEqual(
+ {k: v[0] for k, v in configs_to_fetch.iteritems()},
+ config.get_revisions())
+
# Refetch, nothing new.
config.refetch_config()
self.assertFalse(bumps)
@@ -90,7 +105,8 @@
new_rev = config.Revision('rev', 'url')
body = 'tarball{url:"a" systems:"b"}'
self.assertTrue(config._update_imports_config(new_rev, body))
- self.assertEqual(new_rev, config._get_imports_config_revision())
+ self.assertEqual(
+ new_rev, config._get_imports_config_revision_async().get_result())
def test_validate_ip_whitelist_config_ok(self):
conf = config_pb2.IPWhitelistConfig(
@@ -598,16 +614,19 @@
def test_update_service_config(self):
# Missing.
self.assertIsNone(config._get_service_config('abc.cfg'))
- self.assertIsNone(config._get_service_config_rev('abc.cfg'))
+ self.assertIsNone(
+ config._get_service_config_rev_async('abc.cfg').get_result())
# Updated.
rev = config.Revision('rev', 'url')
self.assertTrue(config._update_service_config('abc.cfg', rev, 'body'))
self.assertEqual('body', config._get_service_config('abc.cfg'))
- self.assertEqual(rev, config._get_service_config_rev('abc.cfg'))
+ self.assertEqual(
+ rev, config._get_service_config_rev_async('abc.cfg').get_result())
# Same body, returns False, though updates rev.
rev2 = config.Revision('rev2', 'url')
self.assertFalse(config._update_service_config('abc.cfg', rev2, 'body'))
- self.assertEqual(rev2, config._get_service_config_rev('abc.cfg'))
+ self.assertEqual(
+ rev2, config._get_service_config_rev_async('abc.cfg').get_result())
def test_settings_updates(self):
# Fetch only settings.cfg in this test case.
diff --git a/appengine/auth_service/handlers_frontend.py b/appengine/auth_service/handlers_frontend.py
index ef87bca..f6f3fd1 100644
--- a/appengine/auth_service/handlers_frontend.py
+++ b/appengine/auth_service/handlers_frontend.py
@@ -70,23 +70,22 @@
template_file = 'auth_service/services.html'
-def get_additional_ui_environment(handler):
+def get_additional_ui_data():
"""Gets injected into Jinja and Javascript environment."""
- # See config._CONFIG_SCHEMAS for where these paths are defined.
- if isinstance(handler, ConfigHandler):
- path = 'imports.cfg'
- elif isinstance(handler, ui.IPWhitelistsHandler):
- path = 'ip_whitelist.cfg'
- elif isinstance(handler, ui.OAuthConfigHandler):
- path = 'oauth.cfg'
- else:
- return {'auth_service_config_locked': config.is_remote_configured()}
- rev = config.get_config_revision(path)
+ if not config.is_remote_configured():
+ return {'auth_service_config_locked': False}
+ config_revisions = {}
+ for path, rev in config.get_revisions().iteritems():
+ config_revisions[path] = {
+ 'rev': rev.revision if rev else 'none',
+ 'url': rev.url if rev else 'about:blank',
+ }
return {
- 'auth_service_config_locked': config.is_remote_configured(),
- 'auth_service_config_remote_url': config.get_remote_url(),
- 'auth_service_config_rev': rev.revision if rev else 'none',
- 'auth_service_config_url': rev.url if rev else 'about:blank',
+ 'auth_service_config_locked': True,
+ 'auth_service_configs': {
+ 'remote_url': config.get_remote_url(),
+ 'revisions': config_revisions,
+ },
}
@@ -440,7 +439,7 @@
ConfigHandler,
ui.ApiDocHandler,
],
- env_callback=get_additional_ui_environment)
+ ui_data_callback=get_additional_ui_data)
template.bootstrap({'auth_service': TEMPLATES_DIR})
# Add a fake admin for local dev server.
diff --git a/appengine/auth_service/templates/config.html b/appengine/auth_service/templates/config.html
index 9d4a101..1b7def5 100644
--- a/appengine/auth_service/templates/config.html
+++ b/appengine/auth_service/templates/config.html
@@ -21,7 +21,7 @@
</div>
<div class="form-group">
{% if auth_service_config_locked %}
- {{ render_config_locked() }}
+ {{ render_config_locked("imports.cfg") }}
{% else %}
{% if is_admin %}
<button type="submit" class="btn btn-primary">Save</button>
diff --git a/appengine/components/components/auth/ui/templates/base.html b/appengine/components/components/auth/ui/templates/base.html
index b5407fa..f77a5ea 100644
--- a/appengine/components/components/auth/ui/templates/base.html
+++ b/appengine/components/components/auth/ui/templates/base.html
@@ -46,13 +46,14 @@
{% endraw %}
{# Used on auth_service if config_service is managing configs #}
-{% macro render_config_locked() -%}
+{% macro render_config_locked(config_path) -%}
+{% set rev = auth_service_configs['revisions'][config_path] %}
<hr>
<p>
The configuration is distributed by
-<a target="_blank" href="{{auth_service_config_remote_url}}">luci-config</a>.
-Using revision <a target="_blank" href="{{auth_service_config_url}}"
-style="font-family:monospace;">{{auth_service_config_rev}}</a>.
+<a target="_blank" href="{{auth_service_configs['remote_url']}}">luci-config</a>.
+Using revision <a target="_blank" href="{{rev['url']}}"
+style="font-family:monospace;">{{rev['rev']}}</a>.
</p>
{%- endmacro %}
diff --git a/appengine/components/components/auth/ui/templates/ip_whitelists.html b/appengine/components/components/auth/ui/templates/ip_whitelists.html
index ab9ad63..9ad5f70 100644
--- a/appengine/components/components/auth/ui/templates/ip_whitelists.html
+++ b/appengine/components/components/auth/ui/templates/ip_whitelists.html
@@ -47,7 +47,7 @@
</div>
{% if auth_service_config_locked %}
<div class="form-group">
- {{ render_config_locked() }}
+ {{ render_config_locked("ip_whitelist.cfg") }}
</div>
{% endif %}
</form>
diff --git a/appengine/components/components/auth/ui/templates/oauth_config.html b/appengine/components/components/auth/ui/templates/oauth_config.html
index 260ffb1..536b1b0 100644
--- a/appengine/components/components/auth/ui/templates/oauth_config.html
+++ b/appengine/components/components/auth/ui/templates/oauth_config.html
@@ -90,7 +90,7 @@
<div class="form-group">
{% if auth_service_config_locked %}
<div class="col-sm-12">
- {{ render_config_locked() }}
+ {{ render_config_locked("oauth.cfg") }}
</div>
{% else %}
{% if is_admin %}
diff --git a/appengine/components/components/auth/ui/ui.py b/appengine/components/components/auth/ui/ui.py
index c0b3956..39a2b08 100644
--- a/appengine/components/components/auth/ui/ui.py
+++ b/appengine/components/components/auth/ui/ui.py
@@ -32,26 +32,26 @@
# Global static configuration set in 'configure_ui'.
_ui_app_name = 'Unknown'
-_ui_env_callback = None
+_ui_data_callback = None
_ui_navbar_tabs = ()
-def configure_ui(app_name, ui_tabs=None, env_callback=None):
+def configure_ui(app_name, ui_tabs=None, ui_data_callback=None):
"""Modifies global configuration of Auth UI.
Args:
app_name: name of the service (visible in page headers, titles, etc.)
ui_tabs: list of UINavbarTabHandler subclasses that define tabs to show, or
None to show the standard set of tabs.
- env_callback: callable that returns a dict with additional environment for
- templates. Will be called each time a template is rendered with single
- argument: UIHandler instance. Used by auth_service.
+ ui_data_callback: an argumentless callable that returns a dict with
+ additional data to return to authenticated users. It can be used by
+ server and client side code to render templates. Used by auth_service.
"""
global _ui_app_name
- global _ui_env_callback
+ global _ui_data_callback
global _ui_navbar_tabs
_ui_app_name = app_name
- _ui_env_callback = env_callback
+ _ui_data_callback = ui_data_callback
if ui_tabs is not None:
assert all(issubclass(cls, UINavbarTabHandler) for cls in ui_tabs)
_ui_navbar_tabs = tuple(ui_tabs)
@@ -339,8 +339,8 @@
'using_gae_auth': self.auth_method == handler.gae_cookie_authentication,
'xsrf_token': self.generate_xsrf_token(),
}
- if _ui_env_callback:
- common.update(_ui_env_callback(self))
+ if _ui_data_callback:
+ common.update(_ui_data_callback())
# Name of Javascript module with page code.
js_module_name = None