OLD | NEW |
1 # Copyright 2015 The Swarming Authors. All rights reserved. | 1 # Copyright 2015 The Swarming Authors. All rights reserved. |
2 # Use of this source code is governed by the Apache v2.0 license that can be | 2 # Use of this source code is governed by the Apache v2.0 license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Cloud Endpoints API for configs. | 5 """Cloud Endpoints API for configs. |
6 | 6 |
7 * Reads/writes config service location. | 7 * Reads/writes config service location. |
8 * Validates configs. TODO(nodir): implement. | 8 * Validates configs. |
| 9 * Provides service metadata. |
9 """ | 10 """ |
10 | 11 |
11 import logging | 12 import logging |
12 | 13 |
13 from components import auth | |
14 from protorpc import messages | 14 from protorpc import messages |
15 from protorpc import message_types | 15 from protorpc import message_types |
16 from protorpc import remote | 16 from protorpc import remote |
| 17 import endpoints |
| 18 |
| 19 from components import auth |
17 | 20 |
18 from . import common | 21 from . import common |
19 from . import validation | 22 from . import validation |
20 | 23 |
21 | 24 |
| 25 METADATA_FORMAT_VERSION = "1.0" |
| 26 |
| 27 |
| 28 def get_default_rule_set(): |
| 29 return validation.DEFAULT_RULE_SET |
| 30 |
| 31 |
22 class ConfigSettingsMessage(messages.Message): | 32 class ConfigSettingsMessage(messages.Message): |
23 """Configuration service location.""" | 33 """Configuration service location. Resembles common.ConfigSettings""" |
24 # Example: 'luci-config.appspot.com' | 34 # Example: 'luci-config.appspot.com' |
25 service_hostname = messages.StringField(1) | 35 service_hostname = messages.StringField(1) |
| 36 # Example: 'user:luci-config@appspot.gserviceaccount.com' |
| 37 trusted_config_account = messages.StringField(2) |
26 | 38 |
27 | 39 |
28 class ValidateRequestMessage(messages.Message): | 40 class ValidateRequestMessage(messages.Message): |
29 config_set = messages.StringField(1, required=True) | 41 config_set = messages.StringField(1, required=True) |
30 path = messages.StringField(2, required=True) | 42 path = messages.StringField(2, required=True) |
31 content = messages.BytesField(3, required=True) | 43 content = messages.BytesField(3, required=True) |
32 | 44 |
33 | 45 |
34 class ValidationMessage(messages.Message): | 46 class ValidationMessage(messages.Message): |
35 class Severity(messages.Enum): | 47 class Severity(messages.Enum): |
36 DEBUG = logging.DEBUG | 48 DEBUG = logging.DEBUG |
37 INFO = logging.INFO | 49 INFO = logging.INFO |
38 WARNING = logging.WARNING | 50 WARNING = logging.WARNING |
39 ERROR = logging.ERROR | 51 ERROR = logging.ERROR |
40 CRITICAL = logging.CRITICAL | 52 CRITICAL = logging.CRITICAL |
41 text = messages.StringField(1, required=True) | 53 text = messages.StringField(1, required=True) |
42 severity = messages.EnumField(Severity, 2, required=True) | 54 severity = messages.EnumField(Severity, 2, required=True) |
43 | 55 |
44 | 56 |
45 class ValidateResponseMessage(messages.Message): | 57 class ValidateResponseMessage(messages.Message): |
46 messages = messages.MessageField(ValidationMessage, 1, repeated=True) | 58 messages = messages.MessageField(ValidationMessage, 1, repeated=True) |
47 | 59 |
48 | 60 |
| 61 def is_trusted_requester(): |
| 62 """Returns True if the requester can see the service metadata. |
| 63 |
| 64 Used in metadata endpoint. |
| 65 |
| 66 Returns: |
| 67 True if the current identity is an admin or the config service. |
| 68 """ |
| 69 if auth.is_admin(): |
| 70 return True |
| 71 |
| 72 settings = common.ConfigSettings.cached() |
| 73 if settings and settings.trusted_config_account: |
| 74 identity = auth.get_current_identity() |
| 75 if identity == settings.trusted_config_account: |
| 76 return True |
| 77 |
| 78 return False |
| 79 |
| 80 |
| 81 class ConfigPattern(messages.Message): |
| 82 """A pattern for one config file. See ServiceDynamicMetadata.""" |
| 83 config_set = messages.StringField(1, required=True) |
| 84 path = messages.StringField(2, required=True) |
| 85 |
| 86 |
| 87 class ServiceDynamicMetadata(messages.Message): |
| 88 """Equivalent of config_service's ServiceDynamicMetadata proto message. |
| 89 |
| 90 Keep this class in sync with: |
| 91 * ServiceDynamicMetadata message in |
| 92 appengine/config_service/proto/service_config.proto |
| 93 * validation.validate_service_dynamic_metadata_blob() |
| 94 * services._dict_to_dynamic_metadata() |
| 95 """ |
| 96 |
| 97 class Validator(messages.Message): |
| 98 patterns = messages.MessageField(ConfigPattern, 1, repeated=True) |
| 99 url = messages.StringField(2, required=True) |
| 100 |
| 101 version = messages.StringField(1, required=True) |
| 102 validation = messages.MessageField(Validator, 2) |
| 103 |
| 104 |
49 @auth.endpoints_api(name='config', version='v1', title='Configuration service') | 105 @auth.endpoints_api(name='config', version='v1', title='Configuration service') |
50 class ConfigApi(remote.Service): | 106 class ConfigApi(remote.Service): |
51 """Configuration service.""" | 107 """Configuration service.""" |
52 | 108 |
53 @auth.endpoints_method( | 109 @auth.endpoints_method( |
54 ConfigSettingsMessage, ConfigSettingsMessage, | 110 ConfigSettingsMessage, ConfigSettingsMessage, |
55 http_method='POST') | 111 http_method='POST') |
56 @auth.require(auth.is_admin) | 112 @auth.require(auth.is_admin) |
57 def settings(self, request): | 113 def settings(self, request): |
58 """Reads/writes config service location. Accessible only by admins.""" | 114 """Reads/writes config service location. Accessible only by admins.""" |
59 settings = common.ConfigSettings.fetch() or common.ConfigSettings() | 115 settings = common.ConfigSettings.fetch() or common.ConfigSettings() |
| 116 delta = {} |
60 if request.service_hostname is not None: | 117 if request.service_hostname is not None: |
61 # Change only if service_hostname was specified. | 118 delta['service_hostname'] = request.service_hostname |
62 changed = settings.modify(service_hostname=request.service_hostname) | 119 if request.trusted_config_account is not None: |
63 if changed: | 120 try: |
64 logging.warning('Updated config settings') | 121 delta['trusted_config_account'] = auth.Identity.from_bytes( |
| 122 request.trusted_config_account) |
| 123 except ValueError as ex: |
| 124 raise endpoints.BadRequestException( |
| 125 'Invalid trusted_config_account %s: %s' % ( |
| 126 request.trusted_config_account, |
| 127 ex.message)) |
| 128 changed = settings.modify(**delta) |
| 129 if changed: |
| 130 logging.warning('Updated config settings') |
65 settings = common.ConfigSettings.fetch() or settings | 131 settings = common.ConfigSettings.fetch() or settings |
66 return ConfigSettingsMessage( | 132 return ConfigSettingsMessage( |
67 service_hostname=settings.service_hostname, | 133 service_hostname=settings.service_hostname, |
| 134 trusted_config_account=( |
| 135 settings.trusted_config_account.to_bytes() |
| 136 if settings.trusted_config_account else None) |
68 ) | 137 ) |
69 | 138 |
70 @auth.endpoints_method( | 139 @auth.endpoints_method( |
71 ValidateRequestMessage, ValidateResponseMessage, http_method='POST') | 140 ValidateRequestMessage, ValidateResponseMessage, http_method='POST') |
72 def validate(self, request): | 141 def validate(self, request): |
73 """Validates a config. | 142 """Validates a config. |
74 | 143 |
75 Compatible with validation protocol described in ValidationCfg message of | 144 Compatible with validation protocol described in ValidationCfg message of |
76 /appengine/config_service/proto/service_config.proto. | 145 /appengine/config_service/proto/service_config.proto. |
77 """ | 146 """ |
78 ctx = validation.Context() | 147 ctx = validation.Context() |
79 validation.validate(request.config_set, request.path, request.content, ctx) | 148 validation.validate(request.config_set, request.path, request.content, ctx) |
80 | 149 |
81 res = ValidateResponseMessage() | 150 res = ValidateResponseMessage() |
82 for m in ctx.result().messages: | 151 for m in ctx.result().messages: |
83 res.messages.append(ValidationMessage( | 152 res.messages.append(ValidationMessage( |
84 severity=ValidationMessage.Severity.lookup_by_number(m.severity), | 153 severity=ValidationMessage.Severity.lookup_by_number(m.severity), |
85 text=m.text, | 154 text=m.text, |
86 )) | 155 )) |
87 return res | 156 return res |
| 157 |
| 158 @auth.endpoints_method( |
| 159 message_types.VoidMessage, ServiceDynamicMetadata, path='metadata') |
| 160 @auth.require(is_trusted_requester) |
| 161 def get_metadata(self, _request): |
| 162 """Describes a service. Used by config service to discover other services. |
| 163 """ |
| 164 meta = ServiceDynamicMetadata(version=METADATA_FORMAT_VERSION) |
| 165 http_headers = dict(self.request_state.headers) |
| 166 assert 'host' in http_headers, http_headers |
| 167 meta.validation = meta.Validator( |
| 168 url='https://{hostname}/_ah/api/{name}/{version}/{path}validate'.format( |
| 169 hostname=http_headers['host'], |
| 170 name=self.api_info.name, |
| 171 version=self.api_info.version, |
| 172 path=self.api_info.path or '', |
| 173 ) |
| 174 ) |
| 175 for p in sorted(get_default_rule_set().patterns()): |
| 176 meta.validation.patterns.append(ConfigPattern(**p._asdict())) |
| 177 return meta |
OLD | NEW |