Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions runbot/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from . import frontend
from . import hook
from . import badge
from . import public_api
87 changes: 87 additions & 0 deletions runbot/controllers/public_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import json

from werkzeug.exceptions import BadRequest, Forbidden

from odoo.exceptions import AccessError
from odoo.http import Controller, request, route
from odoo.tools import mute_logger

from odoo.addons.runbot.models.public_model_mixin import PublicModelMixin


class PublicApi(Controller):

@mute_logger('odoo.addons.base.models.ir_model') # We don't care about logging acl errors
def _get_model(self, model: str) -> PublicModelMixin:
"""
Returns the model from a model string.

Raises the appropriate exception if:
- The model does not exist
- The model is not a public model
- The current user can not read the model
"""
pool = request.env.registry
try:
Model = pool[model]
except KeyError:
raise BadRequest('Unknown model')
if not issubclass(Model, pool['runbot.public.model.mixin']):
raise BadRequest('Unknown model')
Model = request.env[model]
Model.check_access('read')
if not Model._api_request_allow_direct_access():
raise Forbidden('This model does not allow direct access')
return Model

@route('/runbot/api/models', auth='public', methods=['GET'], readonly=True)
def models(self):
models = []
for model in request.env.keys():
try:
models.append(self._get_model(model))
except (BadRequest, AccessError, Forbidden):
pass
return request.make_json_response(
[Model._name for Model in models]
)

@route('/runbot/api/<model>/read', auth='public', methods=['POST'], readonly=True, csrf=False)
def read(self, *, model: str):
Model = self._get_model(model)
required_keys = Model._api_request_required_keys()
allowed_keys = Model._api_request_allowed_keys()
try:
data = request.get_json_data()
except json.JSONDecodeError:
raise BadRequest('Invalid payload, missing or malformed json')
if not isinstance(data, dict):
raise BadRequest('Invalid payload, should be a dict.')
if (missing_keys := required_keys - set(data.keys())):
raise BadRequest(f'Invalid payload, missing keys: {", ".join(missing_keys)}')
if (unknown_keys := set(data.keys()) - allowed_keys):
raise BadRequest(f'Invalid payload, unknown keys: {", ".join(unknown_keys)}')
if Model._api_request_requires_project():
if not isinstance(data['project_id'], int):
raise BadRequest('Invalid project_id, should be an int')
# This is an additional layer of protection for project_id
project = request.env['runbot.project'].browse(data['project_id']).exists()
if not project:
raise BadRequest('Unknown project_id')
project.check_access('read')
Model = Model.with_context(project_id=project.id)
return request.make_json_response(Model._api_request_read(data))

@route('/runbot/api/<model>/spec', auth='public', methods=['GET'], readonly=True)
def spec(self, *, model: str):
Model = self._get_model(model)
required_keys = Model._api_request_required_keys()
allowed_keys = Model._api_request_allowed_keys()
return request.make_json_response({
'requires_project': Model._api_request_requires_project(),
'default_page_size': Model._api_request_default_limit(),
'max_page_size': Model._api_request_max_limit(),
'required_keys': list(Model._api_request_required_keys()),
'allowed_keys': list(allowed_keys - required_keys),
'specification': self._get_model(model)._api_public_specification(),
})
2 changes: 2 additions & 0 deletions runbot/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

from . import public_model_mixin

from . import batch
from . import branch
from . import build
Expand Down
32 changes: 21 additions & 11 deletions runbot/models/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
class Batch(models.Model):
_name = 'runbot.batch'
_description = "Bundle batch"
_inherit = ['runbot.public.model.mixin']

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

@api.model
def _api_project_id_field_path(self):
return 'bundle_id.project_id'

@api.depends('slot_ids.build_id')
def _compute_all_build_ids(self):
all_builds = self.env['runbot.build'].search([('id', 'child_of', self.slot_ids.build_id.ids)])
Expand Down Expand Up @@ -522,20 +527,25 @@ class BatchSlot(models.Model):
_name = 'runbot.batch.slot'
_description = 'Link between a bundle batch and a build'
_order = 'trigger_id,id'
_inherit = ['runbot.public.model.mixin']

