1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16import copy 17import functools 18import re 19from collections import UserList 20 21from twisted.internet import defer 22 23from buildbot.data import exceptions 24 25 26class ResourceType: 27 name = None 28 plural = None 29 endpoints = [] 30 keyField = None 31 eventPathPatterns = "" 32 entityType = None 33 subresources = [] 34 35 def __init__(self, master): 36 self.master = master 37 self.compileEventPathPatterns() 38 39 def compileEventPathPatterns(self): 40 # We'll run a single format, and then split the string 41 # to get the final event path tuple 42 pathPatterns = self.eventPathPatterns 43 pathPatterns = pathPatterns.split() 44 identifiers = re.compile(r':([^/]*)') 45 for i, pp in enumerate(pathPatterns): 46 pp = identifiers.sub(r'{\1}', pp) 47 if pp.startswith("/"): 48 pp = pp[1:] 49 pathPatterns[i] = pp 50 self.eventPaths = pathPatterns 51 52 @functools.lru_cache(1) 53 def getEndpoints(self): 54 endpoints = self.endpoints[:] 55 for i, ep in enumerate(endpoints): 56 if not issubclass(ep, Endpoint): 57 raise TypeError("Not an Endpoint subclass") 58 endpoints[i] = ep(self, self.master) 59 return endpoints 60 61 @functools.lru_cache(1) 62 def getDefaultEndpoint(self): 63 for ep in self.getEndpoints(): 64 if not ep.isCollection: 65 return ep 66 return None 67 68 @functools.lru_cache(1) 69 def getCollectionEndpoint(self): 70 for ep in self.getEndpoints(): 71 if ep.isCollection or ep.isPseudoCollection: 72 return ep 73 return None 74 75 @staticmethod 76 def sanitizeMessage(msg): 77 msg = copy.deepcopy(msg) 78 return msg 79 80 def produceEvent(self, msg, event): 81 if msg is not None: 82 msg = self.sanitizeMessage(msg) 83 for path in self.eventPaths: 84 path = path.format(**msg) 85 routingKey = tuple(path.split("/")) + (event,) 86 self.master.mq.produce(routingKey, msg) 87 88 89class SubResource: 90 def __init__(self, rtype): 91 self.rtype = rtype 92 self.endpoints = {} 93 for endpoint in rtype.endpoints: 94 if endpoint.isCollection: 95 self.endpoints[rtype.plural] = endpoint 96 else: 97 self.endpoints[rtype.name] = endpoint 98 99 100class Endpoint: 101 pathPatterns = "" 102 rootLinkName = None 103 isCollection = False 104 isPseudoCollection = False 105 isRaw = False 106 parentMapping = {} 107 108 def __init__(self, rtype, master): 109 self.rtype = rtype 110 self.master = master 111 112 def get(self, resultSpec, kwargs): 113 raise NotImplementedError 114 115 def control(self, action, args, kwargs): 116 # we convert the action into a mixedCase method name 117 action_method = getattr(self, "action" + action.capitalize(), None) 118 if action_method is None: 119 raise exceptions.InvalidControlException("action: {} is not supported".format(action)) 120 return action_method(args, kwargs) 121 122 def get_kwargs_from_graphql_parent(self, parent, parent_type): 123 if parent_type not in self.parentMapping: 124 rtype = self.master.data.getResourceTypeForGraphQlType(parent_type) 125 if rtype.keyField in parent: 126 parentid = rtype.keyField 127 else: 128 raise NotImplementedError( 129 "Collection endpoint should implement " 130 "get_kwargs_from_graphql or parentMapping" 131 ) 132 else: 133 parentid = self.parentMapping[parent_type] 134 ret = {'graphql': True} 135 ret[parentid] = parent[parentid] 136 return ret 137 138 def get_kwargs_from_graphql(self, parent, resolve_info, args): 139 if self.isCollection or self.isPseudoCollection: 140 if parent is not None: 141 return self.get_kwargs_from_graphql_parent( 142 parent, resolve_info.parent_type.name 143 ) 144 return {'graphql': True} 145 ret = {'graphql': True} 146 k = self.rtype.keyField 147 v = args.pop(k) 148 if v is not None: 149 ret[k] = v 150 return ret 151 152 def __repr__(self): 153 return "endpoint for " + ",".join(self.pathPatterns.split()) 154 155 156class BuildNestingMixin: 157 158 """ 159 A mixin for methods to decipher the many ways a build, step, or log can be 160 specified. 161 """ 162 163 @defer.inlineCallbacks 164 def getBuildid(self, kwargs): 165 # need to look in the context of a step, specified by build or 166 # builder or whatever 167 if 'buildid' in kwargs: 168 return kwargs['buildid'] 169 else: 170 builderid = yield self.getBuilderId(kwargs) 171 if builderid is None: 172 return None 173 build = yield self.master.db.builds.getBuildByNumber( 174 builderid=builderid, 175 number=kwargs['build_number']) 176 if not build: 177 return None 178 return build['id'] 179 180 @defer.inlineCallbacks 181 def getStepid(self, kwargs): 182 if 'stepid' in kwargs: 183 return kwargs['stepid'] 184 else: 185 buildid = yield self.getBuildid(kwargs) 186 if buildid is None: 187 return None 188 189 dbdict = yield self.master.db.steps.getStep(buildid=buildid, 190 number=kwargs.get( 191 'step_number'), 192 name=kwargs.get('step_name')) 193 if not dbdict: 194 return None 195 return dbdict['id'] 196 197 def getBuilderId(self, kwargs): 198 if 'buildername' in kwargs: 199 return self.master.db.builders.findBuilderId(kwargs['buildername'], autoCreate=False) 200 return defer.succeed(kwargs['builderid']) 201 202 203class ListResult(UserList): 204 205 __slots__ = ['offset', 'total', 'limit'] 206 207 def __init__(self, values, 208 offset=None, total=None, limit=None): 209 super().__init__(values) 210 211 # if set, this is the index in the overall results of the first element of 212 # this list 213 self.offset = offset 214 215 # if set, this is the total number of results 216 self.total = total 217 218 # if set, this is the limit, either from the user or the implementation 219 self.limit = limit 220 221 def __repr__(self): 222 return "ListResult(%r, offset=%r, total=%r, limit=%r)" % \ 223 (self.data, self.offset, self.total, self.limit) 224 225 def __eq__(self, other): 226 if isinstance(other, ListResult): 227 return self.data == other.data \ 228 and self.offset == other.offset \ 229 and self.total == other.total \ 230 and self.limit == other.limit 231 return self.data == other \ 232 and self.offset == self.limit is None \ 233 and (self.total is None or self.total == len(other)) 234 235 def __ne__(self, other): 236 return not (self == other) 237 238 239def updateMethod(func): 240 """Decorate this resourceType instance as an update method, made available 241 at master.data.updates.$funcname""" 242 func.isUpdateMethod = True 243 return func 244