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