Skip to content

Commit 4ea8d45

Browse files
[WIP] public api implementation
1 parent f2e7900 commit 4ea8d45

19 files changed

+627
-267
lines changed

runbot/controllers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
from . import frontend
44
from . import hook
55
from . import badge
6+
from . import public_api

runbot/controllers/public_api.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import json
2+
3+
from werkzeug.exceptions import BadRequest, Forbidden
4+
5+
from odoo.exceptions import AccessError
6+
from odoo.http import Controller, request, route
7+
from odoo.tools import mute_logger
8+
9+
from odoo.addons.runbot.models.public_model_mixin import PublicModelMixin
10+
11+
12+
class PublicApi(Controller):
13+
14+
@mute_logger('odoo.addons.base.models.ir_model') # We don't care about logging acl errors
15+
def _get_model(self, model: str) -> PublicModelMixin:
16+
"""
17+
Returns the model from a model string.
18+
19+
Raises the appropriate exception if:
20+
- The model does not exist
21+
- The model is not a public model
22+
- The current user can not read the model
23+
"""
24+
pool = request.env.registry
25+
try:
26+
Model = pool[model]
27+
except KeyError:
28+
raise BadRequest('Unknown model')
29+
if not issubclass(Model, pool['runbot.public.model.mixin']):
30+
raise BadRequest('Unknown model')
31+
Model = request.env[model]
32+
Model.check_access('read')
33+
if not Model._allow_direct_access():
34+
raise Forbidden('This model does not allow direct access')
35+
return Model
36+
37+
@route('/runbot/api/models', auth='public', methods=['GET'], readonly=True)
38+
def models(self):
39+
models = []
40+
for model in request.env.keys():
41+
try:
42+
models.append(self._get_model(model))
43+
except (BadRequest, AccessError, Forbidden):
44+
pass
45+
return request.make_json_response(
46+
[Model._name for Model in models]
47+
)
48+
49+
@route('/runbot/api/<model>/read', auth='public', methods=['POST'], readonly=True, csrf=False)
50+
def read(self, *, model: str):
51+
Model = self._get_model(model)
52+
required_keys = Model._get_request_required_keys()
53+
allowed_keys = Model._get_request_allowed_keys()
54+
try:
55+
data = request.get_json_data()
56+
except json.JSONDecodeError:
57+
raise BadRequest('Invalid payload, missing or malformed json')
58+
if not isinstance(data, dict):
59+
raise BadRequest('Invalid payload, should be a dict.')
60+
if (missing_keys := required_keys - set(data.keys())):
61+
raise BadRequest(f'Invalid payload, missing keys: {", ".join(missing_keys)}')
62+
if (unknown_keys := set(data.keys()) - allowed_keys):
63+
raise BadRequest(f'Invalid payload, unknown keys: {", ".join(unknown_keys)}')
64+
if Model._public_requires_project():
65+
if not isinstance(data['project_id'], int):
66+
raise BadRequest('Invalid project_id, should be an int')
67+
# This is an additional layer of protection for project_id
68+
project = request.env['runbot.project'].browse(data['project_id']).exists()
69+
if not project:
70+
raise BadRequest('Unknown project_id')
71+
project.check_access('read')
72+
Model = Model.with_context(project_id=project.id)
73+
return request.make_json_response(Model._process_read_request(data))
74+
75+
@route('/runbot/api/<model>/spec', auth='public', methods=['GET'], readonly=True)
76+
def spec(self, *, model: str):
77+
Model = self._get_model(model)
78+
required_keys = Model._get_request_required_keys()
79+
allowed_keys = Model._get_request_allowed_keys()
80+
return request.make_json_response({
81+
'requires_project': Model._public_requires_project(),
82+
'default_page_size': Model._get_default_limit(),
83+
'max_page_size': Model._get_max_limit(),
84+
'required_keys': list(Model._get_request_required_keys()),
85+
'allowed_keys': list(allowed_keys - required_keys),
86+
'specification': self._get_model(model)._get_public_specification(),
87+
})

runbot/models/batch.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@
1212
class Batch(models.Model):
1313
_name = 'runbot.batch'
1414
_description = "Bundle batch"
15+
_inherit = ['runbot.public.model.mixin']
1516

