New scmpolicy plugin
Plugin for scm policy using data from SCM checkout. Related: https://pagure.io/koji/issue/3968
This commit is contained in:
parent
0251961929
commit
2013692fc9
4 changed files with 185 additions and 1 deletions
|
|
@ -442,3 +442,45 @@ For example:
|
||||||
For each RPM in the tag, Koji will use the first signed copy that it finds. In other words,
|
For each RPM in the tag, Koji will use the first signed copy that it finds. In other words,
|
||||||
Koji will try the first key (`45719a39`), and if Koji does not have the first key's signature
|
Koji will try the first key (`45719a39`), and if Koji does not have the first key's signature
|
||||||
for that RPM, then it will try the second key (`9867c58f`), third key (`38ab71f4`), and so on.
|
for that RPM, then it will try the second key (`9867c58f`), third key (`38ab71f4`), and so on.
|
||||||
|
|
||||||
|
|
||||||
|
SCM policy
|
||||||
|
==========
|
||||||
|
|
||||||
|
This plugin adds additional policy check after content is checked out from SCM.
|
||||||
|
New policy is simply named ``scm``.
|
||||||
|
|
||||||
|
Data which can be checked there contains ``build_tag``, ``method``,
|
||||||
|
``scratch``, and ``branches`` fields. Especially ``branches`` is the reason -
|
||||||
|
policy can e.g. check if reference being built is part of any allowed branch
|
||||||
|
and e.g. not random commit which can disappear later. Two new policy tests are
|
||||||
|
part of the plugin ``match_any`` and ``match_all`` which tests the list
|
||||||
|
against glob. So, in this case any (or all respectively) branch must pass the
|
||||||
|
glob test.
|
||||||
|
|
||||||
|
|
||||||
|
Example policy:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
scm =
|
||||||
|
# anything can be built as a scratch build
|
||||||
|
bool scratch :: allow
|
||||||
|
|
||||||
|
# regular build must be present at lease on one branch
|
||||||
|
match_all branches * !! deny Source ref must be contained in a branch
|
||||||
|
|
||||||
|
# Combination of method, scm and repo
|
||||||
|
method buildContainer && buildtag container-test-* && match scm_host git.example.com && match scm_repository /containers/* :: allow
|
||||||
|
|
||||||
|
# deny any other buildContainer task
|
||||||
|
method buildContainer :: deny Only specific buildContainer tasks can be executed
|
||||||
|
|
||||||
|
# allow anything else
|
||||||
|
all :: allow
|
||||||
|
|
||||||
|
Builder
|
||||||
|
-------
|
||||||
|
|
||||||
|
Plugin is simply activated by adding it as ``plugin = scmpolicy`` to
|
||||||
|
``/etc/kojid.conf``. No other configuration is required.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import logging
|
||||||
import six
|
import six
|
||||||
|
|
||||||
import koji
|
import koji
|
||||||
from koji.util import to_list
|
from koji.util import to_list, multi_fnmatch
|
||||||
|
|
||||||
|
|
||||||
class BaseSimpleTest(object):
|
class BaseSimpleTest(object):
|
||||||
|
|
@ -141,6 +141,57 @@ class MatchTest(BaseSimpleTest):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class MatchAnyTest(BaseSimpleTest):
|
||||||
|
"""Matches any item of a list/tuple/set value in the data against glob patterns
|
||||||
|
|
||||||
|
True if any of the expressions matches any item in the list/tuple/set, else False.
|
||||||
|
If the field doesn't exist or isn't a list/tuple/set, the test returns False
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
find field pattern1 [pattern2 ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = 'match_any'
|
||||||
|
field = None
|
||||||
|
|
||||||
|
def run(self, data):
|
||||||
|
args = self.str.split()[1:]
|
||||||
|
self.field = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
tgt = data.get(self.field)
|
||||||
|
if tgt and isinstance(tgt, (list, tuple, set)):
|
||||||
|
for i in tgt:
|
||||||
|
if i is not None and multi_fnmatch(str(i), args):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class MatchAllTest(BaseSimpleTest):
|
||||||
|
"""Matches all items of a list/tuple/set value in the data against glob patterns
|
||||||
|
|
||||||
|
True if any of the expressions matches all items in the list/tuple/set, else False.
|
||||||
|
If the field doesn't exist or isn't a list/tuple/set, the test returns False
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
match_all field pattern1 [pattern2 ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = 'match_all'
|
||||||
|
field = None
|
||||||
|
|
||||||
|
def run(self, data):
|
||||||
|
args = self.str.split()[1:]
|
||||||
|
self.field = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
tgt = data.get(self.field)
|
||||||
|
if tgt and isinstance(tgt, (list, tuple, set)):
|
||||||
|
for i in tgt:
|
||||||
|
if i is None or not multi_fnmatch(str(i), args):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class TargetTest(MatchTest):
|
class TargetTest(MatchTest):
|
||||||
"""Matches target in the data against glob patterns
|
"""Matches target in the data against glob patterns
|
||||||
|
|
||||||
|
|
|
||||||
72
plugins/builder/scmpolicy.py
Normal file
72
plugins/builder/scmpolicy.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from koji import ActionNotAllowed, GenericError
|
||||||
|
from koji.plugin import callback
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('koji.plugins.scmpolicy')
|
||||||
|
|
||||||
|
|
||||||
|
@callback('postSCMCheckout')
|
||||||
|
def assert_scm_policy(clb_type, *args, **kwargs):
|
||||||
|
taskinfo = kwargs['taskinfo']
|
||||||
|
session = kwargs['session']
|
||||||
|
build_tag = kwargs['build_tag']
|
||||||
|
scminfo = kwargs['scminfo']
|
||||||
|
srcdir = kwargs['srcdir']
|
||||||
|
scratch = kwargs['scratch']
|
||||||
|
|
||||||
|
method = get_task_method(session, taskinfo)
|
||||||
|
|
||||||
|
policy_data = {
|
||||||
|
'build_tag': build_tag,
|
||||||
|
'method': method,
|
||||||
|
'scratch': scratch,
|
||||||
|
'branches': get_branches(srcdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge scminfo into data with "scm_" prefix. And "scm*" are changed to "scm_*".
|
||||||
|
for k, v in six.iteritems(scminfo):
|
||||||
|
policy_data[re.sub(r'^(scm_?)?', 'scm_', k)] = v
|
||||||
|
|
||||||
|
logger.info("Checking SCM policy for task %s", taskinfo['id'])
|
||||||
|
logger.debug("Policy data: %r", policy_data)
|
||||||
|
|
||||||
|
# check the policy
|
||||||
|
try:
|
||||||
|
session.host.assertPolicy('scm', policy_data)
|
||||||
|
logger.info("SCM policy check for task %s: PASSED", taskinfo['id'])
|
||||||
|
except ActionNotAllowed:
|
||||||
|
logger.warning("SCM policy check for task %s: DENIED", taskinfo['id'])
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_method(session, taskinfo):
|
||||||
|
"""Get the Task method from taskinfo"""
|
||||||
|
method = None
|
||||||
|
if isinstance(taskinfo, six.integer_types):
|
||||||
|
taskinfo = session.getTaskInfo(taskinfo, strict=True)
|
||||||
|
if isinstance(taskinfo, dict):
|
||||||
|
method = taskinfo.get('method')
|
||||||
|
if method is None:
|
||||||
|
raise GenericError("Invalid taskinfo: %s" % taskinfo)
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def get_branches(srcdir):
|
||||||
|
"""Determine which remote branches contain the current checkout"""
|
||||||
|
cmd = ['git', 'branch', '-r', '--contains', 'HEAD']
|
||||||
|
proc = subprocess.Popen(cmd, cwd=srcdir, stdout=subprocess.PIPE)
|
||||||
|
(out, _) = proc.communicate()
|
||||||
|
status = proc.wait()
|
||||||
|
if status != 0:
|
||||||
|
raise Exception('Error getting branches for git checkout')
|
||||||
|
|
||||||
|
# cut off origin/ prefix
|
||||||
|
branches = [b.strip() for b in out.decode().split('\n') if 'origin/HEAD' not in b and b]
|
||||||
|
branches = [re.sub('^origin/', '', b) for b in branches]
|
||||||
|
return branches
|
||||||
|
|
@ -113,6 +113,23 @@ class TestBasicTests(unittest.TestCase):
|
||||||
koji.policy.CompareTest('some thing LOL 2')
|
koji.policy.CompareTest('some thing LOL 2')
|
||||||
|
|
||||||
|
|
||||||
|
def test_match_any_test(self):
|
||||||
|
obj = koji.policy.MatchAnyTest('not_important foo *bar* ext')
|
||||||
|
self.assertTrue(obj.run({'foo': ['barrrr', 'any']}))
|
||||||
|
self.assertTrue(obj.run({'foo': [None, 'bbbbbarrr', None]}))
|
||||||
|
self.assertFalse(obj.run({'foo': ['nah....']}))
|
||||||
|
self.assertFalse(obj.run({'foo': 'nah...'}))
|
||||||
|
self.assertFalse(obj.run({'bar': ['any']}))
|
||||||
|
|
||||||
|
def test_match_all_test(self):
|
||||||
|
obj = koji.policy.MatchAllTest('not_important foo *bar* ext')
|
||||||
|
self.assertTrue(obj.run({'foo': ['barrrr', 'bbbarrr']}))
|
||||||
|
self.assertFalse(obj.run({'foo': ['barrrr', 'nah....']}))
|
||||||
|
self.assertFalse(obj.run({'foo': [None, 'barrrr', None]}))
|
||||||
|
self.assertFalse(obj.run({'foo': 'nah...'}))
|
||||||
|
self.assertFalse(obj.run({'bar': ['any']}))
|
||||||
|
|
||||||
|
|
||||||
class TestDiscovery(unittest.TestCase):
|
class TestDiscovery(unittest.TestCase):
|
||||||
|
|
||||||
def test_find_simple_tests(self):
|
def test_find_simple_tests(self):
|
||||||
|
|
@ -124,6 +141,8 @@ class TestDiscovery(unittest.TestCase):
|
||||||
'false': koji.policy.FalseTest,
|
'false': koji.policy.FalseTest,
|
||||||
'has': koji.policy.HasTest,
|
'has': koji.policy.HasTest,
|
||||||
'match': koji.policy.MatchTest,
|
'match': koji.policy.MatchTest,
|
||||||
|
'match_all': koji.policy.MatchAllTest,
|
||||||
|
'match_any': koji.policy.MatchAnyTest,
|
||||||
'none': koji.policy.NoneTest,
|
'none': koji.policy.NoneTest,
|
||||||
'target': koji.policy.TargetTest,
|
'target': koji.policy.TargetTest,
|
||||||
'true': koji.policy.TrueTest,
|
'true': koji.policy.TrueTest,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue