Add tenant as a config option
Allow user to set a tenant in one of the configuration file's subsections, so that they don't have to provide the argument to the CLI all the time. Fix the API builds() call not building the API URL properly for a white-labeled root URL. Change-Id: Ib3b8b2be07ed580ac9f48738b9157a5d0e4d5e70
This commit is contained in:
parent
cd375aedeb
commit
49451a814a
@ -9,6 +9,7 @@ the ``url`` attribute set. The optional ``verify_ssl`` can be set to False to
|
||||
disable SSL verifications when connecting to Zuul (defaults to True). An
|
||||
authentication token can also be stored in the configuration file under the attribute
|
||||
``auth_token`` to avoid passing the token in the clear on the command line.
|
||||
A default tenant can also be set with the ``tenant`` attribute.
|
||||
|
||||
Here is an example of a ``.zuul.conf`` file that can be used with zuul-client:
|
||||
|
||||
|
@ -6,5 +6,10 @@
|
||||
|
||||
[example]
|
||||
url=https://example.com/zuul/
|
||||
# A default tenant can be specified in the configuration file; it will be
|
||||
# overriden by the --tenant argument if set.
|
||||
# Note that specifying a tenant is not necessary with a white-labeled tenant API URL,
|
||||
# see https://zuul-ci.org/docs/zuul/howtos/installation.html#white-labeled-tenant
|
||||
tenant=mytenant
|
||||
# verify_ssl=False
|
||||
auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add a "tenant" field in the configuration file, so that a user doesn't have
|
||||
to specify this argument in the CLI when using a predefined configuration.
|
@ -93,9 +93,9 @@ class TestApi(BaseTestCase):
|
||||
'node_hold_expiration': 3600}
|
||||
)
|
||||
self.assertEqual(True, ah)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
ah = client.autohold(
|
||||
'tenant', 'project', 'job', 1, None, 'reason', 1, 3600)
|
||||
'tenant1', 'project', 'job', 1, None, 'reason', 1, 3600)
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/project/project/autohold',
|
||||
json={'reason': 'reason',
|
||||
@ -132,7 +132,7 @@ class TestApi(BaseTestCase):
|
||||
client.session.get.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold')
|
||||
self.assertEqual(fakejson, ahl)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
ahl = client.autohold_list('tenant1')
|
||||
client.session.get.assert_called_with(
|
||||
'https://fake.zuul/api/autohold')
|
||||
@ -162,7 +162,7 @@ class TestApi(BaseTestCase):
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold/123'
|
||||
)
|
||||
self.assertEqual(True, ahd)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
ahd = client.autohold_delete(123, 'tenant1')
|
||||
client.session.delete.assert_called_with(
|
||||
'https://fake.zuul/api/autohold/123'
|
||||
@ -194,7 +194,7 @@ class TestApi(BaseTestCase):
|
||||
client.session.get.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold/123')
|
||||
self.assertEqual(fakejson, ahl)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
ahl = client.autohold_info(tenant='tenant1', id=123)
|
||||
client.session.get.assert_called_with(
|
||||
'https://fake.zuul/api/autohold/123')
|
||||
@ -226,7 +226,7 @@ class TestApi(BaseTestCase):
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, enq)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
enq = client.enqueue('tenant1', 'check', 'project1', '1,1')
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/project/project1/enqueue',
|
||||
@ -265,7 +265,7 @@ class TestApi(BaseTestCase):
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, enq_ref)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
enq_ref = client.enqueue_ref(
|
||||
'tenant1', 'check', 'project1', 'refs/heads/stable', '0', '0')
|
||||
client.session.post.assert_called_with(
|
||||
@ -316,7 +316,7 @@ class TestApi(BaseTestCase):
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, deq)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
deq = client.dequeue('tenant1', 'check', 'project1', change='1,1')
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/project/project1/dequeue',
|
||||
@ -359,7 +359,7 @@ class TestApi(BaseTestCase):
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, prom)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
prom = client.promote('tenant1', 'check', ['1,1', '2,1'])
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/promote',
|
||||
@ -394,7 +394,7 @@ GuS6/ewjS+arA1Iyeg/IxmECAwEAAQ==
|
||||
'https://fake.zuul/api/tenant/tenant1/key/project1.pub'
|
||||
)
|
||||
self.assertEqual(pubkey, key)
|
||||
client.info_ = {'tenant': 'scoped'}
|
||||
client.info_ = {'tenant': 'tenant1'}
|
||||
key = client.get_key('tenant1', 'project1')
|
||||
client.session.get.assert_called_with(
|
||||
'https://fake.zuul/api/key/project1.pub'
|
||||
|
@ -63,6 +63,61 @@ class TestCmd(BaseTestCase):
|
||||
'--reason', 'some reason',
|
||||
'--node-hold-expiration', '3600'])
|
||||
|
||||
def test_use_conf(self):
|
||||
"""Test that CLI can use a configuration file"""
|
||||
ZC = ZuulClient()
|
||||
with tempfile.NamedTemporaryFile(delete=False) as conf_file:
|
||||
conf_file.write(
|
||||
b"""
|
||||
[confA]
|
||||
url=https://my.fake.zuul/
|
||||
tenant=mytenant
|
||||
auth_token=mytoken
|
||||
verify_ssl=True"""
|
||||
)
|
||||
conf_file.close()
|
||||
with patch("requests.Session") as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.post = MagicMock(
|
||||
return_value=FakeRequestResponse(200, True)
|
||||
)
|
||||
session.get = MagicMock(side_effect=mock_get())
|
||||
exit_code = ZC._main(
|
||||
[
|
||||
"-c",
|
||||
conf_file.name,
|
||||
"--use-config",
|
||||
"confA",
|
||||
"autohold",
|
||||
"--project",
|
||||
"project1",
|
||||
"--job",
|
||||
"job1",
|
||||
"--change",
|
||||
"3",
|
||||
"--reason",
|
||||
"some reason",
|
||||
"--node-hold-expiration",
|
||||
"3600",
|
||||
]
|
||||
)
|
||||
self.assertEqual("mytoken", ZC.get_client().auth_token)
|
||||
self.assertEqual(True, ZC.get_client().verify)
|
||||
session.post.assert_called_with(
|
||||
"https://my.fake.zuul/api/tenant/mytenant/"
|
||||
"project/project1/autohold",
|
||||
json={
|
||||
"reason": "some reason",
|
||||
"count": 1,
|
||||
"job": "job1",
|
||||
"change": "3",
|
||||
"ref": "",
|
||||
"node_hold_expiration": 3600,
|
||||
},
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
os.unlink(conf_file.name)
|
||||
|
||||
def test_tenant_scoping_errors(self):
|
||||
"""Test the right uses of --tenant"""
|
||||
ZC = ZuulClient()
|
||||
@ -98,8 +153,11 @@ class TestCmd(BaseTestCase):
|
||||
session.get = MagicMock(
|
||||
side_effect=mock_get()
|
||||
)
|
||||
with self.assertRaisesRegex(Exception,
|
||||
'--tenant argument is required'):
|
||||
with self.assertRaisesRegex(
|
||||
Exception,
|
||||
"the --tenant argument or the 'tenant' field "
|
||||
"in the configuration file is required",
|
||||
):
|
||||
ZC._main(['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', ] + args)
|
||||
session.get = MagicMock(
|
||||
@ -480,8 +538,11 @@ class TestCmd(BaseTestCase):
|
||||
'builds', '--tenant', 'tenant1', '--voting', '--non-voting'])
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.post = MagicMock(
|
||||
return_value=FakeRequestResponse(200, {}))
|
||||
session.get = MagicMock(
|
||||
side_effect=mock_get(
|
||||
MagicMock(return_value=FakeRequestResponse(200, []))
|
||||
)
|
||||
)
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul', 'builds',
|
||||
'--pipeline', 'gate',
|
||||
|
@ -80,6 +80,17 @@ class ZuulRESTClient(object):
|
||||
raise ZuulRESTException(
|
||||
'Unknown error code %s: "%s"' % (req.status_code, e))
|
||||
|
||||
def _check_scope(self, tenant):
|
||||
scope = self.info.get("tenant", None)
|
||||
if (
|
||||
(scope is not None)
|
||||
and (tenant not in [None, ""])
|
||||
and scope != tenant
|
||||
):
|
||||
raise Exception(
|
||||
"Tenant %s and tenant scope %s do not match" % (tenant, scope)
|
||||
)
|
||||
|
||||
def autohold(self, tenant, project, job, change, ref,
|
||||
reason, count, node_hold_expiration):
|
||||
if not self.auth_token:
|
||||
@ -91,6 +102,7 @@ class ZuulRESTClient(object):
|
||||
"ref": ref,
|
||||
"node_hold_expiration": node_hold_expiration}
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'project/%s/autohold' % project
|
||||
else:
|
||||
suffix = 'tenant/%s/project/%s/autohold' % (tenant, project)
|
||||
@ -103,6 +115,7 @@ class ZuulRESTClient(object):
|
||||
|
||||
def autohold_list(self, tenant):
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'autohold'
|
||||
else:
|
||||
suffix = 'tenant/%s/autohold' % tenant
|
||||
@ -119,6 +132,7 @@ class ZuulRESTClient(object):
|
||||
if not self.auth_token:
|
||||
raise Exception('Auth Token required')
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'autohold/%s' % id
|
||||
else:
|
||||
suffix = 'tenant/%s/autohold/%s' % (tenant, id)
|
||||
@ -132,6 +146,7 @@ class ZuulRESTClient(object):
|
||||
|
||||
def autohold_info(self, id, tenant):
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'autohold/%s' % id
|
||||
else:
|
||||
suffix = 'tenant/%s/autohold/%s' % (tenant, id)
|
||||
@ -150,6 +165,7 @@ class ZuulRESTClient(object):
|
||||
args = {"change": change,
|
||||
"pipeline": pipeline}
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'project/%s/enqueue' % project
|
||||
else:
|
||||
suffix = 'tenant/%s/project/%s/enqueue' % (tenant, project)
|
||||
@ -168,6 +184,7 @@ class ZuulRESTClient(object):
|
||||
"newrev": newrev,
|
||||
"pipeline": pipeline}
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'project/%s/enqueue' % project
|
||||
else:
|
||||
suffix = 'tenant/%s/project/%s/enqueue' % (tenant, project)
|
||||
@ -189,6 +206,7 @@ class ZuulRESTClient(object):
|
||||
else:
|
||||
raise Exception('need change OR ref')
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'project/%s/dequeue' % project
|
||||
else:
|
||||
suffix = 'tenant/%s/project/%s/dequeue' % (tenant, project)
|
||||
@ -205,6 +223,7 @@ class ZuulRESTClient(object):
|
||||
args = {'pipeline': pipeline,
|
||||
'changes': change_ids}
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'promote'
|
||||
else:
|
||||
suffix = 'tenant/%s/promote' % tenant
|
||||
@ -217,6 +236,7 @@ class ZuulRESTClient(object):
|
||||
|
||||
def get_key(self, tenant, project):
|
||||
if self.info.get('tenant'):
|
||||
self._check_scope(tenant)
|
||||
suffix = 'key/%s.pub' % project
|
||||
else:
|
||||
suffix = 'tenant/%s/key/%s.pub' % (tenant, project)
|
||||
@ -241,9 +261,12 @@ class ZuulRESTClient(object):
|
||||
params['limit'] = 50
|
||||
if 'skip' not in params:
|
||||
params['skip'] = 0
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/builds' % tenant)
|
||||
if self.info.get("tenant"):
|
||||
self._check_scope(tenant)
|
||||
suffix = "builds"
|
||||
else:
|
||||
suffix = "tenant/%s/builds" % tenant
|
||||
url = urllib.parse.urljoin(self.base_url, suffix)
|
||||
req = self.session.get(url, params=kwargs)
|
||||
self._check_request_status(req)
|
||||
return req.json()
|
||||
|
@ -173,16 +173,19 @@ class ZuulClient():
|
||||
sys.exit(1)
|
||||
|
||||
def _check_tenant_scope(self, client):
|
||||
tenant_scope = client.info.get('tenant', None)
|
||||
if self.args.tenant != '':
|
||||
if tenant_scope is not None and tenant_scope != self.args.tenant:
|
||||
tenant_scope = client.info.get("tenant", None)
|
||||
tenant = self.tenant()
|
||||
if tenant != "":
|
||||
if tenant_scope is not None and tenant_scope != tenant:
|
||||
raise ArgumentException(
|
||||
'Error: Zuul API URL %s is '
|
||||
'scoped to tenant "%s"' % (client.base_url, tenant_scope))
|
||||
"Error: Zuul API URL %s is "
|
||||
'scoped to tenant "%s"' % (client.base_url, tenant_scope)
|
||||
)
|
||||
else:
|
||||
if tenant_scope is None:
|
||||
raise ArgumentException(
|
||||
"Error: the --tenant argument is required"
|
||||
"Error: the --tenant argument or the 'tenant' "
|
||||
"field in the configuration file is required"
|
||||
)
|
||||
|
||||
def add_autohold_subparser(self, subparsers):
|
||||
@ -223,7 +226,7 @@ class ZuulClient():
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
r = client.autohold(
|
||||
tenant=self.args.tenant,
|
||||
tenant=self.tenant(),
|
||||
project=self.args.project,
|
||||
job=self.args.job,
|
||||
change=self.args.change,
|
||||
@ -246,7 +249,7 @@ class ZuulClient():
|
||||
def autohold_delete(self):
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
return client.autohold_delete(self.args.id, self.args.tenant)
|
||||
return client.autohold_delete(self.args.id, self.tenant())
|
||||
|
||||
def add_autohold_info_subparser(self, subparsers):
|
||||
cmd_autohold_info = subparsers.add_parser(
|
||||
@ -261,7 +264,7 @@ class ZuulClient():
|
||||
def autohold_info(self):
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
request = client.autohold_info(self.args.id, self.args.tenant)
|
||||
request = client.autohold_info(self.args.id, self.tenant())
|
||||
|
||||
if not request:
|
||||
print("Autohold request not found")
|
||||
@ -292,7 +295,7 @@ class ZuulClient():
|
||||
def autohold_list(self):
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
autohold_requests = client.autohold_list(tenant=self.args.tenant)
|
||||
autohold_requests = client.autohold_list(tenant=self.tenant())
|
||||
|
||||
if not autohold_requests:
|
||||
print("No autohold requests found")
|
||||
@ -335,7 +338,7 @@ class ZuulClient():
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
r = client.enqueue(
|
||||
tenant=self.args.tenant,
|
||||
tenant=self.tenant(),
|
||||
pipeline=self.args.pipeline,
|
||||
project=self.args.project,
|
||||
change=self.args.change)
|
||||
@ -370,7 +373,7 @@ class ZuulClient():
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
r = client.enqueue_ref(
|
||||
tenant=self.args.tenant,
|
||||
tenant=self.tenant(),
|
||||
pipeline=self.args.pipeline,
|
||||
project=self.args.project,
|
||||
ref=self.args.ref,
|
||||
@ -399,7 +402,7 @@ class ZuulClient():
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
r = client.dequeue(
|
||||
tenant=self.args.tenant,
|
||||
tenant=self.tenant(),
|
||||
pipeline=self.args.pipeline,
|
||||
project=self.args.project,
|
||||
change=self.args.change,
|
||||
@ -422,7 +425,7 @@ class ZuulClient():
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
r = client.promote(
|
||||
tenant=self.args.tenant,
|
||||
tenant=self.tenant(),
|
||||
pipeline=self.args.pipeline,
|
||||
change_ids=self.args.changes)
|
||||
return r
|
||||
@ -464,6 +467,22 @@ class ZuulClient():
|
||||
client = ZuulRESTClient(server, verify, auth_token)
|
||||
return client
|
||||
|
||||
def tenant(self):
|
||||
if self.args.tenant == "":
|
||||
if self.config is not None:
|
||||
config_tenant = ""
|
||||
conf_sections = self.config.sections()
|
||||
if (
|
||||
self.args.zuul_config
|
||||
and self.args.zuul_config in conf_sections
|
||||
):
|
||||
zuul_conf = self.args.zuul_config
|
||||
config_tenant = get_default(
|
||||
self.config, zuul_conf, "tenant", ""
|
||||
)
|
||||
return config_tenant
|
||||
return self.args.tenant
|
||||
|
||||
def add_encrypt_subparser(self, subparsers):
|
||||
cmd_encrypt = subparsers.add_parser(
|
||||
'encrypt', help='Encrypt a secret to be used in a project\'s jobs')
|
||||
@ -534,7 +553,7 @@ class ZuulClient():
|
||||
else:
|
||||
client = self.get_client()
|
||||
self._check_tenant_scope(client)
|
||||
key = client.get_key(self.args.tenant, self.args.project)
|
||||
key = client.get_key(self.tenant(), self.args.project)
|
||||
pubkey_file.write(str.encode(key))
|
||||
pubkey_file.close()
|
||||
self.log.debug('Calling openssl')
|
||||
@ -654,7 +673,7 @@ class ZuulClient():
|
||||
if self.args.held:
|
||||
filters['held'] = True
|
||||
client = self.get_client()
|
||||
builds = client.builds(tenant=self.args.tenant, **filters)
|
||||
builds = client.builds(tenant=self.tenant(), **filters)
|
||||
table = prettytable.PrettyTable(
|
||||
field_names=[
|
||||
'ID', 'Job', 'Project', 'Branch', 'Pipeline', 'Change or Ref',
|
||||
|
Loading…
Reference in New Issue
Block a user