batch_id = fields.Many2one('runbot.batch', index=True)
trigger_id = fields.Many2one('runbot.trigger', index=True)
build_id = fields.Many2one('runbot.build', index=True)
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids')
batch_id = fields.Many2one('runbot.batch', index=True, public=True)
trigger_id = fields.Many2one('runbot.trigger', index=True, public=True)
build_id = fields.Many2one('runbot.build', index=True, public=True)
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids', public=True)
params_id = fields.Many2one('runbot.build.params', index=True, required=True)
link_type = fields.Selection([('created', 'Build created'), ('matched', 'Existing build matched'), ('rebuild', 'Rebuild')], required=True) # rebuild type?
active = fields.Boolean('Attached', default=True)
link_type = fields.Selection([('created', 'Build created'), ('matched', 'Existing build matched'), ('rebuild', 'Rebuild')], required=True, public=True) # rebuild type?
active = fields.Boolean('Attached', default=True, public=True)
skipped = fields.Boolean('Skipped', default=False)
# rebuild, what to do: since build can be in multiple batch:
# - replace for all batch?
# - only available on batch and replace for batch only?
# - create a new bundle batch will new linked build?

@api.model
def _api_request_allow_direct_access(self):
return False

@api.depends('build_id')
def _compute_all_build_ids(self):
all_builds = self.env['runbot.build'].search([('id', 'child_of', self.build_id.ids)])
Expand Down
13 changes: 9 additions & 4 deletions runbot/models/branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ class Branch(models.Model):
_description = "Branch"
_order = 'name'
_rec_name = 'dname'
_inherit = ['runbot.public.model.mixin']

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

name = fields.Char('Name', required=True)
name = fields.Char('Name', required=True, public=True)
remote_id = fields.Many2one('runbot.remote', 'Remote', required=True, ondelete='cascade', index=True)

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

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

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

branch_url = fields.Char(compute='_compute_branch_url', string='Branch url', readonly=True)
dname = fields.Char('Display name', compute='_compute_dname', search='_search_dname')
branch_url = fields.Char(compute='_compute_branch_url', string='Branch url', readonly=True, public=True)
dname = fields.Char('Display name', compute='_compute_dname', search='_search_dname', public=True)

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

@api.model
def _api_project_id_field_path(self):
return 'bundle_id.project_id'

@api.depends('name', 'remote_id.short_name')
def _compute_dname(self):
for branch in self:
Expand Down
56 changes: 33 additions & 23 deletions runbot/models/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,25 @@ def make_selection(array):
class BuildParameters(models.Model):
_name = 'runbot.build.params'
_description = "All information used by a build to run, should be unique and set on create only"
_inherit = ['runbot.public.model.mixin']