16-
last_update = fields.Datetime('Last ref update')
17+
last_update = fields.Datetime('Last ref update', public=True)
1718
bundle_id = fields.Many2one('runbot.bundle', required=True, index=True, ondelete='cascade')
18-
commit_link_ids = fields.Many2many('runbot.commit.link')
19+
commit_link_ids = fields.Many2many('runbot.commit.link', public=True)
1920
commit_ids = fields.Many2many('runbot.commit', compute='_compute_commit_ids')
20-
slot_ids = fields.One2many('runbot.batch.slot', 'batch_id')
21+
slot_ids = fields.One2many('runbot.batch.slot', 'batch_id', public=True)
2122
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids', help="Recursive builds")
22-
state = fields.Selection([('preparing', 'Preparing'), ('ready', 'Ready'), ('done', 'Done'), ('skipped', 'Skipped')])
23+
state = fields.Selection([('preparing', 'Preparing'), ('ready', 'Ready'), ('done', 'Done'), ('skipped', 'Skipped')], public=True)
2324
hidden = fields.Boolean('Hidden', default=False)
24-
age = fields.Integer(compute='_compute_age', string='Build age')
25+
age = fields.Integer(compute='_compute_age', string='Build age', public=True)
2526
category_id = fields.Many2one('runbot.category', index=True, default=lambda self: self.env.ref('runbot.default_category', raise_if_not_found=False))
2627
log_ids = fields.One2many('runbot.batch.log', 'batch_id')
2728
has_warning = fields.Boolean("Has warning")
@@ -34,6 +35,10 @@ class Batch(models.Model):
3435
column2='referenced_batch_id',
3536
)
3637

38+
@api.model
39+
def _project_id_field_path(self):
40+
return 'bundle_id.project_id'
41+
3742
@api.depends('slot_ids.build_id')
3843
def _compute_all_build_ids(self):
3944
all_builds = self.env['runbot.build'].search([('id', 'child_of', self.slot_ids.build_id.ids)])
@@ -522,20 +527,25 @@ class BatchSlot(models.Model):
522527
_name = 'runbot.batch.slot'
523528
_description = 'Link between a bundle batch and a build'
524529
_order = 'trigger_id,id'
530+
_inherit = ['runbot.public.model.mixin']
525531

526-
batch_id = fields.Many2one('runbot.batch', index=True)
527-
trigger_id = fields.Many2one('runbot.trigger', index=True)
528-
build_id = fields.Many2one('runbot.build', index=True)
529-
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids')
532+
batch_id = fields.Many2one('runbot.batch', index=True, public=True)
533+
trigger_id = fields.Many2one('runbot.trigger', index=True, public=True)
534+
build_id = fields.Many2one('runbot.build', index=True, public=True)
535+
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids', public=True)
530536
params_id = fields.Many2one('runbot.build.params', index=True, required=True)
531-
link_type = fields.Selection([('created', 'Build created'), ('matched', 'Existing build matched'), ('rebuild', 'Rebuild')], required=True) # rebuild type?
532-
active = fields.Boolean('Attached', default=True)
537+
link_type = fields.Selection([('created', 'Build created'), ('matched', 'Existing build matched'), ('rebuild', 'Rebuild')], required=True, public=True) # rebuild type?
538+
active = fields.Boolean('Attached', default=True, public=True)
533539
skipped = fields.Boolean('Skipped', default=False)
534540
# rebuild, what to do: since build can be in multiple batch:
535541
# - replace for all batch?
536542
# - only available on batch and replace for batch only?
537543
# - create a new bundle batch will new linked build?
538544

545+
@api.model
546+
def _allow_direct_access(self):
547+
return False
548+
539549
@api.depends('build_id')
540550
def _compute_all_build_ids(self):
541551
all_builds = self.env['runbot.build'].search([('id', 'child_of', self.build_id.ids)])

runbot/models/branch.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ class Branch(models.Model):
1313
_description = "Branch"
1414
_order = 'name'
1515
_rec_name = 'dname'
16+
_inherit = ['runbot.public.model.mixin']
1617

1718
_sql_constraints = [('branch_repo_uniq', 'unique (name,remote_id)', 'The branch must be unique per repository !')]
1819

19-
name = fields.Char('Name', required=True)
20+
name = fields.Char('Name', required=True, public=True)
2021
remote_id = fields.Many2one('runbot.remote', 'Remote', required=True, ondelete='cascade', index=True)
2122

2223
head = fields.Many2one('runbot.commit', 'Head Commit', index=True)
@@ -25,7 +26,7 @@ class Branch(models.Model):
2526
reference_name = fields.Char(compute='_compute_reference_name', string='Bundle name', store=True)
2627
bundle_id = fields.Many2one('runbot.bundle', 'Bundle', ondelete='cascade', index=True)
2728

