Index: appengine/components/components/config/endpoint.py |
diff --git a/appengine/components/components/config/endpoint.py b/appengine/components/components/config/endpoint.py |
index e7c9e4ebd31b0cb7f941c0b8c8716993b10c5df8..c48434ef1280cad892992f624a2fe8d9a35c0ba5 100644 |
--- a/appengine/components/components/config/endpoint.py |
+++ b/appengine/components/components/config/endpoint.py |
@@ -5,24 +5,36 @@ |
"""Cloud Endpoints API for configs. |
* Reads/writes config service location. |
-* Validates configs. TODO(nodir): implement. |
+* Validates configs. |
+* Provides service metadata. |
""" |
import logging |
-from components import auth |
from protorpc import messages |
from protorpc import message_types |
from protorpc import remote |
+import endpoints |
+ |
+from components import auth |
from . import common |
from . import validation |
+METADATA_FORMAT_VERSION = "1.0" |
+ |
+ |
+def get_default_rule_set(): |
+ return validation.DEFAULT_RULE_SET |
+ |
+ |
class ConfigSettingsMessage(messages.Message): |
- """Configuration service location.""" |
+ """Configuration service location. Resembles common.ConfigSettings""" |
# Example: 'luci-config.appspot.com' |
service_hostname = messages.StringField(1) |
+ # Example: 'user:luci-config@appspot.gserviceaccount.com' |
+ trusted_config_account = messages.StringField(2) |
class ValidateRequestMessage(messages.Message): |
@@ -46,6 +58,50 @@ class ValidateResponseMessage(messages.Message): |
messages = messages.MessageField(ValidationMessage, 1, repeated=True) |
+def is_trusted_requester(): |
+ """Returns True if the requester can see the service metadata. |
+ |
+ Used in metadata endpoint. |
+ |
+ Returns: |
+ True if the current identity is an admin or the config service. |
+ """ |
+ if auth.is_admin(): |
+ return True |
+ |
+ settings = common.ConfigSettings.cached() |
+ if settings and settings.trusted_config_account: |
+ identity = auth.get_current_identity() |
+ if identity == settings.trusted_config_account: |
+ return True |
+ |
+ return False |
+ |
+ |
+class ConfigPattern(messages.Message): |
+ """A pattern for one config file. See ServiceDynamicMetadata.""" |
+ config_set = messages.StringField(1, required=True) |
+ path = messages.StringField(2, required=True) |
+ |
+ |
+class ServiceDynamicMetadata(messages.Message): |
+ """Equivalent of config_service's ServiceDynamicMetadata proto message. |
+ |
+ Keep this class in sync with: |
+ * ServiceDynamicMetadata message in |
+ appengine/config_service/proto/service_config.proto |
+ * validation.validate_service_dynamic_metadata_blob() |
+ * services._dict_to_dynamic_metadata() |
+ """ |
+ |
+ class Validator(messages.Message): |
+ patterns = messages.MessageField(ConfigPattern, 1, repeated=True) |
+ url = messages.StringField(2, required=True) |
+ |
+ version = messages.StringField(1, required=True) |
+ validation = messages.MessageField(Validator, 2) |
+ |
+ |
@auth.endpoints_api(name='config', version='v1', title='Configuration service') |
class ConfigApi(remote.Service): |
"""Configuration service.""" |
@@ -57,14 +113,27 @@ class ConfigApi(remote.Service): |
def settings(self, request): |
"""Reads/writes config service location. Accessible only by admins.""" |
settings = common.ConfigSettings.fetch() or common.ConfigSettings() |
+ delta = {} |
if request.service_hostname is not None: |
- # Change only if service_hostname was specified. |
- changed = settings.modify(service_hostname=request.service_hostname) |
- if changed: |
- logging.warning('Updated config settings') |
+ delta['service_hostname'] = request.service_hostname |
+ if request.trusted_config_account is not None: |
+ try: |
+ delta['trusted_config_account'] = auth.Identity.from_bytes( |
+ request.trusted_config_account) |
+ except ValueError as ex: |
+ raise endpoints.BadRequestException( |
+ 'Invalid trusted_config_account %s: %s' % ( |
+ request.trusted_config_account, |
+ ex.message)) |
+ changed = settings.modify(**delta) |
+ if changed: |
+ logging.warning('Updated config settings') |
settings = common.ConfigSettings.fetch() or settings |
return ConfigSettingsMessage( |
service_hostname=settings.service_hostname, |
+ trusted_config_account=( |
+ settings.trusted_config_account.to_bytes() |
+ if settings.trusted_config_account else None) |
) |
@auth.endpoints_method( |
@@ -85,3 +154,24 @@ class ConfigApi(remote.Service): |
text=m.text, |
)) |
return res |
+ |
+ @auth.endpoints_method( |
+ message_types.VoidMessage, ServiceDynamicMetadata, path='metadata') |
+ @auth.require(is_trusted_requester) |
+ def get_metadata(self, _request): |
+ """Describes a service. Used by config service to discover other services. |
+ """ |
+ meta = ServiceDynamicMetadata(version=METADATA_FORMAT_VERSION) |
+ http_headers = dict(self.request_state.headers) |
+ assert 'host' in http_headers, http_headers |
+ meta.validation = meta.Validator( |
+ url='https://{hostname}/_ah/api/{name}/{version}/{path}validate'.format( |
+ hostname=http_headers['host'], |
+ name=self.api_info.name, |
+ version=self.api_info.version, |
+ path=self.api_info.path or '', |
+ ) |
+ ) |
+ for p in sorted(get_default_rule_set().patterns()): |
+ meta.validation.patterns.append(ConfigPattern(**p._asdict())) |
+ return meta |