# on param or on build?
# execution parametter
commit_link_ids = fields.Many2many('runbot.commit.link', copy=True)
commit_ids = fields.Many2many('runbot.commit', compute='_compute_commit_ids')
version_id = fields.Many2one('runbot.version', required=True, index=True)
project_id = fields.Many2one('runbot.project', required=True, index=True) # for access rights
trigger_id = fields.Many2one('runbot.trigger', index=True) # for access rights
create_batch_id = fields.Many2one('runbot.batch', index=True)
category = fields.Char('Category', index=True) # normal vs nightly vs weekly, ...
trigger_id = fields.Many2one('runbot.trigger', index=True, public=True) # for access rights
create_batch_id = fields.Many2one('runbot.batch', index=True, public=True)
category = fields.Char('Category', index=True, public=True) # normal vs nightly vs weekly, ...
dockerfile_id = fields.Many2one('runbot.dockerfile', index=True, default=lambda self: self.env.ref('runbot.docker_default', raise_if_not_found=False))
skip_requirements = fields.Boolean('Skip requirements.txt auto install')
# other informations
extra_params = fields.Char('Extra cmd args')
config_id = fields.Many2one('runbot.build.config', 'Run Config', required=True,
config_id = fields.Many2one('runbot.build.config', 'Run Config', required=True, public=True,
default=lambda self: self.env.ref('runbot.runbot_build_config_default', raise_if_not_found=False), index=True)
config_data = JsonDictField('Config Data')
used_custom_trigger = fields.Boolean('Custom trigger was used to generate this build')
config_data = JsonDictField('Config Data', public=True)
used_custom_trigger = fields.Boolean('Custom trigger was used to generate this build', public=True)

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

@api.model
def _api_request_allow_direct_access(self):
return False

# @api.depends('version_id', 'project_id', 'extra_params', 'config_id', 'config_data', 'modules', 'commit_link_ids', 'builds_reference_ids')
def _compute_fingerprint(self):
for param in self:
Expand Down Expand Up @@ -141,6 +146,7 @@ class BuildResult(models.Model):

_name = 'runbot.build'
_description = "Build"
_inherit = ['runbot.public.model.mixin']

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

description = fields.Char('Description', help='Informative description')
md_description = fields.Html(compute='_compute_md_description', string='MD Parsed Description', help='Informative description markdown parsed', sanitize=False)
display_name = fields.Char(compute='_compute_display_name')
description = fields.Char('Description', help='Informative description', public=True)
md_description = fields.Html(compute='_compute_md_description', string='MD Parsed Description', help='Informative description markdown parsed', sanitize=False, public=True)
display_name = fields.Char(compute='_compute_display_name', public=True)

# Related fields for convenience
version_id = fields.Many2one('runbot.version', related='params_id.version_id', store=True, index=True)
config_id = fields.Many2one('runbot.build.config', related='params_id.config_id', store=True, index=True)
trigger_id = fields.Many2one('runbot.trigger', related='params_id.trigger_id', store=True, index=True)
create_batch_id = fields.Many2one('runbot.batch', related='params_id.create_batch_id', store=True, index=True)
create_bundle_id = fields.Many2one('runbot.bundle', related='params_id.create_batch_id.bundle_id', index=True)
version_id = fields.Many2one('runbot.version', related='params_id.version_id', store=True, index=True, public=True)
config_id = fields.Many2one('runbot.build.config', related='params_id.config_id', store=True, index=True, public=True)
trigger_id = fields.Many2one('runbot.trigger', related='params_id.trigger_id', store=True, index=True, public=True)
create_batch_id = fields.Many2one('runbot.batch', related='params_id.create_batch_id', store=True, index=True, public=True)
create_bundle_id = fields.Many2one('runbot.bundle', related='params_id.create_batch_id.bundle_id', index=True, public=True)

# state machine
global_state = fields.Selection(make_selection(state_order), string='Status', compute='_compute_global_state', store=True, recursive=True)
local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, index=True)
global_result = fields.Selection(make_selection(result_order), string='Result', compute='_compute_global_result', store=True, recursive=True)
local_result = fields.Selection(make_selection(result_order), string='Build Result', default='ok')
global_state = fields.Selection(make_selection(state_order), string='Status', compute='_compute_global_state', store=True, recursive=True, public=True)
local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, index=True, public=True)
global_result = fields.Selection(make_selection(result_order), string='Result', compute='_compute_global_result', store=True, recursive=True, public=True)
local_result = fields.Selection(make_selection(result_order), string='Build Result', default='ok', public=True)

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

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

active_step = fields.Many2one('runbot.build.config.step', 'Active step')
job = fields.Char('Active step display name', compute='_compute_job')
Expand Down Expand Up @@ -235,13 +241,17 @@ class BuildResult(models.Model):
slot_ids = fields.One2many('runbot.batch.slot', 'build_id')
killable = fields.Boolean('Killable')

database_ids = fields.One2many('runbot.database', 'build_id')
database_ids = fields.One2many('runbot.database', 'build_id', public=True)
commit_export_ids = fields.One2many('runbot.commit.export', 'build_id')

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

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

@api.model
def _api_project_id_field_path(self):
return 'params_id.project_id'

@api.depends('description', 'params_id.config_id')
def _compute_display_name(self):
for build in self:
Expand Down
Loading