Add disallowed-labels tenant option
To allow a tenant to use any labels *except* some pattern, add the disallowed-labels tenant option. Both this and the allowed-labels option use re2, and therefore lookahead assertions are not supported. A complimentary option to allowed-labels is the only way to support this use case. Change-Id: Ic722b1d2b0b609ec7de583dab159094159f00630
This commit is contained in:
parent
fbc0bbd314
commit
61e5c3a0f9
@ -293,9 +293,21 @@ configuration. Some examples of tenant definitions are:
|
||||
.. attr:: allowed-labels
|
||||
:default: []
|
||||
|
||||
The list of labels regexp a tenant can use in job's nodeset. When set,
|
||||
this setting can be used to restrict what labels a tenant can use.
|
||||
Without this setting, the tenant can use any labels.
|
||||
The list of labels (as strings or regular expressions) a tenant
|
||||
can use in a job's nodeset. When set, this setting can be used
|
||||
to restrict what labels a tenant can use. Without this setting,
|
||||
the tenant can use any labels.
|
||||
|
||||
.. attr:: disallowed-labels
|
||||
:default: []
|
||||
|
||||
The list of labels (as strings or regular expressions) a tenant
|
||||
is forbidden to use in a job's nodeset. When set, this setting
|
||||
can be used to restrict what labels a tenant can use. Without
|
||||
this setting, the tenant can use any labels permitted by
|
||||
:attr:`tenant.allowed-labels`. This check is applied after the
|
||||
check for `allowed-labels` and may therefore be used to further
|
||||
restrict the set of permitted labels.
|
||||
|
||||
.. attr:: report-build-page
|
||||
:default: false
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
A new tenant option :attr:`tenant.disallowed-labels`
|
||||
(similar to :attr:`tenant.allowed-labels`)
|
||||
can be used to restrict what labels a tenant has access to.
|
2
tests/fixtures/config/multi-tenant/main.yaml
vendored
2
tests/fixtures/config/multi-tenant/main.yaml
vendored
@ -19,6 +19,8 @@
|
||||
name: tenant-two
|
||||
max-nodes-per-job: 10
|
||||
allowed-triggers: gerrit
|
||||
disallowed-labels:
|
||||
- tenant-one-.*
|
||||
source:
|
||||
gerrit:
|
||||
config-projects:
|
||||
|
@ -3907,6 +3907,26 @@ class TestAllowedLabels(AnsibleZuulTestCase):
|
||||
A.messages[0],
|
||||
"A should fail because of allowed-labels")
|
||||
|
||||
def test_disallowed_labels(self):
|
||||
in_repo_conf = textwrap.dedent(
|
||||
"""
|
||||
- job:
|
||||
name: test
|
||||
nodeset:
|
||||
nodes:
|
||||
- name: controller
|
||||
label: tenant-one-label
|
||||
""")
|
||||
file_dict = {'zuul.d/test.yaml': in_repo_conf}
|
||||
A = self.fake_gerrit.addFakeChange(
|
||||
'tenant-two-config', 'master', 'A', files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertIn(
|
||||
'Label named "tenant-one-label" is not part of the allowed',
|
||||
A.messages[0],
|
||||
"A should fail because of disallowed-labels")
|
||||
|
||||
|
||||
class TestPragma(ZuulTestCase):
|
||||
tenant_config_file = 'config/pragma/main.yaml'
|
||||
|
@ -955,13 +955,14 @@ class TestWebMultiTenant(BaseTestWeb):
|
||||
def test_web_labels_allowed_list(self):
|
||||
labels = ["tenant-one-label", "fake", "tenant-two-label"]
|
||||
self.fake_nodepool.registerLauncher(labels, "FakeLauncher2")
|
||||
# Tenant-one has label restriction in place
|
||||
# Tenant-one has label restriction in place on tenant-two
|
||||
res = self.get_url('api/tenant/tenant-one/labels').json()
|
||||
self.assertEqual([{'name': 'fake'}, {'name': 'tenant-one-label'}], res)
|
||||
# Tenant-two does not
|
||||
# Tenant-two has label restriction in place on tenant-one
|
||||
expected = ["label1", "fake", "tenant-two-label"]
|
||||
res = self.get_url('api/tenant/tenant-two/labels').json()
|
||||
self.assertEqual(
|
||||
list(map(lambda x: {'name': x}, sorted(labels + ["label1"]))), res)
|
||||
list(map(lambda x: {'name': x}, sorted(expected))), res)
|
||||
|
||||
|
||||
class TestWebSecrets(BaseTestWeb):
|
||||
|
@ -81,13 +81,21 @@ class UnknownConnection(Exception):
|
||||
|
||||
|
||||
class LabelForbiddenError(Exception):
|
||||
def __init__(self, label, allowed_labels):
|
||||
def __init__(self, label, allowed_labels, disallowed_labels):
|
||||
message = textwrap.dedent("""\
|
||||
Label named "{label}" is not part of the allowed
|
||||
labels ({allowed_labels}) for this tenant.""")
|
||||
# Make a string that looks like "a, b and not c, d" if we have
|
||||
# both allowed and disallowed labels.
|
||||
labels = ", ".join(allowed_labels or [])
|
||||
if allowed_labels and disallowed_labels:
|
||||
labels += ' and '
|
||||
if disallowed_labels:
|
||||
labels += 'not '
|
||||
labels += ", ".join(disallowed_labels)
|
||||
message = textwrap.fill(message.format(
|
||||
label=label,
|
||||
allowed_labels=", ".join(allowed_labels)))
|
||||
allowed_labels=labels))
|
||||
super(LabelForbiddenError, self).__init__(message)
|
||||
|
||||
|
||||
@ -500,13 +508,25 @@ class NodeSetParser(object):
|
||||
node_names = set()
|
||||
group_names = set()
|
||||
allowed_labels = self.pcontext.tenant.allowed_labels
|
||||
disallowed_labels = self.pcontext.tenant.disallowed_labels
|
||||
for conf_node in as_list(conf['nodes']):
|
||||
allowed = True
|
||||
if allowed_labels:
|
||||
if not [True for allowed_label in allowed_labels if
|
||||
re2.match(allowed_label, conf_node['label'])]:
|
||||
raise LabelForbiddenError(
|
||||
label=conf_node['label'],
|
||||
allowed_labels=allowed_labels)
|
||||
allowed = False
|
||||
for pattern in allowed_labels:
|
||||
if re2.match(pattern, conf_node['label']):
|
||||
allowed = True
|
||||
break
|
||||
if disallowed_labels:
|
||||
for pattern in disallowed_labels:
|
||||
if re2.match(pattern, conf_node['label']):
|
||||
allowed = False
|
||||
break
|
||||
if not allowed:
|
||||
raise LabelForbiddenError(
|
||||
label=conf_node['label'],
|
||||
allowed_labels=allowed_labels,
|
||||
disallowed_labels=disallowed_labels)
|
||||
for name in as_list(conf_node['name']):
|
||||
if name in node_names:
|
||||
raise DuplicateNodeError(name, conf_node['name'])
|
||||
@ -1456,6 +1476,7 @@ class TenantParser(object):
|
||||
'allowed-triggers': to_list(str),
|
||||
'allowed-reporters': to_list(str),
|
||||
'allowed-labels': to_list(str),
|
||||
'disallowed-labels': to_list(str),
|
||||
'default-parent': str,
|
||||
'default-ansible-version': vs.Any(str, float),
|
||||
'admin-rules': to_list(str),
|
||||
@ -1484,6 +1505,7 @@ class TenantParser(object):
|
||||
tenant.allowed_triggers = conf.get('allowed-triggers')
|
||||
tenant.allowed_reporters = conf.get('allowed-reporters')
|
||||
tenant.allowed_labels = conf.get('allowed-labels')
|
||||
tenant.disallowed_labels = conf.get('disallowed-labels')
|
||||
tenant.default_base_job = conf.get('default-parent', 'base')
|
||||
|
||||
tenant.unparsed_config = conf
|
||||
|
@ -499,10 +499,10 @@ class RPCListener(object):
|
||||
if not tenant:
|
||||
job.sendWorkComplete(json.dumps(None))
|
||||
return
|
||||
labels = tenant.allowed_labels
|
||||
if not labels:
|
||||
labels = []
|
||||
job.sendWorkComplete(json.dumps(labels))
|
||||
ret = {}
|
||||
ret['allowed_labels'] = tenant.allowed_labels or []
|
||||
ret['disallowed_labels'] = tenant.disallowed_labels or []
|
||||
job.sendWorkComplete(json.dumps(ret))
|
||||
|
||||
def handle_pipeline_list(self, job):
|
||||
args = json.loads(job.arguments)
|
||||
|
@ -736,15 +736,33 @@ class ZuulWebAPI(object):
|
||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||
def labels(self, tenant):
|
||||
job = self.rpc.submitJob('zuul:allowed_labels_get', {'tenant': tenant})
|
||||
allowed_labels = json.loads(job.data[0])
|
||||
if allowed_labels is None:
|
||||
data = json.loads(job.data[0])
|
||||
if data is None:
|
||||
raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant)
|
||||
# TODO(jeblair): The first case can be removed after 3.16.0 is
|
||||
# released.
|
||||
if isinstance(data, list):
|
||||
allowed_labels = data
|
||||
disallowed_labels = []
|
||||
else:
|
||||
allowed_labels = data['allowed_labels']
|
||||
disallowed_labels = data['disallowed_labels']
|
||||
labels = set()
|
||||
for launcher in self.zk.getRegisteredLaunchers():
|
||||
for label in launcher.supported_labels:
|
||||
if not allowed_labels or (
|
||||
[True for allowed_label in allowed_labels if
|
||||
re2.match(allowed_label, label)]):
|
||||
allowed = True
|
||||
if allowed_labels:
|
||||
allowed = False
|
||||
for pattern in allowed_labels:
|
||||
if re2.match(pattern, label):
|
||||
allowed = True
|
||||
break
|
||||
if disallowed_labels:
|
||||
for pattern in disallowed_labels:
|
||||
if re2.match(pattern, label):
|
||||
allowed = False
|
||||
break
|
||||
if allowed:
|
||||
labels.add(label)
|
||||
ret = [{'name': label} for label in sorted(labels)]
|
||||
resp = cherrypy.response
|
||||
|
Loading…
x
Reference in New Issue
Block a user