├── .ansible-lint ├── .gitignore ├── .zuul.yaml ├── LICENSE ├── README.rst ├── bindep.txt ├── doc ├── requirements.txt └── source │ ├── conf.py │ └── index.rst ├── docs └── .keep ├── galaxy.yml ├── galaxy.yml.in ├── meta └── runtime.yml ├── playbooks ├── .keep └── run.yaml ├── plugins ├── doc_fragments │ ├── git.py │ ├── gitea.py │ └── github.py ├── module_utils │ ├── git.py │ ├── gitea.py │ └── github.py └── modules │ ├── gitea_org_repository.py │ ├── github_org_members.py │ ├── github_org_repository.py │ ├── github_org_team.py │ ├── github_org_teams.py │ ├── members.py │ ├── repositories.py │ └── teams.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── test_org ├── orgs │ └── opentelekomcloud-gitcontrol-test │ │ ├── people │ │ ├── dismissed_members.yml │ │ └── members.yml │ │ ├── repositories │ │ └── test.yaml │ │ └── teams │ │ ├── dismissed_members.yml │ │ └── members.yml └── templates │ └── default.yml ├── tests ├── .keep ├── integration │ ├── requirements.txt │ └── targets │ │ ├── .keep │ │ ├── gitea_org_repository │ │ └── tasks │ │ │ └── main.yaml │ │ ├── github_org_members │ │ └── tasks │ │ │ └── main.yaml │ │ ├── github_org_repository │ │ ├── defaults │ │ │ └── main.yaml │ │ └── tasks │ │ │ └── main.yaml │ │ ├── github_org_team │ │ └── tasks │ │ │ └── main.yaml │ │ ├── github_org_teams │ │ └── tasks │ │ │ └── main.yaml │ │ ├── members │ │ └── tasks │ │ │ └── main.yaml │ │ ├── repositories │ │ └── tasks │ │ │ └── main.yaml │ │ └── teams │ │ └── tasks │ │ └── main.yaml ├── playbooks │ ├── integration_config.yml.j2 │ └── pre.yaml └── sanity │ └── requirements.txt ├── tools └── build.py └── tox.ini /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - tests/playbooks 4 | - .zuul.yaml 5 | skip_list: 6 | - args[module] 7 | - name[casing] 8 | - name[play] 9 | - galaxy[no-changelog] 10 | - galaxy[tags] 11 | - galaxy[version-incorrect] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | .tox 4 | build_artifact 5 | ansible_collections 6 | FILES.json 7 | MANIFEST.json 8 | importer_result.json 9 | **.swp 10 | 11 | *.tar.gz 12 | doc/build 13 | tmp 14 | tests/output 15 | tests/integration/integration_config.yml 16 | tests/integration/inventory 17 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable 2 | --- 3 | - job: 4 | name: gitcontrol-test-integration 5 | parent: ansible-collection-test-integration 6 | description: | 7 | Execute ansible-test integration for the collection 8 | vars: 9 | ansible_test_test_command: "integration" 10 | ansible_test_no_temp_unicode: true 11 | files: 12 | - ^plugins/module_utils/git.py 13 | - ^plugins/module_utils/github.py 14 | - ^plugins/modules/github* 15 | - ^tests/integration/targets/github* 16 | pre-run: tests/playbooks/pre.yaml 17 | secrets: 18 | - secret: gitcontrol-secret 19 | name: gitcontrol 20 | 21 | - secret: 22 | name: gitcontrol-secret 23 | data: 24 | # github token belongs to otcecocert user. Use of any more powerful account 25 | # is discouraged. Token permissions: 26 | # admin:org, read:user, repo, user:email, delete_repo 27 | github_token: !encrypted/pkcs1-oaep 28 | - eGDYtNbTaziv8creqr54Vs//UTP6Fl7KFi32FDHHrF7ClmAB2uM8zT/03B2ixslM5iWp2 29 | rPXQyur6koQF32M3jnUOiH0F1HI0MLfwO93Gmi76xgfqez9vQ6fjuTahMWppkuLt7Eq7Y 30 | XVbk4fEZ9TdqoNBGBkMIIrtfQJAa5xEwOg4Al+5CsNzrBsDHcppgoGyTEdx5zG1QwfXln 31 | NV5N6yFjvffv0WuISqzRCpB2YIZt0aMzYPez0beyTqLe9K/FTkC+CCUJ5ONcKL8tLbs77 32 | 2XG13526PaNy8O8ksMEaEbZ6MoXxONJN6s9Rg5p+TkeUNnnqJrdaUDCqorCRCHOYQQeiS 33 | cDIeBWudEHwnB8BPaiQkpZNHndnAoGCPQ2o0M/UiW/bp1GDprWIbnctkXVFCfdAt2ZhFB 34 | oNf7/K/srqj4YwqVY77B0PjILk/6G/qv2/yBTf/4jP8mOR1YE7IrSVJLj5fyk5wgFApw6 35 | yMnH8Kz/+WFU8SZDV/NgNyEwoW7jLL08fOdswTrOQ55ARcttDrSZUPdjHgTqWPZIxCNvW 36 | 85KWNGsAcAIiuC4jgN1EJoodR+mExPT8hnpy1hM1EizQG2BVSkfibkWq0aQh5Yic8EmGI 37 | /s4WT7x4sGKjW89hIN1E031aZnIBJnnNRClauKnXpHUmC140G1ih5crc/RAZ3M= 38 | gitea_token: !encrypted/pkcs1-oaep 39 | - DdahPv/8fcUIVNPXPrraaXm8WKblT5IzMTH12fquGebo/IkGpuS0RSFJNjSvuPf959iwo 40 | PJGM3bpSXdlcHy+FTXDuIJDX8YEwSIUjDdTQlhJAVNz3sp5tN6a81SKb+dnTZVhR/JA9o 41 | klXdXFzrmZq/VdIPV1VDw9KdHzQmVhWhzJDSN2riIbbEDH0I64wymTN5p9ZnIMNH4yP3Y 42 | O+na+QG6HvpHPQvg1zH9SJ6zxgvqze+2AFMu+zKw0+F69eEYqyb/ZL5awynEI8uRQCgS8 43 | R67lDoNiz0zUQLRNxqLc1EPihID5C0u+vTTUi+d9AydBC3jqGufV0s11LyyOk36A0sogj 44 | Uol4aEvKZ46rgovbrd6KbNOXjB0pJTdqXLGr5ycLNEYO32EaboNrnx7ynkGZgtg7EN0fo 45 | pFoD0B9oqbW7XJpzRON+zlHZJYg0xVdjbK6jFCXAZEFfSpUUkKR/kdSjgXH4MktI9E1fd 46 | P6XNI9POSea8pj/Ct4GdCPYRUOVlS2uGKz2Wz+4zpL+c5WPjun3EY0J9RhpNERF13Iw8L 47 | YwntZI9IuP+6Dq7fZMnApcHkhMClgQIPW8UWEYwY+f4PFxYpcnnYSpJgLEo5Iu3pfQdgB 48 | c7K9L1tILzhcaLVZTpkf3tBzr6xZea97rGWQSs9LoQLSX9kqEgRPlh+T40VNjQ= 49 | gitea_api_url: !encrypted/pkcs1-oaep 50 | - PMruLkiJmaMG+d/+p966Ta5+FXtAFpqb/yG+2Ukm8p3OKWj4ldMXD8/lDWzZivK76fYJ1 51 | DlnMOJ9bBipb+WTFHVVWyZ9klLnBN//zaWHAvVht61I0fBKbRAvmGf0Fw5qtAn1Dp57bl 52 | FE9YptJJeXBfqZwMuaVjT2W2biweebJSpLXUoKsh0sEaTs0lrT6i6pc0DvJGJTRWL1IxP 53 | G5uSRy1HapqJxRLpxsytL6eHQV6cxDrtMIvLgRlPEVLsv4+vAP2QtjiQctuXHlr8ikl5s 54 | CkywZS3h9ia7KxmIJvT/oAyg38sQgFKE9m5+AG6F5253GdM9raa9NiinkaoSP3NkZjozc 55 | urdr/ai0azHgaGWytN6Q192uU6udRGQ2yIGfT4PJOijlIyX01TuclGp+LLbhaz40x8OEv 56 | BCvgvgR+Khi1iBx5ua5UuX/RNJwrVtzn0+AkjCu0G4k4RiCDAS5+eY9GfkG6gmCmR6zBU 57 | d19zv3zIvyVhx4cHvuXrx9uP9gyDSdD8ANMEq469JsFYJ6aaL7Q5n79u7F4QDkba93nix 58 | QrV8pdmfAa1E43jc+Iz32b724c45JMYQyUAcnbcIxBw/PNH2g6sZbBMrJmeRfCl6vmXgk 59 | CRFzNkNszOLwhfTCeuL1hB1xunOwGjC3sFZ7gq8Y2MW7lbivT+U+XH952w2acs= 60 | 61 | - project: 62 | merge-mode: squash-merge 63 | vars: 64 | ansible_collection_namespace: "opentelekomcloud" 65 | ansible_collection_name: "gitcontrol" 66 | check: 67 | jobs: 68 | - otc-tox-pep8 69 | - otc-tox-linters 70 | - ansible-collection-test-sanity 71 | - ansible-collection-build 72 | - ansible-collection-docs 73 | check-post: 74 | jobs: 75 | - gitcontrol-test-integration 76 | gate: 77 | jobs: 78 | - otc-tox-pep8 79 | - otc-tox-linters 80 | - ansible-collection-test-sanity 81 | - ansible-collection-build 82 | - ansible-collection-docs 83 | - gitcontrol-test-integration 84 | release: 85 | jobs: 86 | - release-ansible-collection 87 | promote: 88 | jobs: 89 | - promote-ansible-collection-docs 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | doc/source/index.rst -------------------------------------------------------------------------------- /bindep.txt: -------------------------------------------------------------------------------- 1 | # This is a cross-platform list tracking distribution packages needed by tests; 2 | # see https://docs.openstack.org/infra/bindep/ for additional information. 3 | 4 | python3-pip [test platform:rpm] 5 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | otcdocstheme # Ansible-2.0 2 | Sphinx>4.0 # BSD 3 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = 'Open Telekom Cloud Ansible Modules Documentation' 20 | copyright = '2021, Open Telekom Cloud' 21 | author = 'Open Telekom Cloud' 22 | 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | 'otcdocstheme', 31 | 'sphinx.ext.autodoc', 32 | 'sphinx.ext.intersphinx', 33 | 'sphinx_antsibull_ext' 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = [] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = 'otcdocs' 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Ansible-collection-gitcontrol 3 | ============================= 4 | 5 | This collection helps to automate management of the GitHub organization using Ansible. 6 | 7 | 8 | Data Structure 9 | -------------- 10 | 11 | Describe all your organizations in `orgs/{{ structure_name }}/`, 12 | 13 | **Each organization must have next folder structure:** 14 | 15 | .. code-block:: yaml 16 | 17 | org: 18 | name: 19 | people: 20 | dismissed_members.yml 21 | members.yml 22 | repositories: 23 | repo_name.yml 24 | teams: 25 | dismissed_members.yml 26 | members.yml 27 | 28 | ```Currently works only repo management``` 29 | 30 | Repositories 31 | ------------ 32 | 33 | Describe your repositories in `orgs/my_org/repositories/my_repo.yml`, file name should be equal to repo name, and one repo per file: 34 | 35 | .. code-block:: yaml 36 | 37 | my_repo: 38 | default_branch: main 39 | description: >- 40 | Brief description. Try to fit it in one line. As linefeeds are not allowed here. 41 | homepage: https://example.com 42 | language: Python 43 | archived: true / false # this is one direction road: once archived the repo can be unarchived via web only 44 | has_issues: true / false 45 | has_projects: true 46 | has_wiki: true / false 47 | private: true / false 48 | delete_branch_on_merge: false 49 | allow_merge_commit: false 50 | allow_squash_merge: true 51 | allow_rebase_merge : false 52 | teams: 53 | maintain: # List of teams who need to manage the repository without access to sensitive or destructive actions. 54 | pull: # List of teams who can only read this repo. 55 | push: # List of teams with push access. 56 | admin: # List of admin teams. 57 | - csm 58 | collaborators: 59 | maintain: # List of members who need to manage the repository without access to sensitive or destructive actions. 60 | pull: # List of members who can only read this repo. 61 | push: # List of members with push access. 62 | admin: # List of admin members. 63 | - anton-sidelnikov 64 | topics: # List of repository topics. 65 | - a 66 | - b 67 | - c 68 | protection_rules: # do not change protection rules structure all fields is required 69 | main: # branch name which already created in branch protection rules 70 | required_status_checks: 71 | strict: true / false 72 | contexts: # The list of status checks to require in order to merge into this branch 73 | - eco/check 74 | enforce_admins: true 75 | required_pull_request_reviews: 76 | dismissal_restrictions: 77 | users: [] # list of members or empty list 78 | teams: [] # list of teams or empty list 79 | dismiss_stale_reviews: true 80 | require_code_owner_reviews: false 81 | required_approving_review_count: 1 82 | restrictions: 83 | users: [] # list of members or empty list 84 | teams: [] # list of teams or empty list 85 | apps: # list of app slugs with push access 86 | - otc-zuul 87 | required_linear_history: false 88 | allow_force_pushes: false 89 | allow_deletions: false 90 | 91 | 92 | Protection rules can be setted up through templates which should exist in **/templates** 93 | 94 | **Note:** Please note it is not possible to set up branch protection rules for 95 | the repository unless branch (`default_branch`) exists. This is especially the 96 | case for newly created repositories. 97 | 98 | Branch Protection Templates 99 | --------------------------- 100 | 101 | Under the `/templates/.yml` a file with following content 102 | can be placed: 103 | 104 | .. code-block:: yaml 105 | 106 | my_repo: 107 | default_branch: main 108 | description: >- 109 | Brief description. Try to fit it in one line. As linefeeds are not allowed here. 110 | homepage: https://example.com 111 | language: Python 112 | archived: true / false # this is one direction road: once archived the repo can be unarchived via web only 113 | has_issues: true / false 114 | has_projects: true 115 | has_wiki: true / false 116 | private: true / false 117 | delete_branch_on_merge: false 118 | allow_merge_commit: false 119 | allow_squash_merge: true 120 | allow_rebase_merge : false 121 | teams: 122 | maintain: # List of teams who need to manage the repository without access to sensitive or destructive actions. 123 | pull: # List of teams who can only read this repo. 124 | push: # List of teams with push access. 125 | admin: # List of admin teams. 126 | - csm 127 | collaborators: 128 | maintain: # List of members who need to manage the repository without access to sensitive or destructive actions. 129 | pull: # List of members who can only read this repo. 130 | push: # List of members with push access. 131 | admin: # List of admin members. 132 | - anton-sidelnikov 133 | topics: # List of repository topics. 134 | - a 135 | - b 136 | - c 137 | protection_rules: template_name 138 | 139 | * Those teams and collaborators should exist in organization. 140 | 141 | Members 142 | ------- 143 | 144 | Under the `ROOT/ORG_NAME/people/members.yml` a yaml file describing desired 145 | organization members with their roles must be placed. All current members and 146 | invites will be checked agains target state and changes in the roles will be 147 | applied. 148 | 149 | Current invites for members not in the target list will be cancelled. Members 150 | not in the target state will be reported as "Not managed". 151 | 152 | .. code-block:: yaml 153 | 154 | users: 155 | - name: "User1" 156 | login: "usr1" 157 | role: Member 158 | 159 | A second file `ROOT/ORG_NAME/users/dismissed_members.yaml` must be also placed 160 | with currently only dummy content (removing users from organizations is not yet 161 | supported. 162 | 163 | .. code-block:: yaml 164 | 165 | dismissed_users: {} 166 | 167 | Teams 168 | ----- 169 | 170 | Under the `ROOT/ORG_NAME/teams/members.yml` a file describing desired teams 171 | must be placed. 172 | 173 | .. code-block:: yaml 174 | 175 | teams: 176 | storage: # Team name (slug) 177 | description: Test team 178 | privacy: closed # privacy according to https://docs.github.com/en/enterprise-server@3.0/rest/reference/teams#create-a-team 179 | parent: 180 | maintainer: 181 | - github_user1 182 | member: 183 | - github_user2 184 | 185 | A second file `ROOT/ORG_NAME/teams/dismissed_members.yaml` must be also placed 186 | with currently only dummy content (removing teams from organizations is not yet 187 | supported. 188 | 189 | .. code-block:: yaml 190 | 191 | dissmissed_in_teams: {} 192 | 193 | How to use it 194 | ------------- 195 | 196 | As a prerequisite, a `PAT `_ 197 | must be created. The rights `repo` and `admin:org` are required. Root dir must 198 | point to the location hosting `/orgs/` 199 | 200 | To apply changes in your organization repositories run: 201 | 202 | .. code-block:: yaml 203 | 204 | ansible-playbook playbooks/run.yaml \ 205 | -e github_repos_state=present \ 206 | -e gitstyring_root_dir=../org \ 207 | -e gitub_token=SECRET 208 | 209 | Testing 210 | ------- 211 | 212 | Testing of the collection locally can be done with the help of ansible-test 213 | utility. For that (under assumption of proper checkout location or setting 214 | environment variables to include working directory) test invokation can be 215 | executed as: `ansible-test integration members` or `ansible-test integration 216 | members`. 217 | 218 | Testing assumes local configuration is prepared in the 219 | tests/integration/integration_config.yml` file: 220 | 221 | .. code-block:: yaml 222 | 223 | root: "/test_org" 224 | token: "" 225 | -------------------------------------------------------------------------------- /docs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentelekomcloud/ansible-collection-gitcontrol/818090c9d93ec6ee3b6cd988d2a6bb826a8873c4/docs/.keep -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: opentelekomcloud 3 | name: gitcontrol 4 | version: 0.2.0 5 | readme: README.rst 6 | authors: 7 | - Anton Sidenlnikov (@anton-sidelnikov) 8 | - Artem Goncharov (@gtema) 9 | 10 | description: Roles for managing GitHub organizations using Ansible 11 | license: 12 | - GPL-2.0-or-later 13 | tags: 14 | - github 15 | - opentelekomcloud 16 | dependencies: {} 17 | repository: http://github.com/opentelekomcloud-infra/ansible-collection-gitcontrol 18 | documentation: http://docs.otc-service.com 19 | homepage: https://open-telekom-cloud.com 20 | issues: http://github.com/opentelekomcloud-infra/ansible-collection-gitcontrol/issue/tracker 21 | build_ignore: 22 | - '*.tar.gz' 23 | - build_artifact/ 24 | - ci/ 25 | - galaxy.yml.in 26 | - setup.cfg 27 | - setup.py 28 | - tools 29 | - tox.ini 30 | - .gitignore 31 | - .gitreview 32 | - .zuul.yaml 33 | - .pytest_cache 34 | - importer_result.json 35 | -------------------------------------------------------------------------------- /galaxy.yml.in: -------------------------------------------------------------------------------- 1 | namespace: opentelekomcloud 2 | name: gitcontrol 3 | version: 0.2.0 4 | readme: README.rst 5 | authors: 6 | - Anton Sidenlnikov (@anton-sidelnikov) 7 | - Artem Goncharov (@gtema) 8 | 9 | description: Roles for managing GitHub organizations using Ansible 10 | license: 11 | - GPL-2.0-or-later 12 | tags: 13 | - github 14 | - opentelekomcloud 15 | dependencies: {} 16 | repository: http://github.com/opentelekomcloud-infra/ansible-collection-gitcontrol 17 | documentation: http://docs.otc-service.com 18 | homepage: https://open-telekom-cloud.com 19 | issues: http://github.com/opentelekomcloud-infra/ansible-collection-gitcontrol/issue/tracker 20 | build_ignore: 21 | - "*.tar.gz" 22 | - build_artifact/ 23 | - ci/ 24 | - galaxy.yml.in 25 | - setup.cfg 26 | - setup.py 27 | - tools 28 | - tox.ini 29 | - .gitignore 30 | - .gitreview 31 | - .zuul.yaml 32 | - .pytest_cache 33 | - importer_result.json 34 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | {} 3 | -------------------------------------------------------------------------------- /playbooks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentelekomcloud/ansible-collection-gitcontrol/818090c9d93ec6ee3b6cd988d2a6bb826a8873c4/playbooks/.keep -------------------------------------------------------------------------------- /playbooks/run.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | tasks: 4 | - name: manage people 5 | opentelekomcloud.gitcontrol.members: 6 | root: "{{ gitstyring_root_dir }}" 7 | token: "{{ github_token }}" 8 | 9 | - name: manage teams 10 | opentelekomcloud.gitcontrol.teams: 11 | root: "{{ gitstyring_root_dir }}" 12 | token: "{{ github_token }}" 13 | 14 | - name: manage repositories 15 | opentelekomcloud.gitcontrol.repositories: 16 | root: "{{ gitstyring_root_dir }}" 17 | token: "{{ github_token }}" 18 | -------------------------------------------------------------------------------- /plugins/doc_fragments/git.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | 7 | class ModuleDocFragment(object): 8 | 9 | # Standard documentation fragment 10 | DOCUMENTATION = r''' 11 | options: 12 | root: 13 | description: 14 | - Root path to the configuration location. 15 | type: str 16 | required: True 17 | github_url: 18 | description: URL of the GitHub API 19 | type: str 20 | default: https://api.github.com 21 | requirements: 22 | - python >= 3.6 23 | - requests 24 | ''' 25 | -------------------------------------------------------------------------------- /plugins/doc_fragments/gitea.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | 7 | class ModuleDocFragment(object): 8 | 9 | # Standard documentation fragment 10 | DOCUMENTATION = r''' 11 | options: 12 | token: 13 | description: Gitea token 14 | type: str 15 | required: True 16 | api_url: 17 | description: URL of the Gitiea API 18 | type: str 19 | required: True 20 | requirements: 21 | - python >= 3.6 22 | - requests 23 | ''' 24 | -------------------------------------------------------------------------------- /plugins/doc_fragments/github.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | 7 | class ModuleDocFragment(object): 8 | 9 | # Standard documentation fragment 10 | DOCUMENTATION = r''' 11 | options: 12 | token: 13 | description: GitHub token 14 | type: str 15 | required: True 16 | github_url: 17 | description: URL of the GitHub API 18 | type: str 19 | default: https://api.github.com 20 | requirements: 21 | - python >= 3.6 22 | - requests 23 | ''' 24 | -------------------------------------------------------------------------------- /plugins/module_utils/git.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | 7 | import abc 8 | import re 9 | 10 | from ansible.module_utils.basic import AnsibleModule 11 | from ansible.module_utils.urls import fetch_url 12 | 13 | 14 | def base_argument_spec(**kwargs): 15 | spec = dict( 16 | ) 17 | spec.update(kwargs) 18 | return spec 19 | 20 | 21 | def parse_header_links(value): 22 | """Return a list of parsed link headers proxies. 23 | i.e. Link: ; rel=front; type="image/jpeg",; rel=back;type="image/jpeg" 24 | :rtype: list 25 | """ 26 | links = [] 27 | replace_chars = ' \'"' 28 | 29 | value = value.strip(replace_chars) 30 | if not value: 31 | return links 32 | 33 | for val in re.split(', *<', value): 34 | try: 35 | url, params = val.split(';', 1) 36 | except ValueError: 37 | url, params = val, '' 38 | 39 | link = {'url': url.strip('<> \'"')} 40 | 41 | for param in params.split(';'): 42 | try: 43 | key, value = param.split('=') 44 | except ValueError: 45 | break 46 | 47 | link[key.strip(replace_chars)] = value.strip(replace_chars) 48 | 49 | links.append(link) 50 | 51 | return links 52 | 53 | 54 | def get_links(headers): 55 | """Returns the parsed header links of the response, if any.""" 56 | 57 | header = headers.get('link') 58 | res = {} 59 | 60 | if header: 61 | for link in parse_header_links(header): 62 | key = link.get('rel') or link.get('url') 63 | res[key] = link 64 | 65 | return res 66 | 67 | 68 | class GitBase: 69 | 70 | argument_spec = {} 71 | module_kwargs = {} 72 | _bp_templates = {} 73 | 74 | def __init__(self): 75 | 76 | self.ansible = AnsibleModule( 77 | base_argument_spec(**self.argument_spec), 78 | **self.module_kwargs) 79 | self.params = self.ansible.params 80 | self.module_name = self.ansible._name 81 | self.results = {'changed': False} 82 | self.errors = [] 83 | self.exit = self.exit_json = self.ansible.exit_json 84 | self.fail = self.fail_json = self.ansible.fail_json 85 | 86 | @abc.abstractmethod 87 | def run(self): 88 | pass 89 | 90 | def __call__(self): 91 | """Execute `run` function when calling the class. 92 | """ 93 | try: 94 | results = self.run() 95 | if results and isinstance(results, dict): 96 | self.ansible.exit_json(**results) 97 | except Exception as ex: 98 | self.ansible.fail_json( 99 | msg='Unhandled exception during execution', 100 | errors=self.errors, 101 | exception=ex 102 | ) 103 | 104 | def save_error(self, msg): 105 | self.ansible.log(msg) 106 | self.errors.append(msg) 107 | 108 | def _prepare_graphql_query(self, query, variables): 109 | data = { 110 | 'query': query, 111 | 'variables': variables, 112 | } 113 | return data 114 | 115 | def _request(self, method, url, headers=None, **kwargs): 116 | if not headers: 117 | headers = dict() 118 | 119 | json_data = kwargs.pop('json', '') 120 | if json_data: 121 | kwargs['data'] = self.ansible.jsonify(json_data) 122 | 123 | response, info = fetch_url( 124 | module=self.ansible, 125 | headers=headers, 126 | method=method, url=url, 127 | **kwargs 128 | ) 129 | content = "" 130 | if response: 131 | content = response.read() 132 | return (content, response, info) 133 | -------------------------------------------------------------------------------- /plugins/module_utils/gitea.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | 7 | import json 8 | 9 | from ansible.module_utils.basic import AnsibleModule 10 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.git import GitBase 11 | 12 | 13 | REPOSITORY_UPDATABLE_ATTRIBUTES = [ 14 | 'allow_manual_merge', 15 | 'allow_merge_commits', 16 | 'allow_rebase', 17 | 'allow_rebase_explicit', 18 | 'allow_rebase_update', 19 | 'allow_squash_merge', 20 | 'archived', 21 | 'autodetect_manual_merge', 22 | 'default_branch', 23 | 'default_delete_branch_after_merge', 24 | 'default_merge_style', 25 | 'description', 26 | 'enable_prune', 27 | 'has_issues', 28 | 'has_projects', 29 | 'has_pull_requests', 30 | 'has_wiki', 31 | 'ignore_whitespace_conflicts', 32 | 'private', 33 | 'template', 34 | 'website' 35 | ] 36 | 37 | 38 | def base_argument_spec(**kwargs): 39 | spec = dict( 40 | token=dict(type='str', required=True, no_log=True), 41 | api_url=dict(type='str', required=True), 42 | ) 43 | spec.update(kwargs) 44 | return spec 45 | 46 | 47 | class GiteaBase(GitBase): 48 | 49 | argument_spec = {} 50 | module_kwargs = {} 51 | _bp_templates = {} 52 | 53 | def __init__(self): 54 | self.ansible = AnsibleModule( 55 | base_argument_spec(**self.argument_spec), 56 | **self.module_kwargs 57 | ) 58 | self.params = self.ansible.params 59 | self.module_name = self.ansible._name 60 | self.results = {'changed': False} 61 | self.exit = self.exit_json = self.ansible.exit_json 62 | self.fail = self.fail_json = self.ansible.fail_json 63 | 64 | self.api_url = self.params['api_url'] 65 | self.errors = [] 66 | self._users_cache = dict() 67 | 68 | def save_error(self, msg): 69 | self.ansible.log(msg) 70 | self.errors.append(msg) 71 | 72 | def _request(self, method, url, headers=None, **kwargs): 73 | if not headers: 74 | headers = dict() 75 | 76 | headers.update({ 77 | 'Authorization': f"token {self.params['token']}", 78 | 'Content-Type': "application/json", 79 | }) 80 | 81 | if not url.startswith('http'): 82 | url = f"{self.api_url}/{url}" 83 | 84 | return super()._request( 85 | method=method, 86 | url=url, 87 | headers=headers, 88 | **kwargs 89 | ) 90 | 91 | def request( 92 | self, method='GET', url=None, headers=None, timeout=15, 93 | error_msg=None, ignore_missing=False, 94 | **kwargs 95 | ): 96 | 97 | body, response, info = self._request( 98 | method=method, 99 | url=url, 100 | headers=headers, 101 | timeout=timeout, 102 | **kwargs, 103 | ) 104 | 105 | status = info['status'] 106 | 107 | if status >= 400 and status != 404: 108 | if not error_msg: 109 | error_msg = ( 110 | f"API returned error on {url}" 111 | ) 112 | error_data = dict(url=url) 113 | for key in ['url', 'msg', 'status', 'body']: 114 | if key in info: 115 | error_data[key] = info[key] 116 | 117 | self.save_error(f"{error_msg}: {error_data}") 118 | elif status == 404 and ignore_missing: 119 | return None 120 | if status == 204: 121 | return response 122 | elif body and status < 400: 123 | return json.loads(body) 124 | 125 | def paginated_request(self, url, headers=None, timeout=15, params=None): 126 | if not url.startswith('http'): 127 | url = f"{self.api_url}/{url}" 128 | 129 | if not params: 130 | params = dict() 131 | total_count = 0 132 | fetched = 0 133 | page = 1 134 | headers['Accept'] = 'application/json' 135 | while True: 136 | content, response, info = self._request( 137 | method='GET', 138 | url=url, 139 | headers=headers, 140 | timeout=timeout, 141 | params=params 142 | ) 143 | total_count = int(response.headers.get('X-Total-Count', 0)) 144 | data = json.loads(content) 145 | if isinstance(data, list): 146 | for rec in data: 147 | yield rec 148 | fetched += 1 149 | if fetched == total_count: 150 | return 151 | else: 152 | page += 1 153 | params['page'] = page 154 | 155 | def get_repo(self, owner, repo, ignore_missing=False): 156 | """Get repository information""" 157 | return self.request( 158 | method='GET', 159 | url=f"repos/{owner}/{repo}", 160 | error_msg=f"Repo {repo}@{owner} cannot be fetched", 161 | ignore_missing=ignore_missing 162 | ) 163 | 164 | def create_repo(self, owner, repo, **args): 165 | if not args: 166 | args = dict() 167 | args['name'] = repo 168 | rsp = self.request( 169 | method='POST', 170 | url=f"orgs/{owner}/repos", 171 | json=args, 172 | error_msg=f"Repo {repo}@{owner} cannot be created" 173 | ) 174 | return rsp 175 | 176 | def update_repo(self, owner, repo, **kwargs): 177 | """Update repository options""" 178 | data = dict() 179 | for attr in REPOSITORY_UPDATABLE_ATTRIBUTES: 180 | if attr in kwargs and kwargs[attr] is not None: 181 | data[attr] = kwargs[attr] 182 | 183 | rsp = self.request( 184 | method='PATCH', 185 | url=f'repos/{owner}/{repo}', 186 | json=data, 187 | error_msg=f"Repo {repo}@{owner} cannot be updated" 188 | ) 189 | return rsp 190 | 191 | def delete_repo(self, owner, repo): 192 | """Delete repository""" 193 | rsp = self.request( 194 | method='DELETE', 195 | url=f'repos/{owner}/{repo}', 196 | error_msg=f"Repo {repo}@{owner} cannot be deleted" 197 | ) 198 | return rsp 199 | 200 | def get_branch_protection(self, owner, repo, branch): 201 | """Get branch protection rules""" 202 | 203 | return self.request( 204 | method='GET', 205 | url=(f'repos/{owner}/{repo}/branch_protections/{branch}'), 206 | error_msg=f"Repo {repo}@{owner} branch protection cannot be updated" 207 | ) 208 | 209 | def create_branch_protection(self, owner, repo, branch, target): 210 | """Set branch protection rules""" 211 | target['branch_name'] = branch 212 | 213 | self.request( 214 | method='POST', 215 | url=(f'repos/{owner}/{repo}/branch_protections'), 216 | json=target, 217 | error_msg=f"Repo {repo}@{owner} branch protection cannot be updated" 218 | ) 219 | 220 | return True 221 | 222 | def update_branch_protection(self, owner, repo, branch, target): 223 | """Set branch protection rules""" 224 | 225 | self.request( 226 | method='PATCH', 227 | url=(f'repos/{owner}/{repo}/branch_protections/{branch}'), 228 | json=target, 229 | error_msg=f"Repo {repo}@{owner} branch protection cannot be updated" 230 | ) 231 | 232 | return True 233 | 234 | def _manage_repository(self, state, current=None, check_mode=False, **kwargs): 235 | 236 | changed = False 237 | owner = kwargs.pop('owner') 238 | repo_name = kwargs.pop('name') 239 | current_repo = current if current else self.get_repo(owner, repo_name, ignore_missing=True) 240 | if not current_repo: 241 | changed = True 242 | if not check_mode: 243 | # TODO(gtema) create and update take different set of props. Deal with that here 244 | current_repo = self.create_repo( 245 | owner, repo_name, **kwargs) 246 | else: 247 | return (changed, kwargs) 248 | 249 | archive = kwargs.pop('archived', False) 250 | if ( 251 | current_repo 252 | and archive and current_repo.get('archived') 253 | ): 254 | # Do nothing for the archived repo 255 | return (changed, current_repo) 256 | 257 | if current_repo and self._is_repo_update_needed(current_repo, kwargs): 258 | changed = True 259 | if not check_mode: 260 | current_repo = self.update_repo(owner, repo_name, **kwargs) 261 | 262 | # Repository collaborator teams 263 | target_teams = kwargs.get('teams') 264 | if ( 265 | current_repo and target_teams is not None 266 | ): 267 | (changed, teams) = self._manage_repo_teams( 268 | owner, repo_name, target_teams, check_mode) 269 | current_repo['teams'] = teams 270 | 271 | # Repository collaborators 272 | target_collaborators = kwargs.get('collaborators') 273 | if ( 274 | current_repo and target_collaborators is not None 275 | ): 276 | changed = self._manage_repo_collaborators( 277 | owner, repo_name, target_collaborators, check_mode) 278 | 279 | # Branch protections 280 | 281 | # Branch protection should be managed after teams and collaborators 282 | # since it can require particular reviewer which still has no access. 283 | 284 | branch_protections = kwargs.pop('branch_protections', []) 285 | if current_repo and branch_protections is not None: 286 | current_repo['branch_protections'] = [] 287 | for bp in branch_protections: 288 | current_bp = self.get_branch_protection( 289 | owner, repo_name, bp['branch_name'] 290 | ) 291 | if not current_bp: 292 | self.create_branch_protection( 293 | owner, repo_name, bp['branch_name'], bp 294 | ) 295 | else: 296 | if ( 297 | self._is_branch_protection_update_needed( 298 | owner, repo_name, bp['branch_name'], bp, current_bp) 299 | ): 300 | changed = True 301 | if not check_mode: 302 | self.update_branch_protection( 303 | owner, repo_name, bp['branch_name'], bp) 304 | current_repo['branch_protections'].append(bp) 305 | 306 | # If we need to archive - do this after updating everything else 307 | if ( 308 | current_repo 309 | and archive and not current_repo.get('archived') 310 | ): 311 | changed = True 312 | if not check_mode: 313 | current_repo = self.update_repo( 314 | owner, repo_name, archived=True) 315 | 316 | return (changed, current_repo) 317 | 318 | def _is_repo_update_needed(self, current, target): 319 | for attr in REPOSITORY_UPDATABLE_ATTRIBUTES: 320 | if attr in target and target[attr] != current.get(attr): 321 | return True 322 | 323 | def _is_branch_protection_update_needed( 324 | self, 325 | owner, 326 | repo, 327 | branch, 328 | target, 329 | current=None 330 | ): 331 | BRANCH_PROTECTION_PROPS = [ 332 | 'approvals_whitelist_teams', 333 | 'approvals_whitelist_username', 334 | 'block_on_official_review_requests', 335 | 'block_on_outdated_branch', 336 | 'block_on_rejected_reviews', 337 | 'dismiss_stale_reviews', 338 | 'enable_approvals_whitelist', 339 | 'enable_merge_whitelist', 340 | 'enable_push', 341 | 'enable_push_whitelist', 342 | 'enable_status_check', 343 | 'merge_whitelist_teams', 344 | 'merge_whitelist_usernames', 345 | 'protected_file_patterns', 346 | 'push_whitelist_deploy_keys', 347 | 'push_whitelist_teams', 348 | 'push_whitelist_usernames', 349 | 'require_signed_commits', 350 | 'required_approvals', 351 | 'status_check_contexts', 352 | 'unprotected_file_patterns' 353 | ] 354 | for prop in BRANCH_PROTECTION_PROPS: 355 | if prop in target and current.get(prop) != target[prop]: 356 | return True 357 | return False 358 | 359 | def get_repo_teams(self, owner, repo): 360 | """Get repo teams""" 361 | rsp = self.request( 362 | method='GET', 363 | url=(f"repos/{owner}/{repo}/teams"), 364 | error_msg=f"Cannot fetch team {owner}/{repo} teams" 365 | ) 366 | 367 | return rsp 368 | 369 | def delete_repo_team_access(self, owner, repo, team): 370 | """Delete team from repo collaborators""" 371 | self.request( 372 | method='DELETE', 373 | url=f"repos/{owner}/{repo}/teams/{team}", 374 | error_msg=f"Cannot delete team {team}@{owner}/{repo} access" 375 | ) 376 | 377 | def add_repo_team_access(self, owner, repo, team): 378 | """Add team as repo collaborators""" 379 | self.request( 380 | method='PUT', 381 | url=f"repos/{owner}/{repo}/teams/{team}", 382 | error_msg=f"Cannot add team {team}@{owner}/{repo} access" 383 | ) 384 | 385 | def _manage_repo_teams( 386 | self, owner, repo_name, target, check_mode=False 387 | ): 388 | """Manage repository teams""" 389 | changed = False 390 | teams = None 391 | current_teams = set([x['name'] for x in 392 | self.get_repo_teams(owner, repo_name) or []]) 393 | target_teams = set(target + ['Owners']) 394 | for old_team in current_teams.difference(target_teams): 395 | changed = True 396 | if not check_mode: 397 | self.delete_repo_team_access(owner, repo_name, old_team) 398 | for new_team in target_teams.difference(current_teams): 399 | changed = True 400 | if not check_mode: 401 | self.add_repo_team_access(owner, repo_name, new_team) 402 | teams = set([x['name'] for x in 403 | self.get_repo_teams(owner, repo_name) or []]) 404 | return (changed, teams) 405 | 406 | def get_repo_collaborators(self, owner, repo): 407 | """Get repo collaborators""" 408 | rsp = self.request( 409 | method='GET', 410 | url=(f"repos/{owner}/{repo}/collaborators"), 411 | error_msg=f"Cannot fetch team {owner}/{repo} collaborators" 412 | ) 413 | 414 | return rsp 415 | 416 | def add_repo_collaborator(self, owner, repo, login, permission): 417 | """Add repo collaborator""" 418 | rsp = self.request( 419 | method='PUT', 420 | url=(f"repos/{owner}/{repo}/collaborators/{login}"), 421 | json={"permission": permission}, 422 | error_msg=f"Cannot add collaborator to {owner}/{repo}" 423 | ) 424 | 425 | return rsp 426 | 427 | def remove_repo_collaborator(self, owner, repo, login): 428 | """Remove repo collaborator""" 429 | rsp = self.request( 430 | method='DELETE', 431 | url=(f"repos/{owner}/{repo}/collaborators/{login}"), 432 | error_msg=f"Cannot add collaborator to {owner}/{repo}" 433 | ) 434 | 435 | return rsp 436 | 437 | def get_repo_collaborator_permission(self, owner, repo, login): 438 | """Get repo collaborators""" 439 | rsp = self.request( 440 | method='GET', 441 | url=(f"repos/{owner}/{repo}/collaborators/{login}/permission"), 442 | error_msg=f"Cannot fetch team {owner}/{repo} collaborators" 443 | ) 444 | 445 | return rsp 446 | 447 | def _manage_repo_collaborators( 448 | self, owner, repo_name, target, check_mode=False 449 | ): 450 | """Manage repository collaborators""" 451 | changed = False 452 | current_collaborators = {x['login']: 1 for x in 453 | self.get_repo_collaborators(owner, repo_name) or []} 454 | target_collaborators = {x['username']: x['permission'] for x in 455 | target} 456 | for login, permission in target_collaborators.items(): 457 | if login not in current_collaborators: 458 | changed = True 459 | if not check_mode: 460 | self.add_repo_collaborator( 461 | owner, repo_name, login, permission 462 | ) 463 | else: 464 | current = self.get_repo_collaborator_permission( 465 | owner, repo_name, login 466 | ) 467 | if current["permission"] != permission: 468 | changed = True 469 | if not check_mode: 470 | self.add_repo_collaborator( 471 | owner, repo_name, login, permission 472 | ) 473 | 474 | for login in current_collaborators.keys(): 475 | if login not in target_collaborators: 476 | changed = True 477 | if not check_mode: 478 | self.delete_repo_collaborator( 479 | owner, repo_name, login 480 | ) 481 | 482 | return changed 483 | -------------------------------------------------------------------------------- /plugins/module_utils/github.py: -------------------------------------------------------------------------------- 1 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 2 | 3 | from __future__ import (absolute_import, division, print_function) 4 | __metaclass__ = type 5 | 6 | 7 | import os 8 | import json 9 | 10 | try: 11 | import yaml 12 | HAS_YAML = True 13 | except ImportError: 14 | HAS_YAML = False 15 | 16 | from ansible.module_utils.basic import AnsibleModule 17 | from ansible.module_utils.basic import missing_required_lib 18 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.git import (GitBase, get_links) 19 | 20 | 21 | QUERY_MEMBERS = ''' 22 | query members( 23 | $owner: String! 24 | $memberCursor: String 25 | ) { 26 | organization(login: $owner) { 27 | membersWithRole(first: 100, after: $memberCursor) { 28 | edges { 29 | role 30 | node { 31 | login 32 | } 33 | } 34 | pageInfo { 35 | hasNextPage 36 | endCursor 37 | } 38 | } 39 | } 40 | } 41 | ''' 42 | 43 | REPOSITORY_UPDATABLE_ATTRIBUTES = [ 44 | 'allow_auto_merge', 45 | 'allow_forking', 46 | 'allow_merge_commit', 47 | 'allow_rebase_merge', 48 | 'allow_squash_merge', 49 | 'allow_update_branch', 50 | 'archived', 51 | 'default_branch', 52 | 'delete_branch_on_merge', 53 | 'description', 54 | 'has_issues', 55 | 'has_projects', 56 | 'has_wiki', 57 | 'homepage', 58 | 'is_template', 59 | 'private', 60 | 'visibility' 61 | ] 62 | 63 | 64 | def base_argument_spec(**kwargs): 65 | spec = dict( 66 | token=dict(type='str', required=True, no_log=True), 67 | github_url=dict(type='str', default='https://api.github.com') 68 | ) 69 | spec.update(kwargs) 70 | return spec 71 | 72 | 73 | class GitHubBase(GitBase): 74 | 75 | argument_spec = {} 76 | module_kwargs = {} 77 | _bp_templates = {} 78 | 79 | def __init__(self): 80 | self.ansible = AnsibleModule( 81 | base_argument_spec(**self.argument_spec), 82 | **self.module_kwargs) 83 | self.params = self.ansible.params 84 | self.module_name = self.ansible._name 85 | self.results = {'changed': False} 86 | self.exit = self.exit_json = self.ansible.exit_json 87 | self.fail = self.fail_json = self.ansible.fail_json 88 | 89 | self.gh_url = self.params['github_url'] 90 | self.errors = [] 91 | self._users_cache = dict() 92 | 93 | if not HAS_YAML: 94 | self.fail_json(msg=missing_required_lib('yaml')) 95 | 96 | def save_error(self, msg): 97 | self.ansible.log(msg) 98 | self.errors.append(msg) 99 | 100 | def get_config(self): 101 | output = {} 102 | for root, dirs, files in os.walk(self.params['root'] + '/orgs'): 103 | for file in [x for x in files if x.endswith(('.yml', '.yaml'))]: 104 | current_root = os.path.basename(root) 105 | a_yaml_file = open(os.path.join(root, file)) 106 | parsed_yaml_file = yaml.safe_load(a_yaml_file) 107 | parent = os.path.basename(os.path.abspath(os.path.join(root, os.pardir))) 108 | if parent in output: 109 | if current_root in output[parent]: 110 | output[parent][current_root].update(parsed_yaml_file) 111 | else: 112 | output[parent].update({current_root: parsed_yaml_file}) 113 | else: 114 | output.update({parent: {current_root: parsed_yaml_file}}) 115 | return output 116 | 117 | def _prepare_graphql_query(self, query, variables): 118 | data = { 119 | 'query': query, 120 | 'variables': variables, 121 | } 122 | return data 123 | 124 | def get_teams(self): 125 | teams = dict() 126 | conf = self.get_config() 127 | for owner, val in conf.items(): 128 | teams[owner] = dict() 129 | teams[owner]['present'] = self.read_yaml_file( 130 | path=(f"{self.params['root']}/orgs/{owner}/" 131 | "teams/members.yml") 132 | ) 133 | teams[owner]['dismissed'] = self.read_yaml_file( 134 | path=(f"{self.params['root']}/orgs/{owner}/" 135 | "teams/dismissed_members.yml") 136 | ) 137 | 138 | return teams 139 | 140 | def get_members(self): 141 | members = dict() 142 | conf = self.get_config() 143 | for owner, val in conf.items(): 144 | members[owner] = dict() 145 | members[owner]['present'] = self.read_yaml_file( 146 | 147 | path=(f"{self.params['root']}/orgs/{owner}/" 148 | "people/members.yml") 149 | ) 150 | members[owner]['dismissed'] = self.read_yaml_file( 151 | path=(f"{self.params['root']}/orgs/{owner}/" 152 | "people/dismissed_members.yml") 153 | ) 154 | 155 | return members 156 | 157 | def get_branch_protections(self, name): 158 | if name not in self._bp_templates: 159 | self._bp_templates[name] = self.read_yaml_file( 160 | f"{self.params['root']}/templates/{name}.yml") 161 | tmpl = self._bp_templates.get(name) 162 | if tmpl: 163 | if 'who_can_push' in tmpl: 164 | tmpl['restrictions'] = tmpl.pop('who_can_push') 165 | 166 | return tmpl 167 | 168 | def read_yaml_file(self, path, org=None, endpoint=None, repo_name=None): 169 | if endpoint in ['manage_collaborators', 'branch_protection', 'options', 'topics']: 170 | path += f'/{org}/repositories/{repo_name}.yml' 171 | if endpoint in ['teams']: 172 | path += f'/{org}/teams/members.yml' 173 | if endpoint in ['members']: 174 | path += f'/{org}/people/members.yml' 175 | with open(path, 'r') as file: 176 | data = yaml.safe_load(file) 177 | return data 178 | 179 | def _request(self, method, url, headers=None, **kwargs): 180 | if not headers: 181 | headers = dict() 182 | 183 | headers.update({ 184 | 'Authorization': f"token {self.params['token']}", 185 | 'Content-Type': "application/json", 186 | }) 187 | if 'Accept' not in headers: 188 | headers['Accept'] = 'application/vnd.github.v3+json' 189 | 190 | if not url.startswith('http'): 191 | url = f"{self.gh_url}/{url}" 192 | 193 | return super()._request( 194 | method=method, 195 | url=url, 196 | headers=headers, 197 | **kwargs 198 | ) 199 | 200 | def request( 201 | self, method='GET', url=None, headers=None, timeout=15, 202 | error_msg=None, ignore_missing=False, 203 | **kwargs 204 | ): 205 | 206 | body, response, info = self._request( 207 | method=method, 208 | url=url, 209 | headers=headers, 210 | timeout=timeout, 211 | **kwargs, 212 | ) 213 | 214 | status = info['status'] 215 | 216 | if status >= 400 and status != 404: 217 | if not error_msg: 218 | error_msg = ( 219 | f"API returned error on {url}" 220 | ) 221 | error_data = dict(url=url) 222 | for key in ['url', 'msg', 'status', 'body']: 223 | if key in info: 224 | error_data[key] = info[key] 225 | 226 | error_data["response"] = response or "no response" 227 | error_data["body"] = body or "no body" 228 | self.save_error(f"request failed {error_msg}: {error_data}") 229 | elif status == 404 and ignore_missing: 230 | return None 231 | if status == 204: 232 | return response 233 | elif body and status < 400: 234 | return json.loads(body) 235 | 236 | def paginated_request(self, url, headers=None, timeout=15, **kwargs): 237 | if not url.startswith('http'): 238 | url = f"{self.gh_url}/{url}" 239 | 240 | while url: 241 | content, response, info = self._request( 242 | method='GET', 243 | url=url, 244 | headers=headers, 245 | timeout=timeout, 246 | ) 247 | if info["status"] == 404: 248 | break 249 | 250 | url = get_links(response.headers).get("next", {}).get("url") 251 | 252 | yield from json.loads(content) 253 | 254 | def get_owner_teams(self, owner): 255 | """Get Team information""" 256 | return self.paginated_request( 257 | url=f'orgs/{owner}/teams', 258 | error_msg=f"Cannot fetch teams for {owner}" 259 | ) 260 | 261 | def get_team(self, owner, name, ignore_missing=False): 262 | return self.request( 263 | url=f"orgs/{owner}/teams/{name}", 264 | error_msg=f"Error fetching {owner}/{name} team", 265 | ignore_missing=ignore_missing 266 | ) 267 | 268 | def create_team( 269 | self, owner, name, description=None, privacy=None, 270 | parent=None, maintainers=None 271 | ): 272 | """Create Team""" 273 | body = dict( 274 | name=name, 275 | description=description, 276 | privacy=privacy, 277 | parent=parent, 278 | maintainers=maintainers 279 | ) 280 | 281 | rsp = self.request( 282 | method='POST', 283 | url=f"orgs/{owner}/teams", 284 | json=body, 285 | error_msg=f"Error creating {owner}/{name}" 286 | ) 287 | return rsp 288 | 289 | def delete_team(self, owner, team_slug): 290 | """Delete Team""" 291 | return self.request( 292 | method='DELETE', 293 | url=f"orgs/{owner}/teams/{team_slug}", 294 | error_msg=f"Error deleting team {owner}/{team_slug}" 295 | ) 296 | 297 | def update_team( 298 | self, owner, team, **kwargs 299 | ): 300 | """Update team properties 301 | """ 302 | body = dict() 303 | if 'name' in kwargs: 304 | body['name'] = kwargs['name'] 305 | if 'description' in kwargs: 306 | body['description'] = kwargs['description'] 307 | if 'privacy' in kwargs: 308 | body['privacy'] = kwargs['privacy'] 309 | 310 | return self.request( 311 | method='PATCH', 312 | url=f"orgs/{owner}/teams/{team}", 313 | json=body, 314 | error_msg=f"Cannot update team {team}@{owner}" 315 | ) 316 | 317 | def get_team_members(self, owner, team, role='maintainer'): 318 | """Get team members""" 319 | return self.paginated_request( 320 | method='GET', 321 | url=(f"orgs/{owner}/" 322 | f"teams/{team}/members?role={role}"), 323 | error_msg=f"Cannot fetch team {team}@{owner} with role {role}" 324 | ) 325 | 326 | def get_team_repo_permissions(self, owner, team, repo): 327 | """Get team permissions on a repo""" 328 | return self.request( 329 | method='GET', 330 | url=(f"orgs/{owner}/" 331 | f"teams/{team}/projects/{owner}/{repo}"), 332 | error_msg=f"Cannot fetch team {team}@{owner}/{repo} permissions" 333 | ) 334 | 335 | def update_team_repo_permissions2(self, org, team, owner, repo, priv): 336 | """Set team permissions on a repo""" 337 | self.request( 338 | method='PUT', 339 | url=(f"orgs/{org}/" 340 | f"teams/{team}/repos/{owner}/{repo}"), 341 | json={'permission': priv}, 342 | error_msg=f"Cannot update team {org}:{team}@{owner}/{repo}" 343 | f" permissions to {priv}" 344 | ) 345 | 346 | def update_team_repo_permissions(self, owner, team, repo, priv): 347 | """Set team permissions on a repo""" 348 | self.request( 349 | method='PUT', 350 | url=(f"orgs/{owner}/" 351 | f"teams/{team}/repos/{owner}/{repo}"), 352 | json={'permission': priv}, 353 | error_msg=f"Cannot update team {team}@{owner}/{repo} permissions" 354 | ) 355 | 356 | def delete_team_repo_access(self, owner, team, repo): 357 | """Delete repo access from team""" 358 | self.request( 359 | method='DELETE', 360 | url=(f"orgs/{owner}/" 361 | f"teams/{team}/repos/{owner}/{repo}"), 362 | error_msg=f"Cannot delete team {team}@{owner}/{repo} access" 363 | ) 364 | 365 | def set_team_member(self, owner, team, login, role='member'): 366 | """Add user into the team 367 | """ 368 | return self.request( 369 | method='PUT', 370 | url=(f"orgs/{owner}/" 371 | f"teams/{team}/memberships/{login}"), 372 | json={'role': role}, 373 | error_msg=f"Membership {login}@{team} not updated" 374 | ) 375 | 376 | def delete_team_member(self, owner, team, login): 377 | """Add user into the team 378 | """ 379 | return self.request( 380 | method='PUT', 381 | url=(f"orgs/{owner}/" 382 | f"teams/{team}/memberships/{login}"), 383 | error_msg=f"Membership {login}@{team} not deleted" 384 | ) 385 | 386 | def get_org_members(self, owner): 387 | """Get organization members""" 388 | return self.paginated_request( 389 | url=f'orgs/{owner}/members', 390 | error_msg="Cannot fetch organizaition members" 391 | ) 392 | 393 | def update_org_membership(self, owner, username, role): 394 | """Set organization membership for the user""" 395 | return self.request( 396 | method="PUT", 397 | url=f"orgs/{owner}/memberships/{username}", 398 | json={"role": role}, 399 | error_msg=f"Membership for user {username} not updated" 400 | ) 401 | 402 | def delete_org_membership(self, owner, username): 403 | """Delete organization membership for the user""" 404 | return self.request( 405 | method="DELETE", 406 | url=f"orgs/{owner}/memberships/{username}", 407 | error_msg=f"Membership for user {username} not deleted" 408 | ) 409 | 410 | def get_org_invitations(self, owner): 411 | """List existing user invitations 412 | """ 413 | return self.paginated_request( 414 | url=f"orgs/{owner}/invitations", 415 | error_msg=f"Cannot fetch invitations for {owner}" 416 | ) 417 | 418 | def create_organization_invitation(self, owner, user, role='direct_member'): 419 | """Send Invitation to join the org""" 420 | return self.request( 421 | method='POST', 422 | url=f"orgs/{owner}/invitations", 423 | json={'invitee_id': user['id'], 'role': role}, 424 | error_msg=f"Member {user['id']} not invited" 425 | ) 426 | 427 | def delete_org_invitation(self, owner, id): 428 | """Cancel organization invitation""" 429 | return self.request( 430 | method='DELETE', 431 | url=f"orgs/{owner}/invitations/{id}", 432 | error_msg=f"Organization invite {owner}/{id} not cacnelled" 433 | ) 434 | 435 | def delete_org_member(self, owner, login): 436 | """Remove organization member""" 437 | return self.request( 438 | method='DELETE', 439 | url=f"orgs/{owner}/members/{login}", 440 | error_msg=f"Organization member {owner}/{login} not removed" 441 | ) 442 | 443 | def get_user(self, login): 444 | """Get user info""" 445 | user = None 446 | if login not in self._users_cache: 447 | user = self.request( 448 | method='GET', 449 | url=f"users/{login}", 450 | ) 451 | if user: 452 | self._users_cache[login] = user 453 | user = self._users_cache.get(login) 454 | return user 455 | 456 | def get_repo(self, owner, repo, ignore_missing=False): 457 | """Get repository information""" 458 | return self.request( 459 | method='GET', 460 | url=f"repos/{owner}/{repo}", 461 | error_msg=f"Repo {repo}@{owner} cannot be fetched", 462 | ignore_missing=ignore_missing 463 | ) 464 | 465 | def create_repo(self, owner, repo, **args): 466 | if not args: 467 | args = dict() 468 | args['name'] = repo 469 | rsp = self.request( 470 | method='POST', 471 | url=f"orgs/{owner}/repos", 472 | json=args, 473 | error_msg=f"Repo {repo}@{owner} cannot be created" 474 | ) 475 | return rsp 476 | 477 | def update_repo(self, owner, repo, **kwargs): 478 | """Update repository options""" 479 | data = dict() 480 | for attr in REPOSITORY_UPDATABLE_ATTRIBUTES: 481 | if attr in kwargs and kwargs[attr] is not None: 482 | data[attr] = kwargs[attr] 483 | 484 | rsp = self.request( 485 | method='PATCH', 486 | url=f'repos/{owner}/{repo}', 487 | json=data, 488 | error_msg=f"Repo {repo}@{owner} cannot be updated" 489 | ) 490 | return rsp 491 | 492 | def delete_repo(self, owner, repo): 493 | """Delete repository""" 494 | rsp = self.request( 495 | method='DELETE', 496 | url=f'repos/{owner}/{repo}', 497 | error_msg=f"Repo {repo}@{owner} cannot be deleted" 498 | ) 499 | return rsp 500 | 501 | def get_repo_topics(self, owner, repo): 502 | """Get repository topics""" 503 | headers = dict( 504 | Accept='application/vnd.github.mercy-preview+json' 505 | ) 506 | 507 | rsp = self.request( 508 | method='GET', 509 | url=f'repos/{owner}/{repo}/topics', 510 | headers=headers, 511 | error_msg=f"Repo {repo}@{owner} topics cannot be updated" 512 | ) 513 | return rsp['names'] 514 | 515 | def update_repo_topics(self, owner, repo, topics): 516 | """Set repository topics""" 517 | rsp = self.request( 518 | method='PUT', 519 | url=f'repos/{owner}/{repo}/topics', 520 | json={'names': topics}, 521 | error_msg=f"Repo {repo}@{owner} topics cannot be updated" 522 | ) 523 | 524 | return rsp 525 | 526 | def get_branch_protection(self, owner, repo, branch): 527 | """Get branch protection rules""" 528 | rsp = self.request( 529 | method='GET', 530 | url=(f'repos/{owner}/{repo}/branches/{branch}/protection'), 531 | error_msg=f"Repo {repo}@{owner} branch protection " 532 | f"cannot be fetched" 533 | ) 534 | 535 | return rsp 536 | 537 | def update_branch_protection(self, owner, repo, branch, target): 538 | """Set branch protection rules""" 539 | # Checks takes precedence as being more fine granular 540 | required_status_checks = target.get('required_status_checks', {}) 541 | if required_status_checks: 542 | checks = required_status_checks.get('checks', '') 543 | contexts = required_status_checks.get('contexts', []) 544 | if checks: 545 | target['required_status_checks'].pop('contexts', '') 546 | elif contexts: 547 | target['required_status_checks'].pop('checks', '') 548 | # Restrictions is a mandatory param that supports being "null" 549 | restrictions = target.setdefault("restrictions", None) 550 | if restrictions == {}: 551 | target["restrictions"] = None 552 | 553 | self.request( 554 | method='PUT', 555 | url=(f'repos/{owner}/{repo}/branches/{branch}/protection'), 556 | json=target, 557 | error_msg=f"Repo {repo}@{owner} branch protection cannot be updated" 558 | ) 559 | 560 | return True 561 | 562 | def get_repo_teams(self, owner, repo): 563 | """Get repo teams""" 564 | rsp = self.paginated_request( 565 | method='GET', 566 | url=(f"repos/{owner}/{repo}/teams"), 567 | error_msg=f"Cannot fetch team {owner}/{repo} teams" 568 | ) 569 | 570 | return rsp 571 | 572 | def get_repo_collaborators(self, owner, repo, affiliation='direct'): 573 | """Get repo collaborators""" 574 | return self.paginated_request( 575 | method='GET', 576 | url=(f"repos/{owner}/{repo}/collaborators?affiliation={affiliation}"), 577 | error_msg=f"Cannot fetch repo {owner}/{repo} collaborators" 578 | ) 579 | 580 | def delete_repo_collaborator(self, owner, repo, username): 581 | """Delete repo collaborator""" 582 | self.request( 583 | method='DELETE', 584 | url=(f"repos/{owner}/{repo}/collaborators/{username}"), 585 | error_msg=f"Cannot delete {owner}/{repo} collaborator {username}" 586 | ) 587 | 588 | def update_repo_collaborator(self, owner, repo, username, permission='pull'): 589 | """Add/Update repo collaborator""" 590 | return self.request( 591 | method='PUT', 592 | url=(f"repos/{owner}/{repo}/collaborators/{username}"), 593 | json={ 594 | 'permission': permission 595 | }, 596 | error_msg=f"Cannot add repo {owner}/{repo} collaborator" 597 | ) 598 | 599 | def get_members_with_role(self, owner): 600 | """Fetch current organization members with the role using GraphQL""" 601 | members = [] 602 | params = {'owner': owner} 603 | url = f"{self.gh_url}/graphql" 604 | 605 | while True: 606 | query = self._prepare_graphql_query( 607 | QUERY_MEMBERS, params 608 | ) 609 | body, response, info = self._request( 610 | method="POST", url=url, 611 | data=self.ansible.jsonify(query) 612 | ) 613 | status = info['status'] 614 | data = json.loads(body) 615 | errors = data.get("errors") 616 | if status >= 400 or errors: 617 | if not errors: 618 | errors = response.text 619 | self.save_error(f"Error performing query: {errors}") 620 | break 621 | data = data["data"]["organization"]["membersWithRole"] 622 | for item in data["edges"]: 623 | members.append({ 624 | "login": item["node"]["login"].lower(), 625 | "role": 'Member' if item["role"] == 'MEMBER' else 'Owner' 626 | }) 627 | if data["pageInfo"]["hasNextPage"]: 628 | # Put cursor to next page into params 629 | params['memberCursor'] = data["pageInfo"]["endCursor"] 630 | else: 631 | break 632 | return members 633 | 634 | def _process_member(self, owner, login, role, members, check=True): 635 | """Process current member - check role""" 636 | changed = False 637 | # Pop member from current members 638 | current_state = members.pop(login, {}) 639 | if (current_state.get('role', '').lower() != role.lower()): 640 | changed = True 641 | msg = f"role updated to {role.lower()}" 642 | if not check: 643 | self.update_org_membership( 644 | owner, login, role) 645 | else: 646 | msg = role 647 | return (changed, msg) 648 | 649 | def _process_invitee(self, owner, login, role, invites, check=True): 650 | """Process invite for single user""" 651 | target_invite_role = 'direct_member' 652 | if role.lower() == 'owner': 653 | target_invite_role = 'admin' 654 | # Pop user from invites 655 | invite = invites.pop(login, {}) 656 | if invite: 657 | if invite['role'] != target_invite_role: 658 | # Invitation with wrong role - discard 659 | if not check: 660 | self.delete_org_invitation( 661 | owner, invite['id']) 662 | else: 663 | return (False, 'Already invited') 664 | 665 | user = self.get_user(login) 666 | if not check: 667 | self.create_organization_invitation( 668 | owner, user, target_invite_role) 669 | return (True, 'Invited') 670 | 671 | def _manage_org_members(self, org, target_members, exclusive=False, check=True): 672 | status = dict() 673 | changed = False 674 | invites_supported = True 675 | 676 | # Try to read current members 677 | try: 678 | current_members = {x['login'].lower(): x for x in 679 | self.get_members_with_role(org)} 680 | except Exception as ex: 681 | self.fail_json( 682 | msg='Cannot fetch current organization members', 683 | errors=self.errors, 684 | ex=str(ex)) 685 | 686 | # Try to read current invites 687 | try: 688 | current_invites = {x['login'].lower(): x for x in 689 | self.get_org_invitations(org)} 690 | except Exception: 691 | # GH Enterprise does not support invites, no worries 692 | invites_supported = False 693 | current_invites = {} 694 | 695 | # Loop through target users 696 | for member in target_members: 697 | login = member['login'].lower() 698 | target_role = member['role'].lower() 699 | msg = None 700 | try: 701 | if login not in current_members: 702 | if invites_supported: 703 | # Process invites 704 | (is_changed, msg) = self._process_invitee( 705 | org, 706 | login, 707 | target_role, 708 | current_invites, 709 | check 710 | ) 711 | else: 712 | is_changed = True 713 | msg = f"invited as {target_role}" 714 | if not check: 715 | self.update_org_membership( 716 | org, 717 | login, 718 | target_role, 719 | ) 720 | else: 721 | # Process member 722 | (is_changed, msg) = self._process_member( 723 | org, 724 | login, 725 | target_role, 726 | current_members, 727 | check 728 | ) 729 | 730 | except Exception as ex: 731 | self.save_error(f"Error processing member {login}:" 732 | f"{str(ex)}") 733 | (is_changed, msg) = (False, str(ex)) 734 | 735 | status[login] = msg 736 | if is_changed: 737 | changed = True 738 | 739 | # Cancel invitations for members not in the target state 740 | for login, invite in current_invites.items(): 741 | changed = True 742 | if not check: 743 | self.delete_org_invitation( 744 | org, 745 | invite['id'] 746 | ) 747 | status[login] = 'Invite cancelled' 748 | 749 | # Report current members that are not in the target state 750 | for member, ignore in current_members.items(): 751 | if not exclusive: 752 | status[member] = 'Not Managed' 753 | else: 754 | if not check: 755 | self.delete_org_member( 756 | org, 757 | member 758 | ) 759 | status[member] = 'Removed' 760 | 761 | return (changed, status) 762 | 763 | def _is_team_update_necessary(self, target, current): 764 | if ( 765 | target.get('name') != current.get('name') 766 | ): 767 | return True 768 | if ( 769 | target.get('description') != current.get('description') 770 | ): 771 | return True 772 | if ( 773 | target.get('privacy') != current.get('privacy') 774 | ): 775 | return True 776 | return False 777 | 778 | def _manage_org_team( 779 | self, owner, slug, current, target, exclusive=False, check_mode=True 780 | ): 781 | changed = False 782 | status = dict() 783 | is_existing = True 784 | team_status = 'unchanged' 785 | if not current: 786 | 787 | # Create new team 788 | changed = True 789 | team_status = 'created' 790 | if not check_mode: 791 | current = self.create_team( 792 | owner=owner, 793 | name=slug, 794 | description=target.get('description'), 795 | privacy=target.get('privacy'), 796 | parent=target.get('parent'), 797 | maintainers=target.get('maintainer', []) 798 | ) 799 | if current is None: 800 | self.save_error(f"Unable to create team: {slug} / {target} with " 801 | f"maintainers : {', '.join(target.get('maintainer', []))} " 802 | "(all maintainers must be completely onboarded)") 803 | slug = current['slug'] 804 | else: 805 | is_existing = False 806 | else: 807 | slug = current['slug'] 808 | 809 | if not target['name']: 810 | target['name'] = slug 811 | if ( 812 | is_existing 813 | and self._is_team_update_necessary(target, current) 814 | ): 815 | # Update Team 816 | changed = True 817 | team_status = 'updated' 818 | if not check_mode: 819 | self.update_team(owner, slug, **target) 820 | 821 | status['slug'] = slug 822 | status['status'] = team_status 823 | for attr in ['name', 'description', 'privacy']: 824 | status[attr] = target.get(attr) 825 | 826 | if is_existing: 827 | current_members = { 828 | x['login']: x for x in self.get_team_members( 829 | owner, slug, role='member') 830 | } 831 | current_maintainers = { 832 | x['login']: x for x in self.get_team_members( 833 | owner, slug, role='maintainer') 834 | } 835 | else: 836 | current_members = {} 837 | current_maintainers = {} 838 | 839 | status['members'] = dict() 840 | target_members = target.get('members', []) or [] 841 | if 'member' in target: 842 | target_members = target.get('member', []) 843 | for login in target_members: 844 | # Member should exist 845 | if login not in current_members: 846 | changed = True 847 | if not check_mode: 848 | self.set_team_member( 849 | owner, slug, login, role='member') 850 | status['members'][login] = 'Added' 851 | else: 852 | status['members'][login] = 'Present' 853 | current_members.pop(login, None) 854 | 855 | status['maintainers'] = dict() 856 | target_maintainers = target.get('maintainers', []) or [] 857 | if 'maintainer' in target: 858 | target_maintainers = target.get('maintainer', []) 859 | for login in target_maintainers: 860 | # Maintainer should exist 861 | if login not in current_maintainers: 862 | changed = True 863 | if not check_mode: 864 | self.set_team_member( 865 | owner, slug, login, role='maintainer') 866 | status['maintainers'][login] = 'Added' 867 | else: 868 | status['maintainers'][login] = 'Present' 869 | current_maintainers.pop(login, None) 870 | 871 | # In the exclusive mode drop maintainers and members not present in the 872 | # target state 873 | if exclusive: 874 | for member, ignore in current_members.items(): 875 | changed = True 876 | if not check_mode: 877 | self.delete_team_member( 878 | owner, slug, member) 879 | status['members'][member] = 'removed' 880 | for member, ignore in current_maintainers.items(): 881 | changed = True 882 | if not check_mode: 883 | self.delete_team_member( 884 | owner, slug, member) 885 | status['maintainers'][member] = 'removed' 886 | return (changed, status) 887 | 888 | def _manage_org_teams(self, owner, teams, exclusive=False, check_mode=True): 889 | # Get current org teams 890 | status = dict() 891 | changed = False 892 | current_teams = list(self.get_owner_teams(owner)) 893 | required_team_slugs = [] 894 | if current_teams is None: 895 | self.fail_json( 896 | msg=f'Cannot fetch current teams for {owner}', 897 | errors=self.errors) 898 | 899 | # Go over teams required to exist 900 | for team in teams: 901 | slug = team.get('slug') 902 | required_team_slugs.append(slug) 903 | current = None 904 | # Find current team 905 | for t in current_teams: 906 | if t['slug'].lower() == slug.lower(): 907 | current = t 908 | break 909 | 910 | (is_changed, status[slug]) = self._manage_org_team( 911 | owner, 912 | slug, 913 | current, 914 | team, 915 | exclusive, 916 | check_mode 917 | ) 918 | if is_changed: 919 | changed = True 920 | if exclusive: 921 | for team in current_teams: 922 | slug = team['slug'] 923 | if slug not in required_team_slugs: 924 | changed = True 925 | status[slug] = {'status': 'deleted'} 926 | if not check_mode: 927 | self.delete_team( 928 | owner, 929 | slug 930 | ) 931 | 932 | return (changed, status) 933 | 934 | def _is_repo_update_needed(self, current, target): 935 | for attr in REPOSITORY_UPDATABLE_ATTRIBUTES: 936 | if attr in target and target[attr] != current.get(attr): 937 | return True 938 | 939 | def _is_branch_protection_update_needed( 940 | self, owner, repo, branch, target, current=None 941 | ): 942 | if not current: 943 | current = self.get_branch_protection(owner, repo, branch) 944 | 945 | if not current: 946 | return True 947 | else: 948 | for attr in ['allow_deletions', 'allow_force_pushes', 949 | 'allow_fork_syncing', 'enforce_admins', 950 | 'required_linear_history', 951 | 'required_conversation_resolution']: 952 | if ( 953 | attr in target 954 | and ( 955 | attr not in current 956 | or target[attr] != current[attr]['enabled'] 957 | ) 958 | ): 959 | return True 960 | 961 | current_restrictions = current.get('restrictions') 962 | target_restrictions = target.get('restrictions') 963 | current_pr_review = current.get('required_pull_request_reviews', {}) 964 | target_pr_review = target.get('required_pull_request_reviews', {}) 965 | current_status_checks = current.get('required_status_checks', {}) 966 | target_status_checks = target.get('required_status_checks', {}) 967 | if (current_status_checks.get( 968 | 'strict', False) != target_status_checks.get( 969 | 'strict', False)): 970 | return True 971 | 972 | # rsc.checks: 973 | # Only checks or contexts can be present 974 | current_checks = current_status_checks.get('checks', []) or [] 975 | target_checks = target_status_checks.get('checks', []) or [] 976 | if current_checks or target_checks: 977 | if ( 978 | set( 979 | [f"{x.get('context')}:{x.get('app_id')}" for x in 980 | current_checks] 981 | ) != set( 982 | [f"{x.get('context')}:{x.get('app_id')}" for x in 983 | target_checks] 984 | ) 985 | ): 986 | return True 987 | else: 988 | # checks were not present, process contexts 989 | current_contexts = current_status_checks.get('contexts', []) or [] 990 | target_contexts = target_status_checks.get('contexts', []) or [] 991 | if ( 992 | set( 993 | current_contexts 994 | ) != set( 995 | target_contexts 996 | ) 997 | ): 998 | return True 999 | 1000 | # GH api supports "null" as a way to disable restrictions on the branch 1001 | if not target_restrictions and current_restrictions or target_restrictions and not current_restrictions: 1002 | return True 1003 | if target_restrictions: 1004 | for case in [ 1005 | ('users', 'login'), 1006 | ('teams', 'slug'), 1007 | ('apps', 'slug') 1008 | ]: 1009 | if not current_restrictions or case[0] not in current_restrictions: 1010 | return True 1011 | if ( 1012 | set( 1013 | [x[case[1]] for x in current_restrictions[case[0]]] 1014 | ) != set(target_restrictions[case[0]]) 1015 | ): 1016 | return True 1017 | 1018 | if target_pr_review: 1019 | for attr in ['dismiss_stale_reviews', 1020 | 'require_code_owner_reviews', 1021 | 'required_approving_review_count']: 1022 | if ( 1023 | attr in target_pr_review 1024 | and target_pr_review[attr] != current_pr_review.get( 1025 | attr, False) 1026 | ): 1027 | return True 1028 | 1029 | if 'dismissal_restrictions' in target_pr_review: 1030 | t = target_pr_review['dismissal_restrictions'] 1031 | 1032 | if 'dismissal_restrictions' not in current_pr_review: 1033 | return True 1034 | 1035 | c = current_pr_review['dismissal_restrictions'] 1036 | 1037 | for case in ['users', 'teams']: 1038 | if ( 1039 | set( 1040 | [x['login'] for x in c.get(case, [])] 1041 | ) != set(t[case]) 1042 | ): 1043 | return True 1044 | 1045 | return False 1046 | 1047 | def _manage_repo_teams( 1048 | self, owner, repo_name, target, check_mode=False 1049 | ): 1050 | """Manage repository teams""" 1051 | changed = False 1052 | current_teams = {x['slug']: x['permission'] for x in 1053 | self.get_repo_teams(owner, repo_name) or []} 1054 | target_teams = {x['slug']: x['permission'] for x in 1055 | target} 1056 | if current_teams != target_teams: 1057 | changed = True 1058 | # Short check showed mismatch 1059 | for team, current_priv in current_teams.items(): 1060 | target_priv = target_teams.pop(team, '') 1061 | if not target_priv: 1062 | if not check_mode: 1063 | self.delete_team_repo_access( 1064 | owner, team, repo_name) 1065 | continue 1066 | if target_priv != current_priv: 1067 | if not check_mode: 1068 | self.update_team_repo_permissions2( 1069 | owner, team, owner, repo_name, target_priv) 1070 | # target_teams not contain remainings 1071 | for team, target_priv in target_teams.items(): 1072 | if not check_mode: 1073 | self.update_team_repo_permissions2( 1074 | owner, team, owner, repo_name, target_priv) 1075 | 1076 | return changed 1077 | 1078 | def _manage_repo_collaborators( 1079 | self, owner, repo_name, target, check_mode=False 1080 | ): 1081 | """Manage repository collaborators""" 1082 | changed = False 1083 | current_collaborators = {x['login']: x['permissions'] for x in 1084 | self.get_repo_collaborators( 1085 | owner, repo_name) or []} 1086 | target_collaborators = {x['username']: x['permission'] for x in 1087 | target} 1088 | if target_collaborators != current_collaborators: 1089 | changed = True 1090 | # Short comparison showed mismatch 1091 | for username, permissions in current_collaborators.items(): 1092 | priv = 'pull' 1093 | if 'push' in permissions and permissions['push']: 1094 | priv = 'push' 1095 | else: 1096 | for p in ['pull', 'triage', 'maintain', 'admin']: 1097 | if permissions[p]: 1098 | priv = p 1099 | break 1100 | target_priv = target_collaborators.pop('username', None) 1101 | if not target_priv: 1102 | # Collaborator should be removed 1103 | if not check_mode: 1104 | self.delete_repo_collaborator( 1105 | owner, repo_name, username) 1106 | continue 1107 | if target_priv != priv: 1108 | # Update priv 1109 | if not check_mode: 1110 | # Update as such is not really working, thus drop and 1111 | # create new 1112 | self.delete_repo_collaborator( 1113 | owner, repo_name, username) 1114 | self.update_repo_collaborator( 1115 | owner, repo_name, username, target_priv) 1116 | # target now contains remainings 1117 | for username, priv in target_collaborators.items(): 1118 | if not check_mode: 1119 | self.update_repo_collaborator( 1120 | owner, repo_name, username, priv) 1121 | return changed 1122 | 1123 | def _manage_repository(self, state, current=None, check_mode=False, **kwargs): 1124 | 1125 | changed = False 1126 | owner = kwargs.pop('owner') 1127 | repo_name = kwargs.pop('name') 1128 | current_repo = current if current else self.get_repo(owner, repo_name, ignore_missing=True) 1129 | if not current_repo: 1130 | changed = True 1131 | if not check_mode: 1132 | current_repo = self.create_repo( 1133 | owner, repo_name, **kwargs) 1134 | else: 1135 | return (changed, kwargs) 1136 | archive = kwargs.pop('archived', False) 1137 | if ( 1138 | current_repo 1139 | and archive and current_repo.get('archived') 1140 | ): 1141 | # Do nothing for the archived repo 1142 | return (changed, current_repo) 1143 | 1144 | if current_repo and self._is_repo_update_needed(current_repo, kwargs): 1145 | changed = True 1146 | if not check_mode: 1147 | current_repo = self.update_repo(owner, repo_name, **kwargs) 1148 | 1149 | # Repo topics 1150 | # TODO(gtema): get rid of this as soon as this becomes part of native 1151 | # repository API 1152 | if current_repo and 'topics' in kwargs: 1153 | current_topics = current_repo['topics'] 1154 | if set(kwargs['topics']) != set(current_topics): 1155 | changed = True 1156 | if not check_mode: 1157 | self.update_repo_topics( 1158 | owner, repo_name, kwargs['topics']) 1159 | current_repo['topics'] = kwargs['topics'] 1160 | 1161 | # Branch protections 1162 | branch_protections = kwargs.pop('branch_protections', []) 1163 | if current_repo and branch_protections is not None: 1164 | current_repo['branch_protections'] = [] 1165 | for bp in branch_protections: 1166 | if ( 1167 | self._is_branch_protection_update_needed( 1168 | owner, repo_name, bp['branch'], bp) 1169 | ): 1170 | changed = True 1171 | if not check_mode: 1172 | self.update_branch_protection( 1173 | owner, repo_name, bp['branch'], bp) 1174 | current_repo['branch_protections'].append(bp) 1175 | 1176 | # Teams 1177 | target_teams = kwargs.get('teams') 1178 | if ( 1179 | current_repo and target_teams is not None 1180 | ): 1181 | changed = self._manage_repo_teams( 1182 | owner, repo_name, target_teams, check_mode) 1183 | 1184 | # Collaborators 1185 | target_collaborators = kwargs.get('collaborators') 1186 | if ( 1187 | current_repo and target_collaborators is not None 1188 | ): 1189 | changed = self._manage_repo_collaborators( 1190 | owner, repo_name, target_collaborators, check_mode) 1191 | 1192 | # If we need to archive - do this after updating everything else 1193 | if ( 1194 | current_repo 1195 | and archive and not current_repo.get('archived') 1196 | ): 1197 | changed = True 1198 | if not check_mode: 1199 | current_repo = self.update_repo( 1200 | owner, repo_name, archived=True) 1201 | 1202 | if current_repo: 1203 | # Get rid of all those XXX_url properties 1204 | for k in list(current_repo.keys()): 1205 | if k.endswith('_url'): 1206 | current_repo.pop(k) 1207 | org = current_repo.pop('organization', None) 1208 | current_repo['organization'] = dict( 1209 | login=org.get('login') 1210 | ) 1211 | owner = current_repo.pop('owner', None) 1212 | if owner: 1213 | current_repo['owner'] = dict( 1214 | login=owner.get('login') 1215 | ) 1216 | 1217 | return (changed, current_repo) 1218 | -------------------------------------------------------------------------------- /plugins/modules/gitea_org_repository.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = ''' 9 | module: gitea_org_repository 10 | short_description: Manage Gitea Organization Repository setting 11 | extends_documentation_fragment: opentelekomcloud.gitcontrol.gitea 12 | version_added: "0.2.0" 13 | author: "Artem Goncharov (@gtema)" 14 | description: 15 | - Manages organization repositories inside of the organization repository 16 | options: 17 | owner: 18 | description: Name of the GitHub organization 19 | type: str 20 | required: True 21 | name: 22 | description: Repository name 23 | type: str 24 | required: True 25 | state: 26 | description: Repository state 27 | type: str 28 | choices: [present, absent] 29 | default: present 30 | description: 31 | description: a short description of the repository. 32 | type: str 33 | required: False 34 | allow_manual_merge: 35 | description: | 36 | either true to allow mark pr as merged manually, or false to prevent it. 37 | has_pull_requests must be true. 38 | type: bool 39 | required: False 40 | allow_merge_commits: 41 | description: | 42 | either true to allow merging pull requests with a merge commit, or false 43 | to prevent merging pull requests with merge commits. has_pull_requests must be true. 44 | type: bool 45 | required: False 46 | allow_rebase: 47 | description: | 48 | either true to allow rebase-merging pull requests, or false to prevent rebase-merging. has_pull_requests must be true. 49 | type: bool 50 | required: False 51 | allow_rebase_explicit: 52 | description: | 53 | either true to allow rebase with explicit merge commits (--no-ff), or false to prevent rebase with explicit merge commits. has_pull_requests must be true. 54 | type: bool 55 | required: False 56 | allow_rebase_update: 57 | description: | 58 | either true to allow updating pull request branch by rebase, or false to prevent it. has_pull_requests must be true. 59 | type: bool 60 | required: False 61 | allow_squash_merge: 62 | description: | 63 | either true to allow squash-merging pull requests, or false to prevent squash-merging. has_pull_requests must be true. 64 | type: bool 65 | required: False 66 | archived: 67 | description: | 68 | set to true to archive this repository. 69 | type: bool 70 | required: False 71 | default: False 72 | autodetect_manual_merge: 73 | description: | 74 | either true to enable AutodetectManualMerge, or false to prevent it. has_pull_requests must be true, Note: In some special cases, misjudgments can occur. 75 | type: bool 76 | required: False 77 | auto_init: 78 | description: | 79 | Whether the repository should be auto-initialized? 80 | type: bool 81 | required: False 82 | default_branch: 83 | description: | 84 | sets the default branch for this repository. 85 | type: str 86 | required: False 87 | default_delete_branch_after_merge: 88 | description: | 89 | set to true to delete pr branch after merge by default 90 | type: bool 91 | required: False 92 | default_merge_style: 93 | description: | 94 | set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash". has_pull_requests must be true. 95 | type: str 96 | choices: [merge, rebase, rebase-merge, squash] 97 | required: False 98 | enable_prune: 99 | description: | 100 | enable prune - remove obsolete remote-tracking references 101 | type: bool 102 | required: False 103 | gitignores: 104 | description: | 105 | Gitignores to use 106 | type: str 107 | required: False 108 | has_issues: 109 | description: | 110 | either true to enable issues for this repository or false to disable them. 111 | type: bool 112 | required: False 113 | has_projects: 114 | description: | 115 | either true to enable project unit, or false to disable them. 116 | type: bool 117 | required: False 118 | has_pull_requests: 119 | description: | 120 | either true to allow pull requests, or false to prevent pull request. 121 | type: bool 122 | required: False 123 | has_wiki: 124 | description: | 125 | either true to enable the wiki for this repository or false to disable it. 126 | type: bool 127 | required: False 128 | ignore_whitespace_conflicts: 129 | description: | 130 | either true to ignore whitespace for conflicts, or false to not ignore whitespace. has_pull_requests must be true. 131 | type: bool 132 | required: False 133 | issue_labels: 134 | description: | 135 | Label-Set to use 136 | type: str 137 | required: False 138 | license: 139 | description: | 140 | License to use 141 | type: str 142 | required: False 143 | private: 144 | description: | 145 | either true to make the repository private or false to make it public. 146 | Note: you will get a 422 error if the organization restricts changing repository visibility to organization 147 | owners and a non-owner tries to change the value of private. 148 | type: bool 149 | required: False 150 | readme: 151 | description: | 152 | Readme of the repository to create 153 | type: str 154 | required: False 155 | template: 156 | description: | 157 | either true to make this repository a template or false to make it a normal repository 158 | type: bool 159 | required: False 160 | trust_model: 161 | description: | 162 | TrustModel of the repository 163 | type: str 164 | choices: [default, collaborator, commiter, collaboratorcommiter] 165 | required: False 166 | website: 167 | description: | 168 | a URL with more information about the repository. 169 | type: str 170 | required: False 171 | branch_protections: 172 | description: Branch protection definitions. 173 | type: list 174 | elements: dict 175 | suboptions: 176 | branch_name: 177 | description: Branch name to protect 178 | type: str 179 | required: True 180 | approvals_whitelist_teams: 181 | description: Whitelisted teams for reviews 182 | type: list 183 | elements: str 184 | approvals_whitelist_username: 185 | description: Whitelisted usernames for reviews 186 | type: list 187 | elements: str 188 | block_on_official_review_requests: 189 | description: Merging will not be possible when it has official review requests, even if there are enough approvals. 190 | type: bool 191 | block_on_outdated_branch: 192 | description: Merging will not be possible when head branch is behind base branch. 193 | type: bool 194 | block_on_rejected_reviews: 195 | description: Merging will not be possible when changes are requested by official reviewers, even if there are enough approvals. 196 | type: bool 197 | dismiss_stale_approvals: 198 | description: When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed. 199 | type: bool 200 | enable_approvals_whitelist: 201 | description: | 202 | Only reviews from whitelisted users or teams will count to the required approvals. 203 | Without approval whitelist, reviews from anyone with write access count to the required approvals. 204 | type: bool 205 | enable_merge_whitelist: 206 | description: Allow only whitelisted users or teams to merge pull requests into this branch. 207 | type: bool 208 | enable_push: 209 | description: Anyone with write access will be allowed to push to this branch (but not force push). 210 | type: bool 211 | enable_push_whitelist: 212 | description: Only whitelisted users or teams will be allowed to push to this branch (but not force push). 213 | type: bool 214 | enable_status_check: 215 | description: Require status checks to pass before merging. 216 | type: bool 217 | merge_whitelist_teams: 218 | description: Whitelisted teams for merging 219 | type: list 220 | elements: str 221 | merge_whitelist_usernames: 222 | description: Whitelisted users for merging 223 | type: list 224 | elements: str 225 | protected_file_patterns: 226 | description: Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. 227 | type: str 228 | unprotected_file_patterns: 229 | description: Unprotected files that are allowed to be changed directly if user has write access, bypassing push restriction. 230 | type: str 231 | push_whitelist_deploy_keys: 232 | description: Whitelist deploy keys with write access to push 233 | type: bool 234 | push_whitelist_teams: 235 | description: Whitelisted teams for pushing 236 | type: list 237 | elements: str 238 | push_whitelist_usernames: 239 | description: Whitelisted users for pushing 240 | type: list 241 | elements: str 242 | require_signed_commits: 243 | description: Reject pushes to this branch if they are unsigned or unverifiable. 244 | type: bool 245 | required_approvals: 246 | description: Allow only to merge pull request with enough positive reviews. 247 | type: int 248 | status_check_contexts: 249 | description: Require status checks to pass before merging. 250 | type: list 251 | elements: str 252 | collaborators: 253 | description: | 254 | Repository collaborators with their permissions 255 | type: list 256 | elements: dict 257 | suboptions: 258 | username: 259 | description: Username 260 | type: str 261 | required: True 262 | permission: 263 | description: | 264 | The permission to grant the collaborator. Only valid on 265 | organization-owned repositories. Can be one of: 266 | 267 | * read - can pull 268 | * write - can pull and push, but not administer this repository. 269 | * administrator - can pull, push and administer this repository. 270 | 271 | type: str 272 | choices: [read, write, administrator] 273 | default: read 274 | teams: 275 | description: Repository collaborator teams. Permissions are managed on the team level 276 | type: list 277 | elements: str 278 | 279 | ''' 280 | 281 | 282 | RETURN = ''' 283 | ''' 284 | 285 | 286 | EXAMPLES = ''' 287 | ''' 288 | 289 | 290 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.gitea import ( 291 | GiteaBase 292 | ) 293 | 294 | 295 | class GTOrgRepositoryModule(GiteaBase): 296 | argument_spec = dict( 297 | owner=dict(type='str', required=True), 298 | name=dict(type='str', required=True), 299 | state=dict(type='str', default='present', 300 | choices=['present', 'absent']), 301 | allow_manual_merge=dict(type='bool'), 302 | allow_merge_commits=dict(type='bool'), 303 | allow_rebase=dict(type='bool'), 304 | allow_rebase_explicit=dict(type='bool'), 305 | allow_rebase_update=dict(type='bool'), 306 | allow_squash_merge=dict(type='bool'), 307 | auto_init=dict(type='bool'), 308 | archived=dict(type='bool', default=False), 309 | autodetect_manual_merge=dict(type='bool'), 310 | default_branch=dict(type='str'), 311 | default_delete_branch_after_merge=dict(type='bool'), 312 | default_merge_style=dict( 313 | type='str', 314 | choices=['merge', 'rebase', 'rebase-merge', 'squash']), 315 | description=dict(type='str', required=False), 316 | enable_prune=dict(type='bool'), 317 | gitignores=dict(type='str'), 318 | has_issues=dict(type='bool'), 319 | has_projects=dict(type='bool'), 320 | has_pull_requests=dict(type='bool'), 321 | has_wiki=dict(type='bool'), 322 | ignore_whitespace_conflicts=dict(type='bool'), 323 | issue_labels=dict(type='str'), 324 | license=dict(type='str'), 325 | private=dict(type='bool'), 326 | readme=dict(type='str'), 327 | template=dict(type='bool'), 328 | trust_model=dict( 329 | type='str', 330 | choices=['default', 'collaborator', 'commiter', 'collaboratorcommiter'] 331 | ), 332 | website=dict(type='str'), 333 | branch_protections=dict( 334 | type='list', elements='dict', options=dict( 335 | approvals_whitelist_teams=dict(type='list', elements='str'), 336 | approvals_whitelist_username=dict(type='list', elements='str'), 337 | block_on_official_review_requests=dict(type='bool'), 338 | block_on_outdated_branch=dict(type='bool'), 339 | block_on_rejected_reviews=dict(type='bool'), 340 | branch_name=dict(type='str', required=True), 341 | dismiss_stale_approvals=dict(type='bool'), 342 | enable_approvals_whitelist=dict(type='bool'), 343 | enable_merge_whitelist=dict(type='bool'), 344 | enable_push=dict(type='bool'), 345 | enable_push_whitelist=dict(type='bool'), 346 | enable_status_check=dict(type='bool'), 347 | merge_whitelist_teams=dict(type='list', elements='str'), 348 | merge_whitelist_usernames=dict(type='list', elements='str'), 349 | protected_file_patterns=dict(type='str'), 350 | push_whitelist_deploy_keys=dict(type='bool'), 351 | push_whitelist_teams=dict(type='list', elements='str'), 352 | push_whitelist_usernames=dict(type='list', elements='str'), 353 | require_signed_commits=dict(type='bool'), 354 | required_approvals=dict(type='int'), 355 | status_check_contexts=dict(type='list', elements='str'), 356 | unprotected_file_patterns=dict(type='str') 357 | ) 358 | ), 359 | collaborators=dict( 360 | type='list', elements='dict', options=dict( 361 | username=dict(type='str', required=True), 362 | permission=dict( 363 | type='str', default='read', 364 | choices=['administrator', 'write', 'read'] 365 | ) 366 | ) 367 | ), 368 | teams=dict(type='list', elements='str'), 369 | ) 370 | module_kwargs = dict( 371 | supports_check_mode=True 372 | ) 373 | 374 | def run(self): 375 | changed = False 376 | repo = dict() 377 | 378 | state = self.params.pop('state') 379 | target_attrs = self.params 380 | 381 | current_state = self.get_repo( 382 | target_attrs['owner'], 383 | target_attrs['name'], 384 | ignore_missing=True 385 | ) 386 | 387 | if current_state and state == 'absent': 388 | changed = True 389 | if not self.ansible.check_mode: 390 | self.delete_repo( 391 | target_attrs['owner'], 392 | target_attrs['name'] 393 | ) 394 | repo = {} 395 | else: 396 | changed, repo = self._manage_repository( 397 | state=state, 398 | current=current_state, 399 | check_mode=self.ansible.check_mode, 400 | **target_attrs 401 | ) 402 | if len(self.errors) == 0: 403 | self.exit_json( 404 | changed=changed, 405 | repository=repo 406 | ) 407 | else: 408 | self.fail_json( 409 | msg='Failures occured', 410 | errors=self.errors, 411 | repository=repo 412 | ) 413 | 414 | 415 | def main(): 416 | module = GTOrgRepositoryModule() 417 | module() 418 | 419 | 420 | if __name__ == "__main__": 421 | main() 422 | -------------------------------------------------------------------------------- /plugins/modules/github_org_members.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = ''' 9 | module: github_org_members 10 | short_description: Manage GitHub Organization Members 11 | extends_documentation_fragment: opentelekomcloud.gitcontrol.github 12 | version_added: "0.0.2" 13 | author: "Artem Goncharov (@gtema)" 14 | description: 15 | - Manages organization members inside of the organization repository 16 | options: 17 | organization: 18 | description: Name of the GitHub organization 19 | type: str 20 | required: True 21 | members: 22 | description: Dictionary of organization members with permissions 23 | type: list 24 | required: True 25 | elements: dict 26 | suboptions: 27 | login: 28 | description: User login. 29 | type: str 30 | required: True 31 | name: 32 | description: Optional user name (for the reference, it is not used) 33 | type: str 34 | required: False 35 | role: 36 | description: Member role. 37 | type: str 38 | choices: [member, admin] 39 | default: member 40 | exclusive: 41 | description: | 42 | Flag specifying whether unmanaged organization members should be removed 43 | or not. 44 | type: bool 45 | default: False 46 | required: False 47 | ''' 48 | 49 | 50 | RETURN = ''' 51 | members: 52 | description: List of organization member statuses 53 | returned: always 54 | type: list 55 | elements: str 56 | ''' 57 | 58 | 59 | EXAMPLES = ''' 60 | - name: Apply org members 61 | opentelekomcloud.gitcontrol.github_org_members: 62 | token: "{{ secret }}" 63 | organization: "test_org" 64 | members: 65 | - login: github_user1 66 | name: "some not required user name" 67 | role: "member" 68 | ''' 69 | 70 | 71 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.github import ( 72 | GitHubBase 73 | ) 74 | 75 | 76 | class GHOrgMembersModule(GitHubBase): 77 | argument_spec = dict( 78 | organization=dict(type='str', required=True), 79 | members=dict( 80 | type='list', 81 | required=True, 82 | elements='dict', 83 | options=dict( 84 | login=dict(type='str', required=True), 85 | name=dict(type='str', required=False), 86 | role=dict(type='str', choices=['member', 'admin'], 87 | default='member', required=False), 88 | ), 89 | ), 90 | exclusive=dict(type='bool', default=False) 91 | ) 92 | module_kwargs = dict( 93 | supports_check_mode=True 94 | ) 95 | 96 | def run(self): 97 | status = dict() 98 | changed = False 99 | 100 | (changed, status) = self._manage_org_members( 101 | self.params['organization'], 102 | self.params['members'], 103 | self.params['exclusive'], 104 | self.ansible.check_mode 105 | ) 106 | 107 | if len(self.errors) == 0: 108 | self.exit_json( 109 | changed=changed, 110 | members=status 111 | ) 112 | else: 113 | self.fail_json( 114 | msg='Failures occured', 115 | errors=self.errors, 116 | members=status 117 | ) 118 | 119 | 120 | def main(): 121 | module = GHOrgMembersModule() 122 | module() 123 | 124 | 125 | if __name__ == "__main__": 126 | main() 127 | -------------------------------------------------------------------------------- /plugins/modules/github_org_repository.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = ''' 9 | module: github_org_repository 10 | short_description: Manage GitHub Organization Repository setting 11 | extends_documentation_fragment: opentelekomcloud.gitcontrol.github 12 | version_added: "0.0.2" 13 | author: "Artem Goncharov (@gtema)" 14 | description: 15 | - Manages organization repositories inside of the organization repository 16 | options: 17 | owner: 18 | description: Name of the GitHub organization 19 | type: str 20 | required: True 21 | name: 22 | description: Repository name 23 | type: str 24 | required: True 25 | state: 26 | description: Repository state 27 | type: str 28 | choices: [present, absent] 29 | default: present 30 | description: 31 | description: Repository description 32 | type: str 33 | required: False 34 | homepage: 35 | description: Repository homepage link 36 | type: str 37 | required: False 38 | private: 39 | description: Whether the repository is private. 40 | type: bool 41 | default: False 42 | visibility: 43 | description: | 44 | Can be public or private. If your organization is associated with an 45 | enterprise account using GitHub Enterprise Cloud or GitHub Enterprise 46 | Server 2.20+, visibility can also be internal. 47 | type: str 48 | choices: [public, private, internal] 49 | default: public 50 | has_issues: 51 | description: Either true to enable issues for this repository or false to disable them. 52 | type: bool 53 | default: True 54 | has_projects: 55 | description: | 56 | Either true to enable projects for this repository or false to disable 57 | them. Note: If you're creating a repository in an organization that has 58 | disabled repository projects, the default is false, and if you pass true, 59 | the API returns an error. 60 | type: bool 61 | default: True 62 | has_wiki: 63 | description: | 64 | Either true to enable the wiki for this repository or false to disable it. 65 | type: bool 66 | default: True 67 | is_template: 68 | description: | 69 | Either true to make this repo available as a template repository or false 70 | to prevent it. 71 | type: bool 72 | default: False 73 | auto_init: 74 | description: | 75 | Pass true to create an initial commit with empty README. 76 | type: bool 77 | default: False 78 | gitignore_template: 79 | description: | 80 | Desired language or platform .gitignore template to apply. Use the name 81 | of the template without the extension. For example, "Haskell". 82 | type: str 83 | required: False 84 | license_template: 85 | description: | 86 | Choose an open source license template that best suits your needs, and 87 | then use the license keyword as the license_template string. For example, 88 | "mit" or "mpl-2.0". 89 | type: str 90 | allow_squash_merge: 91 | description: | 92 | Either true to allow squash-merging pull requests, or false to prevent 93 | squash-merging. 94 | type: bool 95 | default: True 96 | allow_forking: 97 | description: | 98 | Either true to allow private forks, or false to prevent private forks. 99 | Please note that setting this attribute requires organization to overall 100 | allow forking of private repositories, otherwise GitHub refuses setting 101 | this variable to any value. 102 | type: bool 103 | allow_merge_commit: 104 | description: | 105 | Either true to allow merging pull requests with a merge commit, or false 106 | to prevent merging pull requests with merge commits. 107 | type: bool 108 | default: True 109 | allow_rebase_merge: 110 | description: | 111 | Either true to allow rebase-merging pull requests, or false to prevent 112 | rebase-merging. 113 | type: bool 114 | default: True 115 | allow_auto_merge: 116 | description: | 117 | Either true to allow auto-merge on pull requests, or false to disallow 118 | auto-merge. 119 | type: bool 120 | default: False 121 | allow_update_branch: 122 | description: | 123 | Either true to always allow a pull request head branch that is behind its 124 | base branch to be updated even if it is not required to be up to date 125 | before merging, or false otherwise. Default: false 126 | type: bool 127 | default: False 128 | delete_branch_on_merge: 129 | description: | 130 | Either true to allow automatically deleting head branches when pull 131 | requests are merged, or false to prevent automatic deletion. 132 | type: bool 133 | default: False 134 | default_branch: 135 | description: | 136 | Default branch name for the repository. 137 | type: str 138 | archived: 139 | description: | 140 | true to archive this repository. Note: You cannot unarchive repositories 141 | through the API. 142 | type: bool 143 | default: False 144 | topics: 145 | description: | 146 | An array of topics to add to the repository. 147 | type: list 148 | elements: str 149 | default: [] 150 | teams: 151 | description: | 152 | Repository teams with their permissions 153 | type: list 154 | elements: dict 155 | suboptions: 156 | slug: 157 | description: Team slug 158 | type: str 159 | required: True 160 | permission: 161 | description: | 162 | The permission to grant to the team for this project. Can be one of: 163 | 164 | * pull - can pull, but not push to or administer this repository. 165 | * push - can pull and push, but not administer this repository. 166 | * admin - can pull, push and administer this repository. 167 | * maintain - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions. 168 | * triage - Recommended for contributors who need to proactively manage issues and pull requests without write access. 169 | type: str 170 | choices: [pull, push, admin, maintain, triage] 171 | default: pull 172 | collaborators: 173 | description: | 174 | Repository collaborators with their permissions 175 | type: list 176 | elements: dict 177 | suboptions: 178 | username: 179 | description: Username 180 | type: str 181 | required: True 182 | permission: 183 | description: | 184 | The permission to grant the collaborator. Only valid on 185 | organization-owned repositories. Can be one of: 186 | 187 | * pull - can pull, but not push to or administer this repository. 188 | * push - can pull and push, but not administer this repository. 189 | * admin - can pull, push and administer this repository. 190 | * maintain - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions. 191 | * triage - Recommended for contributors who need to proactively manage issues and pull requests without write access. 192 | 193 | type: str 194 | choices: [pull, push, admin, maintain, triage] 195 | default: pull 196 | branch_protections: 197 | description: | 198 | Branch protection definitions. 199 | type: list 200 | elements: dict 201 | suboptions: 202 | branch: 203 | description: Branch name to protect. 204 | type: str 205 | required: True 206 | required_status_checks: 207 | description: Require status checks to pass before merging. 208 | type: dict 209 | required: True 210 | suboptions: 211 | strict: 212 | description: Require branches to be up to date before merging. 213 | type: bool 214 | default: False 215 | contexts: 216 | description: | 217 | The list of status checks to require in order to merge into this 218 | branch. If any of these checks have recently been set by a 219 | particular GitHub App, they will be required to come from that 220 | app in future for the branch to merge. Use checks instead of 221 | contexts for more fine-grained control. 222 | type: list 223 | elements: str 224 | default: [] 225 | checks: 226 | description: | 227 | The list of status checks to require in order to merge into this 228 | branch. 229 | type: list 230 | elements: dict 231 | suboptions: 232 | context: 233 | description: | 234 | The name of the required check. 235 | type: str 236 | app_id: 237 | description: | 238 | The ID of the GitHub App that must provide this check. Set to 239 | null to accept the check from any source. 240 | type: int 241 | enforce_admins: 242 | description: | 243 | Enforce all configured restrictions for administrators. Set 244 | to true to enforce required status checks for repository 245 | administrators. 246 | type: bool 247 | default: False 248 | required_pull_request_reviews: 249 | description: | 250 | Require at least one approving review on a pull request, 251 | before merging. 252 | type: dict 253 | suboptions: 254 | dismissal_restrictions: 255 | description: | 256 | Specify which users and teams can dismiss pull request reviews. 257 | type: dict 258 | suboptions: 259 | users: 260 | description: | 261 | The list of user logins with dismissal access. 262 | type: list 263 | elements: str 264 | default: [] 265 | teams: 266 | description: | 267 | The list of team slugs with dismissal access. 268 | type: list 269 | elements: str 270 | default: [] 271 | dismiss_stale_reviews: 272 | description: | 273 | Set to true if you want to automatically dismiss approving 274 | reviews when someone pushes a new commit. 275 | type: bool 276 | default: True 277 | require_code_owner_reviews: 278 | description: | 279 | Blocks merging pull requests until code owners review them. 280 | type: bool 281 | default: True 282 | required_approving_review_count: 283 | description: | 284 | Specify the number of reviewers required to approve pull 285 | requests. Use a number between 1 and 6. 286 | type: int 287 | choices: [1, 2, 3, 4, 5] 288 | restrictions: 289 | description: | 290 | Restrict who can push to the protected branch. User, app, 291 | and team restrictions are only available for organization-owned repositories. 292 | type: dict 293 | suboptions: 294 | users: 295 | description: | 296 | The list of user logins with push access. 297 | type: list 298 | elements: str 299 | default: [] 300 | teams: 301 | description: | 302 | The list of team slugs with push access. 303 | type: list 304 | elements: str 305 | default: [] 306 | apps: 307 | description: | 308 | The list of app slugs with push access. 309 | type: list 310 | elements: str 311 | required_linear_history: 312 | description: | 313 | Enforces a linear commit Git history, which prevents anyone from 314 | pushing merge commits to a branch. Set to true to enforce a linear 315 | commit history. 316 | type: bool 317 | default: False 318 | allow_fork_syncing: 319 | description: | 320 | Whether users can pull changes from upstream when the branch is 321 | locked. Set to true to allow fork syncing. Set to false to prevent 322 | fork syncing. Default: false 323 | type: bool 324 | default: false 325 | allow_force_pushes: 326 | description: | 327 | Permits force pushes to the protected branch by anyone with write 328 | access to the repository. Set to true to allow force pushes. 329 | type: bool 330 | default: False 331 | allow_deletions: 332 | description: | 333 | Allows deletion of the protected branch by anyone with write access 334 | to the repository. 335 | type: bool 336 | default: False 337 | required_conversation_resolution: 338 | description: | 339 | Requires all conversations on code to be resolved before a pull 340 | request can be merged into a branch that matches this rule. 341 | type: bool 342 | default: False 343 | ''' 344 | 345 | 346 | RETURN = ''' 347 | ''' 348 | 349 | 350 | EXAMPLES = ''' 351 | ''' 352 | 353 | 354 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.github import ( 355 | GitHubBase 356 | ) 357 | 358 | 359 | class GHOrgRepositoryModule(GitHubBase): 360 | argument_spec = dict( 361 | owner=dict(type='str', required=True), 362 | name=dict(type='str', required=True), 363 | state=dict(type='str', default='present', 364 | choices=['present', 'absent']), 365 | description=dict(type='str', required=False), 366 | homepage=dict(type='str', required=False), 367 | private=dict(type='bool', default=False), 368 | visibility=dict(type='str', default='public', 369 | choices=['public', 'private', 'internal']), 370 | has_issues=dict(type='bool', default=True), 371 | has_projects=dict(type='bool', default=True), 372 | has_wiki=dict(type='bool', default=True), 373 | is_template=dict(type='bool', default=False), 374 | auto_init=dict(type='bool', default=False), 375 | gitignore_template=dict(type='str'), 376 | license_template=dict(type='str'), 377 | allow_forking=dict(type='bool'), 378 | allow_squash_merge=dict(type='bool', default=True), 379 | allow_merge_commit=dict(type='bool', default=True), 380 | allow_rebase_merge=dict(type='bool', default=True), 381 | allow_auto_merge=dict(type='bool', default=False), 382 | allow_update_branch=dict(type='bool', default=False), 383 | delete_branch_on_merge=dict(type='bool', default=False), 384 | default_branch=dict(type='str'), 385 | archived=dict(type='bool', default=False), 386 | topics=dict(type='list', elements='str', default=[]), 387 | branch_protections=dict( 388 | type='list', required=False, elements='dict', options=dict( 389 | allow_deletions=dict(type='bool', default=False), 390 | allow_fork_syncing=dict(type='bool', default=False), 391 | allow_force_pushes=dict(type='bool', default=False), 392 | branch=dict(type='str', required=True), 393 | enforce_admins=dict(type='bool', default=False), 394 | required_conversation_resolution=dict(type='bool', 395 | default=False), 396 | required_status_checks=dict( 397 | type='dict', required=True, 398 | required_one_of=[('contexts', 'checks')], 399 | options=dict( 400 | strict=dict(type='bool', default=False), 401 | contexts=dict(type='list', elements='str', default=[]), 402 | checks=dict( 403 | type='list', elements='dict', options=dict( 404 | context=dict(type='str'), 405 | app_id=dict(type='int') 406 | ) 407 | ) 408 | ) 409 | ), 410 | required_linear_history=dict(type='bool', default=False), 411 | required_pull_request_reviews=dict( 412 | type='dict', options=dict( 413 | dismissal_restrictions=dict( 414 | type='dict', options=dict( 415 | users=dict(type='list', elements='str', default=[]), 416 | teams=dict(type='list', elements='str', default=[]) 417 | ) 418 | ), 419 | dismiss_stale_reviews=dict(type='bool', default=True), 420 | require_code_owner_reviews=dict( 421 | type='bool', default=True), 422 | required_approving_review_count=dict(type='int', 423 | choices=[ 424 | 1, 2, 3, 425 | 4, 5]) 426 | ) 427 | ), 428 | restrictions=dict( 429 | type='dict', 430 | options=dict( 431 | users=dict(type='list', elements='str', default=[]), 432 | teams=dict(type='list', elements='str', default=[]), 433 | apps=dict(type='list', elements='str') 434 | ) 435 | ) 436 | ) 437 | ), 438 | teams=dict( 439 | type='list', elements='dict', options=dict( 440 | slug=dict(type='str', required=True), 441 | permission=dict( 442 | type='str', default='pull', 443 | choices=['pull', 'push', 'admin', 'maintain', 'triage'] 444 | ) 445 | ) 446 | ), 447 | collaborators=dict( 448 | type='list', elements='dict', options=dict( 449 | username=dict(type='str', required=True), 450 | permission=dict( 451 | type='str', default='pull', 452 | choices=['pull', 'push', 'admin', 'maintain', 'triage'] 453 | ) 454 | ) 455 | ), 456 | ) 457 | module_kwargs = dict( 458 | supports_check_mode=True 459 | ) 460 | 461 | def run(self): 462 | changed = False 463 | repo = dict() 464 | 465 | state = self.params.pop('state') 466 | target_attrs = self.params 467 | 468 | current_state = self.get_repo( 469 | target_attrs['owner'], 470 | target_attrs['name'], 471 | ignore_missing=True 472 | ) 473 | 474 | if current_state and state == 'absent': 475 | changed = True 476 | if not self.ansible.check_mode: 477 | self.delete_repo( 478 | target_attrs['owner'], 479 | target_attrs['name'] 480 | ) 481 | repo = {} 482 | else: 483 | changed, repo = self._manage_repository( 484 | state=state, 485 | current=current_state, 486 | check_mode=self.ansible.check_mode, 487 | **target_attrs 488 | ) 489 | if len(self.errors) == 0: 490 | self.exit_json( 491 | changed=changed, 492 | repository=repo 493 | ) 494 | else: 495 | self.fail_json( 496 | msg='Failures occured', 497 | errors=self.errors, 498 | repository=repo 499 | ) 500 | 501 | 502 | def main(): 503 | module = GHOrgRepositoryModule() 504 | module() 505 | 506 | 507 | if __name__ == "__main__": 508 | main() 509 | -------------------------------------------------------------------------------- /plugins/modules/github_org_team.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | 9 | DOCUMENTATION = ''' 10 | module: github_org_team 11 | short_description: Manage GitHub Organization Team 12 | extends_documentation_fragment: opentelekomcloud.gitcontrol.github 13 | version_added: "0.0.2" 14 | author: "Artem Goncharov (@gtema)" 15 | description: 16 | - Manages organization teams. 17 | options: 18 | organization: 19 | description: Name of the GitHub organization 20 | type: str 21 | required: True 22 | state: 23 | description: Team state 24 | type: str 25 | choices: [present, absent] 26 | default: present 27 | slug: 28 | description: Team slug. 29 | type: str 30 | required: True 31 | name: 32 | description: Team name. 33 | type: str 34 | required: False 35 | description: 36 | description: Team description 37 | type: str 38 | required: False 39 | privacy: 40 | description: | 41 | The level of privacy this team should have. The options are: 42 | For a non-nested team: 43 | * secret - only visible to organization owners and members of this team. 44 | * closed - visible to all members of this organization. 45 | Default: secret 46 | For a parent or child team: 47 | * closed - visible to all members of this organization. 48 | Default for child team: closed 49 | type: str 50 | choices: [secret, closed] 51 | default: secret 52 | required: False 53 | maintainers: 54 | description: List GitHub IDs for organization members who will become team maintainers. 55 | type: list 56 | elements: str 57 | required: False 58 | default: [] 59 | members: 60 | description: List GitHub IDs for organization members who will become team members. 61 | type: list 62 | elements: str 63 | required: False 64 | default: [] 65 | exclusive: 66 | description: Whether only listed members and maintainers should be present. 67 | type: bool 68 | default: False 69 | ''' 70 | 71 | 72 | RETURN = ''' 73 | opentelekomcloud.gitcontrol.github_org_team: 74 | description: List of organization teams statuses 75 | returned: always 76 | type: list 77 | elements: str 78 | ''' 79 | 80 | 81 | EXAMPLES = ''' 82 | - name: Apply org members 83 | opentelekomcloud.gitcontrol.github_org_team: 84 | token: "{{ secret }}" 85 | organization: "test_org" 86 | description: description of the team 87 | maintainers: 88 | - userA 89 | members: 90 | - userB 91 | ''' 92 | 93 | 94 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.github import GitHubBase 95 | 96 | 97 | class GHOrgTeamModule(GitHubBase): 98 | argument_spec = dict( 99 | organization=dict(type='str', required=True), 100 | state=dict(type='str', choices=['present', 'absent'], 101 | default='present'), 102 | slug=dict(type='str', required=True), 103 | name=dict(type='str', required=False), 104 | description=dict(type='str', required=False), 105 | privacy=dict( 106 | type='str', required=False, choices=['secret', 'closed'], 107 | default='secret'), 108 | maintainers=dict(type='list', elements='str', default=[]), 109 | members=dict(type='list', elements='str', default=[]), 110 | exclusive=dict(type='bool', default=False), 111 | ) 112 | module_kwargs = dict( 113 | supports_check_mode=True 114 | ) 115 | 116 | def run(self): 117 | changed = False 118 | owner = self.params['organization'] 119 | slug = self.params['slug'] 120 | status = None 121 | 122 | current_team = self.get_team(owner, slug, ignore_missing=True) 123 | if self.params['state'] == 'absent' and current_team: 124 | changed = True 125 | if not self.ansible.check_mode: 126 | self.delete_team(owner, slug) 127 | else: 128 | changed, status = self._manage_org_team( 129 | owner, 130 | slug, 131 | current_team, 132 | { 133 | 'name': self.params['name'], 134 | 'description': self.params['description'], 135 | 'privacy': self.params['privacy'], 136 | 'maintainers': self.params['maintainers'], 137 | 'members': self.params['members'] 138 | }, 139 | exclusive=self.params['exclusive'], 140 | check_mode=self.ansible.check_mode 141 | ) 142 | 143 | if len(self.errors) == 0: 144 | self.exit_json( 145 | changed=changed, 146 | team=status 147 | ) 148 | else: 149 | self.fail_json( 150 | msg='Failures occured', 151 | errors=self.errors, 152 | ) 153 | 154 | 155 | def main(): 156 | module = GHOrgTeamModule() 157 | module() 158 | 159 | 160 | if __name__ == "__main__": 161 | main() 162 | -------------------------------------------------------------------------------- /plugins/modules/github_org_teams.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | 9 | DOCUMENTATION = ''' 10 | module: github_org_teams 11 | short_description: Manage GitHub Organization Teams 12 | extends_documentation_fragment: opentelekomcloud.gitcontrol.github 13 | version_added: "0.0.2" 14 | author: "Artem Goncharov (@gtema)" 15 | description: 16 | - Manages organization teams. 17 | options: 18 | organization: 19 | description: Name of the GitHub organization 20 | type: str 21 | required: True 22 | teams: 23 | description: Dictionary of organization teams 24 | type: list 25 | required: True 26 | elements: dict 27 | suboptions: 28 | slug: 29 | description: Team slug 30 | type: str 31 | required: True 32 | name: 33 | description: Team name 34 | type: str 35 | required: False 36 | description: 37 | description: Team description 38 | type: str 39 | required: False 40 | privacy: 41 | description: Team privacy option 42 | type: str 43 | choices: [secret, closed] 44 | default: secret 45 | parent: 46 | description: Slug of the parent team 47 | type: str 48 | required: False 49 | maintainers: 50 | description: List of team maintainers 51 | type: list 52 | elements: str 53 | required: False 54 | aliases: [maintainer] 55 | members: 56 | description: List of team members 57 | type: list 58 | elements: str 59 | required: False 60 | aliases: [member] 61 | exclusive: 62 | description: | 63 | Whether exclusive mode should be enabled. This enforces that not 64 | configured, but existing teams as well as team maintainers and members 65 | will be deleted. 66 | type: bool 67 | default: False 68 | ''' 69 | 70 | 71 | RETURN = ''' 72 | opentelekomcloud.gitcontrol.github_org_teams: 73 | description: List of organization teams statuses 74 | returned: always 75 | type: list 76 | elements: str 77 | ''' 78 | 79 | 80 | EXAMPLES = ''' 81 | - name: Apply org members 82 | opentelekomcloud.gitcontrol.github_org_teams: 83 | token: "{{ secret }}" 84 | organization: "test_org" 85 | teams: 86 | team1: 87 | description: description of the team 88 | maintainer: 89 | - userA 90 | member: 91 | - userB 92 | ''' 93 | 94 | 95 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.github import GitHubBase 96 | 97 | 98 | class GHOrgTeamsModule(GitHubBase): 99 | argument_spec = dict( 100 | organization=dict(type='str', required=True), 101 | teams=dict( 102 | type='list', 103 | required=True, 104 | elements='dict', 105 | options=dict( 106 | slug=dict(type='str', required=True), 107 | name=dict(type='str', required=False), 108 | description=dict(type='str', required=False), 109 | privacy=dict(type='str', choices=['secret', 'closed'], 110 | default='secret'), 111 | parent=dict(type='str', required=False), 112 | maintainers=dict( 113 | type='list', 114 | elements='str', 115 | aliases=['maintainer'] 116 | ), 117 | members=dict( 118 | type='list', 119 | elements='str', 120 | aliases=['member'] 121 | ) 122 | ) 123 | ), 124 | exclusive=dict(type='bool', default=False), 125 | ) 126 | module_kwargs = dict( 127 | supports_check_mode=True 128 | ) 129 | 130 | def run(self): 131 | status = dict() 132 | changed = False 133 | 134 | (changed, status) = self._manage_org_teams( 135 | self.params['organization'], 136 | self.params['teams'], 137 | self.params['exclusive'], 138 | self.ansible.check_mode 139 | ) 140 | 141 | if len(self.errors) == 0: 142 | self.exit_json( 143 | changed=changed, 144 | teams=status 145 | ) 146 | else: 147 | self.fail_json( 148 | msg='Failures occured', 149 | errors=self.errors, 150 | teams=status 151 | ) 152 | 153 | 154 | def main(): 155 | module = GHOrgTeamsModule() 156 | module() 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /plugins/modules/members.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = ''' 9 | module: members 10 | short_description: Manage GitHub Organization Members 11 | extends_documentation_fragment: opentelekomcloud.gitcontrol.github 12 | version_added: "0.0.1" 13 | author: "Artem Goncharov (@gtema)" 14 | description: 15 | - Manages organization members inside of the organization repository 16 | options: 17 | root: 18 | description: Checkout directory 19 | type: str 20 | required: False 21 | token: 22 | description: GitHub token 23 | type: str 24 | required: True 25 | ''' 26 | 27 | 28 | RETURN = ''' 29 | ''' 30 | 31 | 32 | EXAMPLES = ''' 33 | ''' 34 | 35 | 36 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.github import ( 37 | GitHubBase 38 | ) 39 | 40 | 41 | class MembersModule(GitHubBase): 42 | argument_spec = dict( 43 | root=dict(type='str', required=False), 44 | ) 45 | module_kwargs = dict( 46 | supports_check_mode=True 47 | ) 48 | 49 | def run(self): 50 | status = dict() 51 | changed = False 52 | 53 | for owner, owner_dict in self.get_members().items(): 54 | (org_changed, status[owner]) = self._manage_org_members( 55 | owner, 56 | owner_dict['present'].get('users', []), 57 | False, 58 | self.ansible.check_mode 59 | ) 60 | if org_changed: 61 | changed = True 62 | 63 | if len(self.errors) == 0: 64 | self.exit_json( 65 | changed=changed, 66 | members=status 67 | ) 68 | else: 69 | self.fail_json( 70 | msg='Failures occured', 71 | errors=self.errors, 72 | members=status 73 | ) 74 | 75 | 76 | def main(): 77 | module = MembersModule() 78 | module() 79 | 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /plugins/modules/repositories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = ''' 9 | module: repositories 10 | short_description: Manage GitHub Repository 11 | extends_documentation_fragment: opentelekomcloud.gitcontrol.git 12 | version_added: "0.0.1" 13 | author: "Artem Goncharov (@gtema)" 14 | description: 15 | - Manages repository options 16 | options: 17 | root: 18 | description: Checkout directory 19 | type: str 20 | required: False 21 | token: 22 | description: GitHub token 23 | type: str 24 | required: True 25 | ''' 26 | 27 | RETURN = ''' 28 | ''' 29 | 30 | EXAMPLES = ''' 31 | ''' 32 | 33 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.github import GitHubBase 34 | 35 | 36 | class Repo(GitHubBase): 37 | argument_spec = dict( 38 | root=dict(type='str', required=False), 39 | ) 40 | module_kwargs = dict( 41 | supports_check_mode=True 42 | ) 43 | 44 | def _is_repo_update_needed(self, current, target): 45 | for attr in [ 46 | 'description', 'homepage', 'private', 'visibility', 47 | 'has_issues', 'has_projects', 'has_wiki', 'is_template', 48 | 'default_branch', 'allow_squash_merge', 49 | 'allow_merge_commit', 'allow_rebase_merge', 50 | 'delete_branch_on_merge', 'archived' 51 | ]: 52 | if attr in target and target[attr] != current.get(attr): 53 | return True 54 | 55 | def _is_branch_protection_update_needed(self, owner, repo, branch, target): 56 | current = self.get_branch_protection(owner, repo, branch) 57 | 58 | if not current: 59 | return True 60 | else: 61 | for attr in ['enforce_admins', 'required_linear_history', 62 | 'allow_force_pushes', 'allow_deletions', 63 | 'required_conversation_resolution']: 64 | if ( 65 | attr in target 66 | and ( 67 | attr not in current 68 | or target[attr] != current[attr]['enabled'] 69 | ) 70 | ): 71 | return True 72 | 73 | current_restrictions = current.get('restrictions', {}) 74 | target_restrictions = target.get('restrictions', {}) 75 | current_pr_review = current.get('required_pull_request_reviews', {}) 76 | target_pr_review = target.get('required_pull_request_reviews', {}) 77 | current_status_checks = current.get('required_status_checks', {}) 78 | target_status_checks = target.get('required_status_checks', {}) 79 | if target_status_checks: 80 | if not current_status_checks: 81 | return True 82 | if (current_status_checks.get( 83 | 'strict', False) != target_status_checks.get( 84 | 'strict', False)): 85 | return True 86 | 87 | if ( 88 | set( 89 | current_status_checks.get('contexts', []) 90 | ) != set( 91 | target_status_checks.get('contexts', []) 92 | ) 93 | ): 94 | return True 95 | 96 | if target_restrictions: 97 | if not current_restrictions: 98 | return True 99 | if ( 100 | set( 101 | [x['login'] for x in current_restrictions['users']] 102 | ) != set(target_restrictions['users']) 103 | ): 104 | return True 105 | if ( 106 | set( 107 | [x['slug'] for x in current_restrictions['teams']] 108 | ) != set(target_restrictions['teams']) 109 | ): 110 | return True 111 | if ( 112 | set( 113 | [x['slug'] for x in current_restrictions['apps']] 114 | ) != set(target_restrictions['apps']) 115 | ): 116 | return True 117 | if target_pr_review: 118 | for attr in ['dismiss_stale_reviews', 119 | 'require_code_owner_reviews', 120 | 'required_approving_review_count']: 121 | if ( 122 | attr in target_pr_review 123 | and target_pr_review[attr] != current_pr_review.get( 124 | attr, False) 125 | ): 126 | return True 127 | 128 | if 'dismissal_restrictions' in target_pr_review: 129 | t = target_pr_review['dismissal_restrictions'] 130 | c = current_pr_review['dismissal_restrictions'] 131 | if ( 132 | set( 133 | [x['login'] for x in c.get('users', [])] 134 | ) != set(t['users']) 135 | ): 136 | return True 137 | if ( 138 | set( 139 | [x['slug'] for x in c.get('teams', [])] 140 | ) != set(t['teams']) 141 | ): 142 | return True 143 | 144 | return False 145 | 146 | def _get_privs(self, mapping): 147 | """Convert teams/collaborators mapping into entity/priv mapping""" 148 | privs = dict() 149 | for k, v in mapping.items(): 150 | for priv in ['maintain', 'pull', 'push', 'admin', 'triage']: 151 | if isinstance(v, list): 152 | for team in v: 153 | if team not in privs: 154 | privs[team] = dict( 155 | admin=False, pull=False, 156 | push=False, maintain=False, triage=False) 157 | if ( 158 | priv in mapping 159 | and isinstance(mapping[priv], list) 160 | and team in mapping[priv] 161 | ): 162 | privs[team][priv] = True 163 | return privs 164 | 165 | def _pick_priv_from_dict(self, privs_dict): 166 | """Knowing hash of individual privileges return the one (first match) 167 | which is true. 168 | 169 | dict(admin=False, pull=False, push=True, maintain=False) will return 170 | "push" 171 | """ 172 | if privs_dict.get("push") and privs_dict.get("pull"): 173 | return "push" 174 | for k, v in privs_dict.items(): 175 | # permission setting is not getting hash, but 176 | # single value 177 | if v: 178 | return k 179 | 180 | def run(self): 181 | config = self.get_config() 182 | changed = False 183 | status = dict() 184 | 185 | for owner, val in config.items(): 186 | status[owner] = dict() 187 | for repo, repo_dict in val['repositories'].items(): 188 | status[owner][repo] = dict() 189 | current_repo = self.get_repo(owner, repo, ignore_missing=True) 190 | 191 | if not current_repo: 192 | if not self.ansible.check_mode: 193 | repo_args = dict( 194 | description=repo_dict.get('description'), 195 | homepage=repo_dict.get('homepage'), 196 | private=repo_dict.get('private', False), 197 | visibility=repo_dict.get('visibility', 'public'), 198 | has_issues=repo_dict.get('has_issues', True), 199 | has_projects=repo_dict.get('has_projects', True), 200 | has_wiki=repo_dict.get('has_wiki', True), 201 | # is_template=repo_dict.get('is_template', False), 202 | auto_init=repo_dict.get('auto_init', False), 203 | allow_squash_merge=repo_dict.get( 204 | 'allow_squash_merge', True), 205 | allow_merge_commit=repo_dict.get( 206 | 'allow_merge_commit', True), 207 | allow_rebase_merge=repo_dict.get( 208 | 'allow_rebase_merge', True), 209 | allow_auto_merge=repo_dict.get( 210 | 'allow_auto_merge', False), 211 | delete_branch_on_merge=repo_dict.get( 212 | 'delete_branch_on_merge', False) 213 | ) 214 | for k in ['gitignore_template', 'license_template']: 215 | if k in repo_dict: 216 | repo_args[k] = repo_dict[k] 217 | current_repo = self.create_repo( 218 | owner, repo, **repo_args) 219 | 220 | if current_repo and current_repo.get('archived', False): 221 | # Not doing anything on archived repos 222 | continue 223 | 224 | if current_repo and self._is_repo_update_needed(current_repo, repo_dict): 225 | changed = True 226 | if not self.ansible.check_mode: 227 | self.update_repo(owner, repo, **repo_dict) 228 | # Current state is too huge to return it 229 | status[owner][repo]['description'] = repo_dict 230 | 231 | if current_repo and 'topics' in repo_dict: 232 | current_topics = self.get_repo_topics(owner, repo) 233 | if set(repo_dict['topics']) != set(current_topics): 234 | changed = True 235 | if not self.ansible.check_mode: 236 | self.update_repo_topics( 237 | owner, repo, repo_dict['topics']) 238 | status[owner][repo]['topics'] = repo_dict['topics'] 239 | 240 | # TODO(gtema): collaborator management need to be done, 241 | # but we have not proper data structure (team, collaborator, 242 | # outside collaborator) 243 | if current_repo and 'teams' in repo_dict: 244 | status[owner][repo]['teams'] = dict() 245 | privs = self._get_privs(repo_dict['teams']) 246 | 247 | for team in self.get_repo_teams(owner, repo): 248 | # TODO: need to differentiate between org teams and 249 | # project teams 250 | # pop privs for the team to track which team is new 251 | target_privs = privs.pop(team['slug'], {}) 252 | if not target_privs: 253 | # Delete project access from team 254 | changed = True 255 | if not self.ansible.check_mode: 256 | self.delete_team_repo_access( 257 | owner, team['slug'], repo) 258 | target_priv = self._pick_priv_from_dict(target_privs) 259 | if ( 260 | target_priv 261 | and self._pick_priv_from_dict( 262 | team['permissions']) != target_priv 263 | ): 264 | changed = True 265 | if not self.ansible.check_mode: 266 | self.update_team_repo_permissions( 267 | owner, team=team['slug'], repo=repo, 268 | priv=target_priv) 269 | 270 | status[owner][repo]['teams'][team['slug']] = \ 271 | target_priv 272 | # privs dict now contains remaining privileges 273 | for team, target_privs in privs.items(): 274 | target_priv = self._pick_priv_from_dict(target_privs) 275 | 276 | changed = True 277 | if not self.ansible.check_mode: 278 | self.update_team_repo_permissions( 279 | owner, team=team, repo=repo, 280 | priv=target_priv) 281 | status[owner][repo]['teams'][team] = \ 282 | target_priv 283 | 284 | if current_repo and 'protection_rules' in repo_dict: 285 | tmpl = self.get_branch_protections( 286 | repo_dict['protection_rules']) 287 | 288 | if ( 289 | self._is_branch_protection_update_needed( 290 | owner, repo, repo_dict['default_branch'], 291 | tmpl) 292 | ): 293 | changed = True 294 | if not self.ansible.check_mode: 295 | self.update_branch_protection( 296 | owner, repo, repo_dict['default_branch'], 297 | tmpl) 298 | 299 | status[owner][repo]['branch_protection'] = tmpl 300 | 301 | if len(self.errors) == 0: 302 | self.exit_json( 303 | changed=changed, 304 | repositories=status, 305 | errors=self.errors 306 | ) 307 | else: 308 | self.fail_json( 309 | msg='Failures occured', 310 | errors=self.errors, 311 | repositories=status 312 | ) 313 | 314 | 315 | def main(): 316 | module = Repo() 317 | module() 318 | 319 | 320 | if __name__ == "__main__": 321 | main() 322 | -------------------------------------------------------------------------------- /plugins/modules/teams.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 | 4 | from __future__ import (absolute_import, division, print_function) 5 | __metaclass__ = type 6 | 7 | DOCUMENTATION = ''' 8 | module: teams 9 | short_description: Manage GitHub Organization Teams 10 | extends_documentation_fragment: opentelekomcloud.gitcontrol.git 11 | version_added: "0.0.1" 12 | author: "Artem Goncharov (@gtema)" 13 | description: 14 | - Manages team members inside of the organization repository 15 | options: 16 | root: 17 | description: Checkout directory 18 | type: str 19 | required: False 20 | token: 21 | description: GitHub token 22 | type: str 23 | required: True 24 | ''' 25 | 26 | RETURN = ''' 27 | ''' 28 | 29 | EXAMPLES = ''' 30 | ''' 31 | 32 | 33 | from ansible_collections.opentelekomcloud.gitcontrol.plugins.module_utils.github import GitHubBase 34 | 35 | 36 | class TeamsModule(GitHubBase): 37 | argument_spec = dict( 38 | root=dict(type='str', required=False), 39 | ) 40 | module_kwargs = dict( 41 | supports_check_mode=True 42 | ) 43 | 44 | def run(self): 45 | status = dict() 46 | changed = False 47 | 48 | for owner, owner_dict in self.get_teams().items(): 49 | teams = [] 50 | for slug, team in owner_dict['present']['teams'].items(): 51 | team['slug'] = slug 52 | team['name'] = slug 53 | teams.append(team) 54 | 55 | (is_changed, status[owner]) = self._manage_org_teams( 56 | owner, 57 | teams, 58 | self.ansible.check_mode) 59 | 60 | if len(self.errors) == 0: 61 | self.exit_json( 62 | changed=changed, 63 | teams=status, 64 | errors=self.errors 65 | ) 66 | else: 67 | self.fail_json( 68 | msg='Failures occured', 69 | errors=self.errors, 70 | teams=status 71 | ) 72 | 73 | 74 | def main(): 75 | module = TeamsModule() 76 | module() 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.28 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = ansible-collections-gitcontrol 3 | summary = Ansible collections for managing GitHub organizations 4 | description-file = 5 | README.md 6 | 7 | author = opentelekomcloud 8 | home-page = https://open.telekom.cloud 9 | classifier = 10 | License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) 11 | Development Status :: 5 - Production/Stable 12 | Intended Audience :: Developers 13 | Intended Audience :: System Administrators 14 | Intended Audience :: Information Technology 15 | Topic :: System :: Systems Administration 16 | Topic :: Utilities 17 | 18 | [global] 19 | setup-hooks = 20 | pbr.hooks.setup_hook 21 | 22 | [files] 23 | data_files = 24 | share/ansible/collections/ansible_collections/opentelekomcloud/gitcontrol/ = README.md 25 | share/ansible/collections/ansible_collections/opentelekomcloud/gitcontrol/plugins = plugins/* 26 | 27 | [wheel] 28 | universal = 1 29 | 30 | [pbr] 31 | skip_authors = True 32 | skip_changelog = True 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | import setuptools 14 | 15 | setuptools.setup( 16 | setup_requires=['pbr'], 17 | pbr=True) 18 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | ansible-lint>=6.0.0 2 | pycodestyle==2.8.0 3 | flake8>=4.0 4 | pylint 5 | voluptuous 6 | yamllint 7 | rstcheck 8 | ruamel.yaml 9 | requests 10 | -------------------------------------------------------------------------------- /test_org/orgs/opentelekomcloud-gitcontrol-test/people/dismissed_members.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dismissed_users: 3 | -------------------------------------------------------------------------------- /test_org/orgs/opentelekomcloud-gitcontrol-test/people/members.yml: -------------------------------------------------------------------------------- 1 | --- 2 | users: 3 | - name: "ag" 4 | login: "g2tema" 5 | visibility: "Public" 6 | role: Member 7 | -------------------------------------------------------------------------------- /test_org/orgs/opentelekomcloud-gitcontrol-test/repositories/test.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | default_branch: main 3 | description: Test description 4 | homepage: null 5 | language: CSS 6 | archived: false 7 | has_issues: true 8 | has_projects: false 9 | has_wiki: false 10 | private: false 11 | delete_branch_on_merge: true 12 | allow_merge_commit: false 13 | allow_squash_merge: true 14 | allow_rebase_merge: false 15 | allow_update_branch: true 16 | teams: 17 | maintain: 18 | pull: 19 | push: 20 | - test_b 21 | admin: 22 | collaborators: 23 | maintain: 24 | pull: 25 | push: 26 | admin: 27 | -------------------------------------------------------------------------------- /test_org/orgs/opentelekomcloud-gitcontrol-test/teams/dismissed_members.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dissmissed_in_teams: 3 | -------------------------------------------------------------------------------- /test_org/orgs/opentelekomcloud-gitcontrol-test/teams/members.yml: -------------------------------------------------------------------------------- 1 | --- 2 | teams: 3 | storage: 4 | description: Test team 5 | visibility: closed 6 | privacy: closed 7 | parent: 8 | maintainer: 9 | - gtema 10 | member: 11 | - g2tema 12 | -------------------------------------------------------------------------------- /test_org/templates/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | required_status_checks: 3 | strict: false 4 | contexts: [] 5 | enforce_admins: true 6 | required_pull_request_reviews: 7 | dismissal_restrictions: 8 | users: [] 9 | teams: [] 10 | dismiss_stale_reviews: true 11 | require_code_owner_reviews: false 12 | required_approving_review_count: 1 13 | who_can_push: 14 | users: [] 15 | teams: [] 16 | apps: [] 17 | required_linear_history: false 18 | allow_force_pushes: false 19 | allow_deletions: false 20 | -------------------------------------------------------------------------------- /tests/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentelekomcloud/ansible-collection-gitcontrol/818090c9d93ec6ee3b6cd988d2a6bb826a8873c4/tests/.keep -------------------------------------------------------------------------------- /tests/integration/requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.28 2 | -------------------------------------------------------------------------------- /tests/integration/targets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentelekomcloud/ansible-collection-gitcontrol/818090c9d93ec6ee3b6cd988d2a6bb826a8873c4/tests/integration/targets/.keep -------------------------------------------------------------------------------- /tests/integration/targets/gitea_org_repository/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test gitea org repo 3 | module_defaults: 4 | opentelekomcloud.gitcontrol.gitea_org_repository: 5 | token: "{{ gitea_token }}" 6 | api_url: "{{ gitea_api_url }}" 7 | owner: "{{ gitea_test_org }}" 8 | 9 | block: 10 | 11 | # - name: Check mode 12 | # check_mode: true 13 | # opentelekomcloud.gitcontrol.gitea_org_repository: 14 | # name: test 15 | # description: "Test description" 16 | # 17 | # - name: Apply 18 | # check_mode: false 19 | # opentelekomcloud.gitcontrol.gitea_org_repository: 20 | # name: test 21 | # description: "Test description" 22 | 23 | - name: New repository 24 | check_mode: false 25 | opentelekomcloud.gitcontrol.gitea_org_repository: 26 | name: test_gitcontrol 27 | description: "Test description" 28 | auto_init: true 29 | allow_manual_merge: false 30 | allow_merge_commits: false 31 | allow_rebase: true 32 | allow_rebase_explicit: false 33 | allow_rebase_update: true 34 | allow_squash_merge: true 35 | archived: false 36 | autodetect_manual_merge: false 37 | default_branch: "main" 38 | default_delete_branch_after_merge: true 39 | default_merge_style: "squash" 40 | enable_prune: true 41 | has_issues: false 42 | has_projects: false 43 | has_wiki: false 44 | has_pull_requests: true 45 | private: false 46 | trust_model: "default" 47 | website: "nottest.com" 48 | branch_protections: 49 | - branch_name: main 50 | block_on_official_review_requests: true 51 | block_on_outdated_branch: true 52 | block_on_rejected_reviews: true 53 | dismiss_stale_approvals: true 54 | enable_push: false 55 | enable_status_check: true 56 | status_check_contexts: ['a'] 57 | required_approvals: 2 58 | teams: 59 | - test_team 60 | register: repo 61 | 62 | - name: Update repository 63 | check_mode: false 64 | opentelekomcloud.gitcontrol.gitea_org_repository: 65 | name: test_gitcontrol 66 | description: "Test description" 67 | auto_init: true 68 | allow_manual_merge: false 69 | allow_merge_commits: false 70 | allow_rebase: true 71 | allow_rebase_explicit: false 72 | allow_rebase_update: true 73 | allow_squash_merge: true 74 | archived: false 75 | autodetect_manual_merge: false 76 | default_branch: "main" 77 | default_delete_branch_after_merge: true 78 | default_merge_style: "squash" 79 | enable_prune: true 80 | has_issues: true 81 | has_projects: true 82 | has_wiki: true 83 | has_pull_requests: true 84 | private: false 85 | trust_model: "default" 86 | website: "no2ttest.com" 87 | branch_protections: 88 | - branch_name: main 89 | block_on_official_review_requests: false 90 | block_on_outdated_branch: false 91 | block_on_rejected_reviews: true 92 | dismiss_stale_approvals: true 93 | enable_push: false 94 | enable_status_check: true 95 | status_check_contexts: ['b'] 96 | required_approvals: 1 97 | teams: 98 | - test_team2 99 | register: repo 100 | 101 | # - name: Delete repository 102 | # check_mode: false 103 | # opentelekomcloud.gitcontrol.gitea_org_repository: 104 | # name: test_gitcontrol 105 | # state: absent 106 | -------------------------------------------------------------------------------- /tests/integration/targets/github_org_members/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test Github Org members 3 | module_defaults: 4 | opentelekomcloud.gitcontrol.github_org_members: 5 | token: "{{ token }}" 6 | organization: "{{ test_org }}" 7 | members: 8 | - login: gtema 9 | role: "admin" 10 | - login: g2tema 11 | role: "member" 12 | 13 | block: 14 | - name: Apply users - check mode 15 | opentelekomcloud.gitcontrol.github_org_members: 16 | check_mode: true 17 | 18 | - name: Apply users - exclusive check mode 19 | opentelekomcloud.gitcontrol.github_org_members: 20 | exclusive: true 21 | register: members 22 | check_mode: true 23 | 24 | - name: Apply users 25 | opentelekomcloud.gitcontrol.github_org_members: 26 | -------------------------------------------------------------------------------- /tests/integration/targets/github_org_repository/defaults/main.yaml: -------------------------------------------------------------------------------- 1 | test_branch_protection: &bp 2 | required_status_checks: 3 | strict: true 4 | checks: 5 | - context: context_app 6 | app_id: 152782 7 | enforce_admins: true 8 | required_pull_request_reviews: 9 | dismissal_restrictions: 10 | users: [] 11 | teams: [] 12 | dismiss_stale_reviews: true 13 | require_code_owner_reviews: true 14 | required_approving_review_count: 2 15 | restrictions: 16 | users: [] 17 | teams: [] 18 | apps: ['otc-test-app'] 19 | 20 | test_branch_protections: 21 | - branch: main 22 | <<: *bp 23 | -------------------------------------------------------------------------------- /tests/integration/targets/github_org_repository/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test Github org repo 3 | module_defaults: 4 | opentelekomcloud.gitcontrol.github_org_repository: 5 | token: "{{ token }}" 6 | owner: "{{ test_org }}" 7 | 8 | block: 9 | - name: Check mode 10 | check_mode: true 11 | opentelekomcloud.gitcontrol.github_org_repository: 12 | name: test2 13 | description: "Test description" 14 | homepage: "https://test.com" 15 | archived: false 16 | has_issues: false 17 | has_projects: false 18 | has_wiki: false 19 | visibility: public 20 | is_template: false 21 | auto_init: false 22 | gitignore_template: false 23 | license_template: false 24 | allow_squash_merge: true 25 | allow_merge_commit: true 26 | allow_rebase_merge: true 27 | allow_auto_merge: true 28 | delete_branch_on_merge: true 29 | default_branch: main 30 | topics: ['a', 'b'] 31 | teams: 32 | - slug: team_a 33 | permission: admin 34 | collaborators: 35 | - username: fake_user 36 | permission: pull 37 | branch_protections: "{{ test_branch_protections }}" 38 | 39 | - name: Create repository with auto init and templates 40 | opentelekomcloud.gitcontrol.github_org_repository: 41 | name: test2 42 | description: "Test description" 43 | homepage: "https://test.com" 44 | archived: false 45 | has_issues: false 46 | has_projects: false 47 | has_wiki: false 48 | visibility: public 49 | is_template: false 50 | auto_init: true 51 | gitignore_template: "Python" 52 | license_template: "mit" 53 | allow_squash_merge: true 54 | allow_merge_commit: true 55 | allow_rebase_merge: true 56 | allow_auto_merge: true 57 | delete_branch_on_merge: true 58 | default_branch: main 59 | topics: ['a', 'b'] 60 | teams: 61 | - slug: team_c 62 | permission: push 63 | collaborators: 64 | - username: g2tema 65 | permission: pull 66 | branch_protections: "{{ test_branch_protections }}" 67 | 68 | - name: Apply - another iteration 69 | opentelekomcloud.gitcontrol.github_org_repository: 70 | name: test2 71 | description: "Test description" 72 | homepage: "https://test.com" 73 | archived: false 74 | has_issues: false 75 | has_projects: false 76 | has_wiki: false 77 | visibility: public 78 | is_template: false 79 | auto_init: true 80 | gitignore_template: "Python" 81 | license_template: "mit" 82 | allow_squash_merge: true 83 | allow_merge_commit: true 84 | allow_rebase_merge: true 85 | allow_auto_merge: true 86 | delete_branch_on_merge: true 87 | default_branch: main 88 | topics: ['a', 'b'] 89 | teams: 90 | - slug: team_c 91 | permission: push 92 | collaborators: [] 93 | branch_protections: "{{ test_branch_protections }}" 94 | 95 | - name: Drop repository 96 | opentelekomcloud.gitcontrol.github_org_repository: 97 | name: test2 98 | state: absent 99 | 100 | - name: Create repository with auto init and no templates 101 | opentelekomcloud.gitcontrol.github_org_repository: 102 | name: test2 103 | description: "Test description" 104 | homepage: "https://test.com" 105 | archived: false 106 | has_issues: false 107 | has_projects: false 108 | has_wiki: false 109 | visibility: public 110 | is_template: false 111 | auto_init: true 112 | allow_squash_merge: true 113 | allow_merge_commit: true 114 | allow_rebase_merge: true 115 | allow_auto_merge: true 116 | delete_branch_on_merge: true 117 | default_branch: main 118 | topics: ['a', 'b'] 119 | teams: 120 | - slug: team_c 121 | permission: push 122 | collaborators: 123 | - username: g2tema 124 | permission: pull 125 | branch_protections: "{{ test_branch_protections }}" 126 | 127 | - name: Update repository branch protections with "null" restrictions 128 | opentelekomcloud.gitcontrol.github_org_repository: 129 | name: test2 130 | description: "Test description" 131 | homepage: "https://test.com" 132 | archived: false 133 | has_issues: false 134 | has_projects: false 135 | has_wiki: false 136 | visibility: public 137 | is_template: false 138 | auto_init: true 139 | allow_squash_merge: true 140 | allow_merge_commit: true 141 | allow_rebase_merge: true 142 | allow_auto_merge: true 143 | delete_branch_on_merge: true 144 | default_branch: main 145 | topics: ['a', 'b'] 146 | teams: 147 | - slug: team_c 148 | permission: push 149 | collaborators: 150 | - username: g2tema 151 | permission: pull 152 | branch_protections: 153 | - branch: main 154 | restrictions: 155 | required_status_checks: 156 | strict: true 157 | checks: 158 | - context: context_app 159 | app_id: 152782 160 | enforce_admins: true 161 | required_pull_request_reviews: 162 | dismissal_restrictions: 163 | users: [] 164 | teams: [] 165 | dismiss_stale_reviews: true 166 | require_code_owner_reviews: true 167 | required_approving_review_count: 2 168 | 169 | - name: Drop repository 170 | opentelekomcloud.gitcontrol.github_org_repository: 171 | name: test2 172 | state: absent 173 | -------------------------------------------------------------------------------- /tests/integration/targets/github_org_team/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test GitHub org team module 3 | module_defaults: 4 | opentelekomcloud.gitcontrol.github_org_team: 5 | token: "{{ token }}" 6 | organization: "{{ test_org }}" 7 | slug: team_d 8 | description: Test team 9 | 10 | block: 11 | - name: Apply team - check mode 12 | opentelekomcloud.gitcontrol.github_org_team: 13 | check_mode: true 14 | 15 | - name: Apply teams 16 | opentelekomcloud.gitcontrol.github_org_team: 17 | 18 | - name: Apply teams - repeat 19 | opentelekomcloud.gitcontrol.github_org_team: 20 | register: team 21 | 22 | - name: Checks 23 | ansible.builtin.assert: 24 | that: "team is not changed" 25 | 26 | - name: Change team props 27 | opentelekomcloud.gitcontrol.github_org_team: 28 | description: "new description" 29 | privacy: "closed" 30 | members: 31 | - gtema 32 | 33 | - name: Change team maintainers 34 | opentelekomcloud.gitcontrol.github_org_team: 35 | exclusive: true 36 | maintainers: 37 | - gtema 38 | members: 39 | - gtema 40 | 41 | - name: Delete team 42 | opentelekomcloud.gitcontrol.github_org_team: 43 | state: absent 44 | -------------------------------------------------------------------------------- /tests/integration/targets/github_org_teams/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test GitHub org teams 3 | module_defaults: 4 | opentelekomcloud.gitcontrol.github_org_teams: 5 | token: "{{ token }}" 6 | organization: "{{ test_org }}" 7 | teams: 8 | - slug: team_a 9 | description: Test team 10 | privacy: closed 11 | parent: 12 | maintainers: 13 | - gtema 14 | members: 15 | - g2tema 16 | 17 | block: 18 | - name: Apply teams - check mode 19 | opentelekomcloud.gitcontrol.github_org_teams: 20 | check_mode: true 21 | 22 | - name: Apply teams 23 | opentelekomcloud.gitcontrol.github_org_teams: 24 | 25 | - name: Apply teams - empty fields 26 | opentelekomcloud.gitcontrol.github_org_teams: 27 | teams: 28 | - slug: team_c 29 | description: Test team 30 | privacy: closed 31 | 32 | - name: Apply teams - add team maintainers 33 | opentelekomcloud.gitcontrol.github_org_teams: 34 | teams: 35 | - slug: team_c 36 | description: Test team 37 | privacy: closed 38 | maintainers: 39 | - gtema 40 | register: teams 41 | 42 | - name: Checks 43 | ansible.builtin.assert: 44 | that: 45 | - "teams.teams.team_c.maintainers.gtema is defined" 46 | 47 | - name: Exclusive mode 48 | opentelekomcloud.gitcontrol.github_org_teams: 49 | exclusive: true 50 | teams: 51 | - slug: team_c 52 | description: Test team 53 | privacy: closed 54 | register: teams 55 | 56 | - name: Checks 57 | ansible.builtin.assert: 58 | that: 59 | - "teams.teams.team_a.status == 'deleted'" 60 | - "teams.teams.team_c.maintainers.gtema == 'removed'" 61 | -------------------------------------------------------------------------------- /tests/integration/targets/members/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Apply users - check mode 3 | opentelekomcloud.gitcontrol.members: 4 | root: "{{ root }}" 5 | token: "{{ token }}" 6 | check_mode: true 7 | 8 | - name: Apply users 9 | opentelekomcloud.gitcontrol.members: 10 | root: "{{ root }}" 11 | token: "{{ token }}" 12 | -------------------------------------------------------------------------------- /tests/integration/targets/repositories/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Apply Teams as dependency 3 | opentelekomcloud.gitcontrol.github_org_teams: 4 | token: "{{ token }}" 5 | organization: "{{ test_org }}" 6 | teams: 7 | - slug: test_b 8 | description: Test team 9 | privacy: closed 10 | 11 | - name: Apply Repositories - check mode 12 | opentelekomcloud.gitcontrol.repositories: 13 | root: "{{ root }}" 14 | token: "{{ token }}" 15 | check_mode: true 16 | 17 | - name: Apply Repositories 18 | opentelekomcloud.gitcontrol.repositories: 19 | root: "{{ root }}" 20 | token: "{{ token }}" 21 | register: repos 22 | 23 | - name: Apply Repositories - Idempotency check 24 | opentelekomcloud.gitcontrol.repositories: 25 | root: "{{ root }}" 26 | token: "{{ token }}" 27 | register: repos 28 | -------------------------------------------------------------------------------- /tests/integration/targets/teams/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | - name: Apply Teams - check mode 2 | opentelekomcloud.gitcontrol.teams: 3 | root: "{{ root }}" 4 | token: "{{ token }}" 5 | check_mode: true 6 | 7 | - name: Apply Teams 8 | opentelekomcloud.gitcontrol.teams: 9 | root: "{{ root }}" 10 | token: "{{ token }}" 11 | 12 | - name: Drop team 13 | opentelekomcloud.gitcontrol.github_org_team: 14 | token: "{{ token }}" 15 | organization: "{{ test_org }}" 16 | slug: "storage" 17 | state: "absent" 18 | -------------------------------------------------------------------------------- /tests/playbooks/integration_config.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | # Here come the variables definitions for the integration tests 3 | token: "{{ gitcontrol.github_token }}" 4 | root: "{{ ansible_user_dir }}/{{ zuul.project.src_dir }}/test_org" 5 | test_org: "opentelekomcloud-gitcontrol-test" 6 | gitea_test_org: "test_org" 7 | gitea_token: "{{ gitcontrol.gitea_token }}" 8 | gitea_api_url: "{{ gitcontrol.gitea_api_url }}" 9 | -------------------------------------------------------------------------------- /tests/playbooks/pre.yaml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | tasks: 3 | - name: Write integration_config.yml file 4 | template: 5 | src: "integration_config.yml.j2" 6 | dest: "~/.ansible/collections/ansible_collections/{{ ansible_collection_namespace }}/{{ ansible_collection_name}}/tests/integration/integration_config.yml" 7 | no_log: true 8 | -------------------------------------------------------------------------------- /tests/sanity/requirements.txt: -------------------------------------------------------------------------------- 1 | packaging # needed for update-bundled and changelog 2 | sphinx ; python_version >= '3.5' # docs build requires python 3+ 3 | sphinx-notfound-page ; python_version >= '3.5' # docs build requires python 3+ 4 | straight.plugin ; python_version >= '3.5' # needed for hacking/build-ansible.py which will host changelog generation and requires python 3+ 5 | requests 6 | -------------------------------------------------------------------------------- /tools/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2019 Red Hat, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import pbr.version 17 | 18 | from ruamel.yaml import YAML 19 | 20 | import os 21 | import shutil 22 | 23 | 24 | def generate_version_info(): 25 | version_info = pbr.version.VersionInfo('dummy') 26 | semantic_version = version_info.semantic_version() 27 | release_string = semantic_version._long_version('-') 28 | 29 | yaml = YAML() 30 | yaml.explicit_start = True 31 | yaml.indent(sequence=4, offset=2) 32 | 33 | config = yaml.load(open('galaxy.yml.in')) 34 | config['version'] = release_string 35 | 36 | with open('galaxy.yml', 'w') as fp: 37 | yaml.dump(config, fp) 38 | 39 | 40 | def main(): 41 | generate_version_info() 42 | shutil.rmtree('build_artifact', ignore_errors=True) 43 | if os.path.exists('MANIFEST.json'): 44 | os.unlink('MANIFEST.json') 45 | 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.6 3 | envlist = pep8 4 | skipsdist = True 5 | ignore_basepython_conflict = True 6 | 7 | [testenv] 8 | skip_install = True 9 | install_command = python -m pip install {opts} {packages} 10 | basepython = python3 11 | passenv = 12 | setenv = 13 | LANG=C.UTF-8 14 | LC_ALL=C.UTF-8 15 | deps = 16 | -r{toxinidir}/test-requirements.txt 17 | commands = stestr run {posargs} 18 | stestr slowest 19 | 20 | [testenv:pep8] 21 | commands = 22 | flake8 23 | 24 | [testenv:build] 25 | deps = 26 | pbr 27 | ruamel.yaml 28 | ansible-base 29 | 30 | commands = 31 | ansible --version 32 | ansible-galaxy collection build --force {toxinidir} --output-path {toxinidir}/build_artifact 33 | 34 | [testenv:linters] 35 | passenv = * 36 | commands = 37 | ansible-lint -vvv 38 | 39 | [testenv:venv] 40 | deps = 41 | -r{toxinidir}/test-requirements.txt 42 | commands = {posargs} 43 | 44 | [flake8] 45 | # W503 Is supposed to be off by default but in the latest pycodestyle isn't. 46 | # Also, both openstacksdk and Donald Knuth disagree with the rule. Line 47 | # breaks should occur before the binary operator for readability. 48 | # H4 are rules for docstrings. Maybe we should clean them? 49 | # E501,E402,H301 are ignored so we can import the existing 50 | # modules unchanged and then clean them in subsequent patches. 51 | ignore = W503,H4,E501,E402,H301 52 | show-source = True 53 | exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,ansible_collections 54 | 55 | [testenv:ansible] 56 | # Need to pass some env vars for the Ansible playbooks 57 | passenv = HOME USER ANSIBLE_* 58 | deps = 59 | {[testenv]deps} 60 | commands = 61 | /bin/bash {toxinidir}/ci/run-ansible-tests-collection.sh -e {envdir} {posargs} 62 | --------------------------------------------------------------------------------