28-
is_pr = fields.Boolean('IS a pr', required=True)
29+
is_pr = fields.Boolean('IS a pr', required=True, public=True)
2930
pr_title = fields.Char('Pr Title')
3031
pr_body = fields.Char('Pr Body')
3132
pr_author = fields.Char('Pr Author')
@@ -37,12 +38,16 @@ class Branch(models.Model):
3738

3839
reflog_ids = fields.One2many('runbot.ref.log', 'branch_id')
3940

40-
branch_url = fields.Char(compute='_compute_branch_url', string='Branch url', readonly=True)
41-
dname = fields.Char('Display name', compute='_compute_dname', search='_search_dname')
41+
branch_url = fields.Char(compute='_compute_branch_url', string='Branch url', readonly=True, public=True)
42+
dname = fields.Char('Display name', compute='_compute_dname', search='_search_dname', public=True)
4243

4344
alive = fields.Boolean('Alive', default=True)
4445
draft = fields.Boolean('Draft', store=True)
4546

47+
@api.model
48+
def _project_id_field_path(self):
49+
return 'bundle_id.project_id'
50+
4651
@api.depends('name', 'remote_id.short_name')
4752
def _compute_dname(self):
4853
for branch in self:

runbot/models/build.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,25 @@ def make_selection(array):
4949
class BuildParameters(models.Model):
5050
_name = 'runbot.build.params'
5151
_description = "All information used by a build to run, should be unique and set on create only"
52+
_inherit = ['runbot.public.model.mixin']
5253

5354
# on param or on build?
5455
# execution parametter
5556
commit_link_ids = fields.Many2many('runbot.commit.link', copy=True)
5657
commit_ids = fields.Many2many('runbot.commit', compute='_compute_commit_ids')
5758
version_id = fields.Many2one('runbot.version', required=True, index=True)
5859
project_id = fields.Many2one('runbot.project', required=True, index=True) # for access rights
59-
trigger_id = fields.Many2one('runbot.trigger', index=True) # for access rights
60-
create_batch_id = fields.Many2one('runbot.batch', index=True)
61-
category = fields.Char('Category', index=True) # normal vs nightly vs weekly, ...
60+
trigger_id = fields.Many2one('runbot.trigger', index=True, public=True) # for access rights
61+
create_batch_id = fields.Many2one('runbot.batch', index=True, public=True)
62+
category = fields.Char('Category', index=True, public=True) # normal vs nightly vs weekly, ...
6263
dockerfile_id = fields.Many2one('runbot.dockerfile', index=True, default=lambda self: self.env.ref('runbot.docker_default', raise_if_not_found=False))
6364
skip_requirements = fields.Boolean('Skip requirements.txt auto install')
6465
# other informations
6566
extra_params = fields.Char('Extra cmd args')
66-
config_id = fields.Many2one('runbot.build.config', 'Run Config', required=True,
67+
config_id = fields.Many2one('runbot.build.config', 'Run Config', required=True, public=True,
6768
default=lambda self: self.env.ref('runbot.runbot_build_config_default', raise_if_not_found=False), index=True)
68-
config_data = JsonDictField('Config Data')
69-
used_custom_trigger = fields.Boolean('Custom trigger was used to generate this build')
69+
config_data = JsonDictField('Config Data', public=True)
70+
used_custom_trigger = fields.Boolean('Custom trigger was used to generate this build', public=True)
7071

7172
build_ids = fields.One2many('runbot.build', 'params_id')
7273
builds_reference_ids = fields.Many2many('runbot.build', relation='runbot_build_params_references', copy=True)
@@ -84,6 +85,10 @@ class BuildParameters(models.Model):
8485
('unique_fingerprint', 'unique (fingerprint)', 'avoid duplicate params'),
8586
]
8687

88+
@api.model
89+
def _allow_direct_access(self):
90+
return False
91+
8792
# @api.depends('version_id', 'project_id', 'extra_params', 'config_id', 'config_data', 'modules', 'commit_link_ids', 'builds_reference_ids')
8893
def _compute_fingerprint(self):
8994
for param in self:
@@ -141,6 +146,7 @@ class BuildResult(models.Model):
141146

142147
_name = 'runbot.build'
143148
_description = "Build"
149+
_inherit = ['runbot.public.model.mixin']
144150

145151
_parent_store = True
146152
_order = 'id desc'
@@ -154,26 +160,26 @@ class BuildResult(models.Model):
154160
no_auto_run = fields.Boolean('No run')
155161
# could be a default value, but possible to change it to allow duplicate accros branches
156162

157-
description = fields.Char('Description', help='Informative description')
158-
md_description = fields.Html(compute='_compute_md_description', string='MD Parsed Description', help='Informative description markdown parsed', sanitize=False)
159-
display_name = fields.Char(compute='_compute_display_name')
163+
description = fields.Char('Description', help='Informative description', public=True)
164+
md_description = fields.Html(compute='_compute_md_description', string='MD Parsed Description', help='Informative description markdown parsed', sanitize=False, public=True)
165+
display_name = fields.Char(compute='_compute_display_name', public=True)
160166

161167
# Related fields for convenience
162-
version_id = fields.Many2one('runbot.version', related='params_id.version_id', store=True, index=True)
163-
config_id = fields.Many2one('runbot.build.config', related='params_id.config_id', store=True, index=True)
164-
trigger_id = fields.Many2one('runbot.trigger', related='params_id.trigger_id', store=True, index=True)
165-
create_batch_id = fields.Many2one('runbot.batch', related='params_id.create_batch_id', store=True, index=True)
168+
version_id = fields.Many2one('runbot.version', related='params_id.version_id', store=True, index=True, public=True)
169+
config_id = fields.Many2one('runbot.build.config', related='params_id.config_id', store=True, index=True, public=True)
170+
trigger_id = fields.Many2one('runbot.trigger', related='params_id.trigger_id', store=True, index=True, public=True)
171+
create_batch_id = fields.Many2one('runbot.batch', related='params_id.create_batch_id', store=True, index=True, public=True)
166172

167173
# state machine
168-
global_state = fields.Selection(make_selection(state_order), string='Status', compute='_compute_global_state', store=True, recursive=True)
169-
local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, index=True)
170-
global_result = fields.Selection(make_selection(result_order), string='Result', compute='_compute_global_result', store=True, recursive=True)
171-
local_result = fields.Selection(make_selection(result_order), string='Build Result', default='ok')
174+
global_state = fields.Selection(make_selection(state_order), string='Status', compute='_compute_global_state', store=True, recursive=True, public=True)
175+
local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, index=True, public=True)
176+
global_result = fields.Selection(make_selection(result_order), string='Result', compute='_compute_global_result', store=True, recursive=True, public=True)
177+
local_result = fields.Selection(make_selection(result_order), string='Build Result', default='ok', public=True)
172178

173-
requested_action = fields.Selection([('wake_up', 'To wake up'), ('deathrow', 'To kill')], string='Action requested', index=True)
179+
requested_action = fields.Selection([('wake_up', 'To wake up'), ('deathrow', 'To kill')], string='Action requested', index=True, public=True)
174180
# web infos
175-
host = fields.Char('Host name')
176-
host_id = fields.Many2one('runbot.host', string="Host", compute='_compute_host_id')
181+
host = fields.Char('Host name', public=True)
182+
host_id = fields.Many2one('runbot.host', string="Host", compute='_compute_host_id', public=True)
177183
keep_host = fields.Boolean('Keep host on rebuild and for children')
178184

179185
port = fields.Integer('Port')
@@ -183,7 +189,7 @@ class BuildResult(models.Model):
183189
log_ids = fields.One2many('ir.logging', 'build_id', string='Logs')
184190
error_log_ids = fields.One2many('ir.logging', 'build_id', domain=[('level', 'in', ['WARNING', 'ERROR', 'CRITICAL'])], string='Error Logs')
185191
stat_ids = fields.One2many('runbot.build.stat', 'build_id', string='Statistics values')
186-
log_list = fields.Char('Comma separted list of step_ids names with logs')
192+
log_list = fields.Char('Comma separted list of step_ids names with logs', public=True)
187193

188194
active_step = fields.Many2one('runbot.build.config.step', 'Active step')
189195
job = fields.Char('Active step display name', compute='_compute_job')
@@ -234,13 +240,17 @@ class BuildResult(models.Model):
234240
slot_ids = fields.One2many('runbot.batch.slot', 'build_id')
235241
killable = fields.Boolean('Killable')
236242

237-
database_ids = fields.One2many('runbot.database', 'build_id')
243+
database_ids = fields.One2many('runbot.database', 'build_id', public=True)
238244
commit_export_ids = fields.One2many('runbot.commit.export', 'build_id')
239245

240246
static_run = fields.Char('Static run URL')
241247

242248
access_token = fields.Char('Token', default=lambda self: uuid.uuid4().hex)
243249

250+
@api.model
251+
def _project_id_field_path(self):
252+
return 'params_id.project_id'
253+
244254
@api.depends('description', 'params_id.config_id')
245255
def _compute_display_name(self):
246256
for build in self:

0 commit comments

Comments
 (0)