diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..70972a8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +build: + os: "ubuntu-22.04" + tools: + python: "3.11" + +formats: + - pdf + - epub diff --git a/HISTORY.md b/HISTORY.md index e0a92fa..3548ad0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,12 @@ Release History =============== +1.15.0 (2023-08-30) +------------------- +- Support to manage domains for credential + 1.14.1 (2023-07-24) ------------------- -- Support additional session headers +- Support additional session headers 1.14 (2022-11-28) ----------------- @@ -31,7 +35,7 @@ Release History - set dependency version 1.9.1 (2022-03-29) ------------------ +------------------ - change OrganizationFolder to inherit from WorkflowMultiBranchProject - `Jenkins.get_job` return consistent result @@ -59,7 +63,7 @@ Release History - bugfix for `queue.get_build` 1.5.1 (2021-05-11) ------------------ +------------------ - Bugfix for nodes.iter_builds 1.5 (2021-04-29) diff --git a/api4jenkins/__init__.py b/api4jenkins/__init__.py index 41fe8bd..c2c9bda 100644 --- a/api4jenkins/__init__.py +++ b/api4jenkins/__init__.py @@ -284,14 +284,13 @@ def plugins(self): @property def version(self): '''Version of Jenkins''' - return self.handle_req('GET', '').headers['X-Jenkins'] + return self.handle_req('HEAD', '').headers['X-Jenkins'] @property def credentials(self): '''An object for managing credentials. see :class:`Credentials `''' - return Credentials(self, - f'{self.url}credentials/store/system/domain/_/') + return Credentials(self, f'{self.url}credentials/store/system/') @property def views(self): diff --git a/api4jenkins/__version__.py b/api4jenkins/__version__.py index 9e0ac10..2a553a2 100644 --- a/api4jenkins/__version__.py +++ b/api4jenkins/__version__.py @@ -1,5 +1,5 @@ # encoding: utf-8 -__version__ = '1.14.1' +__version__ = '1.15.0' __title__ = 'api4jenkins' __description__ = 'Jenkins Python Client' __url__ = 'https://github.com/joelee2012/api4jenkins' diff --git a/api4jenkins/credential.py b/api4jenkins/credential.py index 47664fb..e5278f1 100644 --- a/api4jenkins/credential.py +++ b/api4jenkins/credential.py @@ -7,11 +7,34 @@ class Credentials(Item): + def get(self, name): + for key in self.api_json(tree='domains[urlName]')['domains'].keys(): + if key == name: + return Domain(self.jenkins, f'{self.url}domain/{key}/') + return None + + def create(self, xml): + self.handle_req('POST', 'createDomain', + headers=self.headers, data=xml) + + def __iter__(self): + for key in self.api_json(tree='domains[urlName]')['domains'].keys(): + yield Domain(self.jenkins, f'{self.url}domain/{key}/') + + def __getitem__(self, name): + return self.get(name) + + @property + def global_domain(self): + return self['_'] + + +class Domain(Item, ConfigurationMixIn, DeletionMixIn): + def get(self, id): for item in self.api_json(tree='credentials[id]')['credentials']: if item['id'] == id: - return Credential(self.jenkins, - f'{self.url}credential/{id}/') + return Credential(self.jenkins, f'{self.url}credential/{id}/') return None def create(self, xml): @@ -20,8 +43,10 @@ def create(self, xml): def __iter__(self): for item in self.api_json(tree='credentials[id]')['credentials']: - yield Credential(self.jenkins, - f'{self.url}credential/{item["id"]}/') + yield Credential(self.jenkins, f'{self.url}credential/{item["id"]}/') + + def __getitem__(self, id): + return self.get(id) class Credential(Item, ConfigurationMixIn, DeletionMixIn): diff --git a/api4jenkins/job.py b/api4jenkins/job.py index c4e83d3..a14b2bb 100644 --- a/api4jenkins/job.py +++ b/api4jenkins/job.py @@ -96,8 +96,7 @@ def views(self): @property def credentials(self): - return Credentials(self.jenkins, - f'{self.url}credentials/store/folder/domain/_/') + return Credentials(self.jenkins, f'{self.url}credentials/store/folder/') def __iter__(self): yield from self.iter() diff --git a/docs/source/conf.py b/docs/source/conf.py index 6d7f464..e70915e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,12 +15,12 @@ sys.path.insert(0, os.path.abspath('../../')) # -- Project information ----------------------------------------------------- +import api4jenkins project = 'api4jenkins' -copyright = '2021, Joe Lee' +copyright = '2023, Joe Lee' author = 'Joe Lee' -import api4jenkins # The full version, including alpha/beta/rc tags release = api4jenkins.__version__ diff --git a/docs/source/index.rst b/docs/source/index.rst index dd4759f..fa6d258 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,7 +19,8 @@ Jenkins Python Client -`Python3 `_ client library for `Jenkins API `_. +`Python3 `_ client library for +`Jenkins API `_. Features @@ -36,7 +37,8 @@ Features Quick start ---------------------------------------- -Here is an example to create and build job, then monitor progressive output until it's done. +Here is an example to create and build job, then monitor progressive output +until it's done. >>> from api4jenkins import Jenkins @@ -86,9 +88,9 @@ Here is an example to create and build job, then monitor progressive output unti :maxdepth: 2 :caption: Contents: - user/install.rst - user/example.rst - user/api.rst + user/install + user/example + user/api Indices and tables diff --git a/docs/source/user/api.rst b/docs/source/user/api.rst index 2996e28..f763664 100644 --- a/docs/source/user/api.rst +++ b/docs/source/user/api.rst @@ -73,11 +73,6 @@ API reference :undoc-members: :inherited-members: -.. automodule:: api4jenkins.utils - :members: - :undoc-members: - :inherited-members: - .. automodule:: api4jenkins.requester :members: :undoc-members: diff --git a/docs/source/user/example.rst b/docs/source/user/example.rst index 407982c..a56de58 100644 --- a/docs/source/user/example.rst +++ b/docs/source/user/example.rst @@ -77,8 +77,7 @@ Call `j.dynamic_attrs` to get the dynamic attributes of an Item:: ['_class', 'mode', 'node_description', 'node_name', 'num_executors', 'description', 'quieting_down', 'slave_agent_port', 'use_crumbs', 'use_security'] With Jenkins object you can manage many Items including: `Job`_, `Credential`_, - `Node`_, `View`_, `Queue`_, `Plugin`_, `System`_ and so on. let's start with -`Job`_ management. + `Node`_, `View`_, `Queue`_, `Plugin`_, `System`_ and so on. let's start with `Job`_ management. create job with `j.create_job()`:: @@ -521,8 +520,8 @@ and abort input:: >>> build.get_pending_input().abort() -WorkflowRun supports `archive artfacts `_, - you can also process with api4jenkins:: +`WorkflowRun` supports `archive artfacts `_, +you can also process with api4jenkins save file you interest:: @@ -537,33 +536,38 @@ save artifacts as zip:: Credential ------------- -Credential is for saving secret data, `api4jenkins` support to manage system -and folder based credentials, all credentials must be in default domain(_). -more detail can be found: `using credentials `_ -and `credentials plugin user.doc `_ +Credential is for saving secret data, `api4jenkins` support to manage :class:`Jenkins ` +and :class:`Folder ` based domains and credentials. + +create/get domain:: + + >>> xml = ''' + ... testing + ... Credentials for use against the *.test.example.com hosts + ... + ... + ... *.test.example.com + ... + ... + ... + ... ''' + >>> folder.credentials.create(xml) + >>> domain = folder.credentials.get('testing') -create/get folder based credential:: +.. note:: - >>> xml = ''' - ... user-id - ... user-name - ... user-password - ... user id for testing - ... ''' - >>> folder.credentials.create(xml) - >>> credential = folder.credentials.get('user-id') + :class:`global_domain ` is shortcut of domain (_) -create system based credential:: +create/get credential in domain:: >>> xml = ''' - ... GLOBAL ... user-id ... user-name ... user-password ... user id for testing ... ''' - >>> j.credentials.create(xml) - >>> credential = j.credentials.get('user-id') + >>> domain.create(xml) + >>> credential = domain.get('user-id') get/update configuration of credential:: @@ -576,16 +580,20 @@ delete credential:: >>> credential.exists() False -iterate folder credentials:: +iterate domain in `Folder` or `Jenkins`:: - >>> for c in folder.credentials: - ... print(c) + >>> for domain in folder: + ... print(domain) -iterate system credentials:: +iterate credentials in `Domain`:: - >>> for c in j.credentials: + >>> for c in domain: ... print(c) +.. seealso:: + + more detail can be found: `using credentials `_ + and `credentials plugin user.doc `_ View ------- @@ -801,9 +809,9 @@ run groovy script >>> j.system.run_script('println "this is test"') -it also supports to manage `jcasc `_ :: +it also supports to manage `jcasc `_ -to reload jcase +to reload jcasc:: >>> j.system.reload_jcasc() @@ -1012,7 +1020,7 @@ iterate all case in test report and filter by status :: ... print(case) Coverage report ------ +--------------- Access coverage report generated by `JaCoCo `_, avaliable types are 'branchCoverage', 'classCoverage', 'complexityScore', 'instructionCoverage', 'lineCoverage', 'methodCoverage':: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 997f1a1..8fa3781 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -65,9 +65,12 @@ def folder(jenkins): @pytest.fixture(autouse=True) -def setup_folder(jenkins, folder_xml): +def setup_folder(jenkins, folder_xml, credential_xml): jenkins.create_job('Level1_Folder1', folder_xml) jenkins.create_job('Level1_Folder1/Level2_Folder1', folder_xml) + folder = jenkins.get_job('Level1_Folder1') + folder.credentials.global_domain.create(credential_xml) + folder.credentials.create(load_xml('domain.xml')) yield jenkins.delete_job('Level1_Folder1') diff --git a/tests/integration/test_credential.py b/tests/integration/test_credential.py new file mode 100644 index 0000000..22c5032 --- /dev/null +++ b/tests/integration/test_credential.py @@ -0,0 +1,15 @@ +class TestCredentials: + def test_credentials_iter(self, folder): + assert len(list(folder.credentials)) == 2 + + def test_credential_get(self, folder): + assert folder.credentials.global_domain is not None + assert folder.credentials.get('not exists') is None + + def test_domain_get(self, folder): + c = folder.credentials.global_domain['user-id'] + assert c.id == 'user-id' + assert folder.credentials.global_domain['not exists'] is None + + def test_domain_iter(self, folder): + assert len(list(folder.credentials.global_domain)) == 1 \ No newline at end of file diff --git a/tests/integration/test_jenkins.py b/tests/integration/test_jenkins.py index 2d42f39..60a7505 100644 --- a/tests/integration/test_jenkins.py +++ b/tests/integration/test_jenkins.py @@ -70,14 +70,14 @@ def test_iter_jobs(self, jenkins): assert len(list(jenkins)) == 1 assert len(list(jenkins(2))) == 2 - def test_credential(self, jenkins, credential_xml): - assert len(list(jenkins.credentials)) == 0 - jenkins.credentials.create(credential_xml) - assert len(list(jenkins.credentials)) == 1 - c = jenkins.credentials.get('user-id') - assert c.id == 'user-id' - c.delete() - assert c.exists() == False + # def test_credential(self, jenkins, credential_xml): + # assert len(list(jenkins.credentials)) == 1 + # jenkins.credentials.global_domain.create(credential_xml) + # assert len(list(jenkins.credentials.global_domain)) == 1 + # c = jenkins.credentials.get('user-id') + # assert c.id == 'user-id' + # c.delete() + # assert c.exists() == False def test_view(self, jenkins, view_xml): assert len(list(jenkins.views)) == 1 diff --git a/tests/integration/tests_data/domain.xml b/tests/integration/tests_data/domain.xml new file mode 100644 index 0000000..820a927 --- /dev/null +++ b/tests/integration/tests_data/domain.xml @@ -0,0 +1,10 @@ + + testing + Credentials for use against the *.test.example.com hosts + + + *.test.example.com + + + + \ No newline at end of file diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 50d5967..a02dd29 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -8,7 +8,7 @@ from api4jenkins.job import WorkflowJob, WorkflowMultiBranchProject from api4jenkins.build import WorkflowRun, FreeStyleBuild from api4jenkins import Credentials -from api4jenkins.credential import Credential +from api4jenkins.credential import Credential, Domain from api4jenkins import PluginsManager from api4jenkins import Queue from api4jenkins.queue import QueueItem @@ -31,6 +31,8 @@ def _api_json(self, tree='', depth=0): elif isinstance(self, FreeStyleBuild): return load_json('run/freestylebuild.json') elif isinstance(self, Credentials): + return load_json('credential/domains.json') + elif isinstance(self, Domain): return load_json('credential/credentials.json') elif isinstance(self, Credential): return load_json('credential/user_psw.json') diff --git a/tests/unit/test_credential.py b/tests/unit/test_credential.py index 47f1ecb..ef68b10 100644 --- a/tests/unit/test_credential.py +++ b/tests/unit/test_credential.py @@ -1,23 +1,44 @@ # encoding: utf-8 import pytest -from api4jenkins.credential import Credential +from api4jenkins.credential import Credential, Domain + + +@pytest.fixture +def global_domain(jenkins): + return jenkins.credentials.global_domain class TestCredentials: - @pytest.mark.parametrize('id_, obj', [('not exist', type(None)), ('test-user', Credential)]) - def test_get(self, jenkins, id_, obj): - assert isinstance(jenkins.credentials.get(id_), obj) + def test_get(self, jenkins): + assert isinstance(jenkins.credentials['_'], Domain) + assert jenkins.credentials['x'] is None def test_create(self, jenkins, mock_resp): - req_url = f'{jenkins.credentials.url}createCredentials' + req_url = f'{jenkins.credentials.url}createDomain' mock_resp.add('POST', req_url) jenkins.credentials.create('xml') assert mock_resp.calls[0].request.url == req_url def test_iter(self, jenkins): - creds = list(jenkins.credentials) + assert len(list(jenkins.credentials)) == 3 + + +class TestDomain: + + @pytest.mark.parametrize('id_, obj', [('not exist', type(None)), ('test-user', Credential)]) + def test_get(self, global_domain, id_, obj): + assert isinstance(global_domain[id_], obj) + + def test_create(self, global_domain, mock_resp): + req_url = f'{global_domain.url}createCredentials' + mock_resp.add('POST', req_url) + global_domain.create('xml') + assert mock_resp.calls[0].request.url == req_url + + def test_iter(self, global_domain): + creds = list(global_domain) assert len(creds) == 2 assert all([isinstance(c, Credential) for c in creds]) diff --git a/tests/unit/test_jenkins.py b/tests/unit/test_jenkins.py index b6f5cb5..b5ee236 100644 --- a/tests/unit/test_jenkins.py +++ b/tests/unit/test_jenkins.py @@ -24,7 +24,7 @@ def test_init_with_token(self, jenkins, monkeypatch, mock_resp): assert j._token.value == data['tokenValue'] def test_version(self, jenkins, mock_resp): - mock_resp.add('GET', jenkins.url, headers={'X-Jenkins': '1.2.3'}) + mock_resp.add('HEAD', jenkins.url, headers={'X-Jenkins': '1.2.3'}) assert jenkins.version == '1.2.3' def test_attrs(self, jenkins): diff --git a/tests/unit/tests_data/credential/domains.json b/tests/unit/tests_data/credential/domains.json new file mode 100644 index 0000000..ca866f3 --- /dev/null +++ b/tests/unit/tests_data/credential/domains.json @@ -0,0 +1,17 @@ +{ + "_class": "com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider$FolderCredentialsProperty$CredentialsStoreActionImpl", + "domains": { + "_": { + "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", + "urlName": "_" + }, + "production": { + "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", + "urlName": "production" + }, + "testing": { + "_class": "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper", + "urlName": "testing" + } + } +} \ No newline at end of file