From b071e3a2548ca9d8cd0081b397e8de78c64d6b2e Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:22:35 -0700 Subject: [PATCH 01/19] new github_gpg_key to manage GPG keys on GitHub --- plugins/modules/github_gpg_key.py | 385 ++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 plugins/modules/github_gpg_key.py diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py new file mode 100644 index 0000000000..e03b045134 --- /dev/null +++ b/plugins/modules/github_gpg_key.py @@ -0,0 +1,385 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2024, Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> +# Based on community.general.github_key module by Ansible Project + +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +module: github_gpg_key +short_description: Manage GitHub access keys +description: + - Creates, removes, or updates GitHub GPG keys. +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + token: + description: + - GitHub OAuth or personal access token (classic) with the read:gpg_key, write:gpg_key, and admin:gpg_key scopes needed to manage GPG keys. + required: true + type: str + name: + description: + - GPG key name + required: true + type: str + armored_public_key: + description: + - ASCII-armored GPG public key value. Required when O(state=present). + type: str + state: + description: + - Whether to remove a key, ensure that it exists, or update its value. + choices: ['present', 'absent'] + default: 'present' + type: str + force: + description: + - The default is V(true), which will replace the existing remote key + if it is different than O(pubkey). If V(false), the key will only be + set if no key with the given O(name) exists. + type: bool + default: true + +author: Austin Lucas Lake (@austinlucaslake) +''' + +RETURN = ''' +deleted_keys: + description: An array of key objects that were deleted. Only present on state=absent + type: list + returned: When state=absent + sample: [{ + "id": 3, + "name": "Octocat's GPG Key", + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [{ + "email": "octocat@users.noreply.github.com", + "verified": True + }], + "subkeys": [{ + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": False, + "can_encrypt_comms": True, + "can_encrypt_storage": True, + "can_certify": False, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False + }], + "can_sign": True, + "can_encrypt_comms": False, + "can_encrypt_storage": False, + "can_certify": True, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False, + "raw_key": "string" + }] +matching_keys: + description: An array of keys matching the specified name. Only present on state=present + type: list + returned: When state=present + sample: [{ + "id": 3, + "name": "Octocat's GPG Key", + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [{ + "email": "octocat@users.noreply.github.com", + "verified": True + }], + "subkeys": [{ + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": False, + "can_encrypt_comms": True, + "can_encrypt_storage": True, + "can_certify": False, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False + }], + "can_sign": True, + "can_encrypt_comms": False, + "can_encrypt_storage": False, + "can_certify": True, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False, + "raw_key": "string" + }] +key: + description: Metadata about the key just created. Only present on state=present + type: dict + returned: success + sample: { + "id": 3, + "name": "Octocat's GPG Key", + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [{ + "email": "octocat@users.noreply.github.com", + "verified": True + }], + "subkeys": [{ + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": False, + "can_encrypt_comms": True, + "can_encrypt_storage": True, + "can_certify": False, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False + }], + "can_sign": True, + "can_encrypt_comms": False, + "can_encrypt_storage": False, + "can_certify": True, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False, + "raw_key": "string" + } +''' + +EXAMPLES = ''' +- name: Read ASCII-armored GPG public key to authorize + ansible.builtin.command: + cmd: gpg --armor --export EXAMPLE_KEY_ID + register: gpg_public_key + +- name: Authorize key with GitHub + community.general.github_gpg_key: + name: Access Key for Some Machine + token: '{{ github_access_token }}' + armored_public_key: '{{ gpg_public_key.stdout }}' +''' + +import datetime +import json +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url + +from ansible_collections.community.general.plugins.module_utils.datetime import ( + now, +) + + +API_BASE = 'https://api.github.com' + + +class GitHubResponse(object): + def __init__(self, response, info): + self.content = response.read() + self.info = info + + def json(self): + return json.loads(self.content) + + def links(self): + links = {} + if 'link' in self.info: + link_header = self.info['link'] + matches = re.findall('<([^>]+)>; rel="([^"]+)"', link_header) + for url, rel in matches: + links[rel] = url + return links + + +class GitHubSession(object): + def __init__(self, module, token): + self.module = module + self.token = token + + def request(self, method, url, data=None): + headers = { + 'Authorization': 'token %s' % self.token, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github.v3+json', + } + response, info = fetch_url( + self.module, url, method=method, data=data, headers=headers) + if not (200 <= info['status'] < 400): + self.module.fail_json( + msg=(" failed to send request %s to %s: %s" + % (method, url, info['msg']))) + return GitHubResponse(response, info) + + +def get_all_keys(session): + url = API_BASE + result = [] + while url: + r = session.request('GET', url) + result.extend(r.json()) + url = r.links().get('next') + return result + + +def create_key(session, name, armored_public_key, check_mode): + if check_mode: + now_t = datetime.datetime.now() + return { + "id": 3, + "name": name, + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [{ + "email": "octocat@users.noreply.github.com", + "verified": True + }], + "subkeys": [{ + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": False, + "can_encrypt_comms": True, + "can_encrypt_storage": True, + "can_certify": False, + "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "expires_at": None, + "revoked": False + }], + "can_sign": True, + "can_encrypt_comms": False, + "can_encrypt_storage": False, + "can_certify": True, + "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "expires_at": None, + "revoked": False, + "raw_key": armored_public_key + } + else: + return session.request( + 'POST', + API_BASE + '/user/gpg_keys', + data=json.dumps({'name': name, 'raw_key': armored_public_key})).json() + + +def delete_keys(session, to_delete, check_mode): + if check_mode: + return + + for key in to_delete: + session.request('DELETE', API_BASE + '/user/keys/%s' % key["id"]) + + +def ensure_key_absent(session, name, check_mode): + to_delete = [key for key in get_all_keys(session) if key['name'] == name] + delete_keys(session, to_delete, check_mode=check_mode) + + return {'changed': bool(to_delete), + 'deleted_keys': to_delete} + + +def ensure_key_present(module, session, name, armored_public_key, force, check_mode): + all_keys = get_all_keys(session) + matching_keys = [k for k in all_keys if k['name'] == name] + deleted_keys = [] + + new_signature = armored_public_key + for key in all_keys: + existing_signature = key['raw_key'] + if new_signature == existing_signature and key['name'] != name: + module.fail_json(msg=( + "another key with the same content is already registered " + "under the name |{0}|").format(key['title'])) + + if matching_keys and force and matching_keys[0]['raw_key'] != new_signature: + delete_keys(session, matching_keys, check_mode=check_mode) + (deleted_keys, matching_keys) = (matching_keys, []) + + if not matching_keys: + key = create_key(session, name, armored_public_key, check_mode=check_mode) + else: + key = matching_keys[0] + + return { + 'changed': bool(deleted_keys or not matching_keys), + 'deleted_keys': deleted_keys, + 'matching_keys': matching_keys, + 'key': key + } + + +def run_module(module, token, name, armored_public_key, state, force, check_mode) + session = GitHubSession(module, token) + if state == 'present': + ensure_key_present(module, session, name, armored_public_key, force=force, + check_mode=check_mode) + elif state == 'absent': + result = ensure_key_absent(session, name, check_mode=check_mode) + + return result + + +def main(): + argument_spec = { + 'token': {'required': True, 'no_log': True}, + 'name': {'required': True}, + 'armored_public_key': {}, + 'state': {'choices': ['present', 'absent'], 'default': 'present'}, + 'force': {'default': True, 'type': 'bool'}, + } + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + armored_public_key = module.params.get('armored_public_key') + + if armored_public_key: + armored_public_key_parts = armored_public_key.split("\r\n") + armored_public_key_start = "-----BEGIN PGP PUBLIC KEY BLOCK-----" + armored_public_key_end = "-----END PGP PUBLIC KEY BLOCK-----" + if armored_public_key_parts[0] != armored_public_key_start or \ + armored_public_key_parts[-1] != armored_public_key_end: + module.fail_json(msg='"armored_public_key" parameter has an invalid format') + elif state == "present": + module.fail_json(msg='"armored_public_key" is required when state=present') + + result = run_module( + module=module, + token=module.params["token"], + name=module.params["name"], + armored_public_key=armored_public_key, + state=module.params["state"], + force=module.params["force"], + check_mode=check_mode + ) + module.exit_json(**result) + +if __name__ == "__main__": + main() From 556bee6b7027c57287999f3438e697775a77696c Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:23:28 -0700 Subject: [PATCH 02/19] github_gpg_key module integration tests --- .../targets/github_gpg_key/aliases | 6 ++++ .../targets/github_gpg_key/tasks/main.yml | 28 +++++++++++++++++++ .../targets/github_gpg_key/vars/main.yml | 8 ++++++ 3 files changed, 42 insertions(+) create mode 100644 tests/integration/targets/github_gpg_key/aliases create mode 100644 tests/integration/targets/github_gpg_key/tasks/main.yml create mode 100644 tests/integration/targets/github_gpg_key/vars/main.yml diff --git a/tests/integration/targets/github_gpg_key/aliases b/tests/integration/targets/github_gpg_key/aliases new file mode 100644 index 0000000000..428e8289dc --- /dev/null +++ b/tests/integration/targets/github_gpg_key/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/1 +destructive diff --git a/tests/integration/targets/github_gpg_key/tasks/main.yml b/tests/integration/targets/github_gpg_key/tasks/main.yml new file mode 100644 index 0000000000..5c63eca9a0 --- /dev/null +++ b/tests/integration/targets/github_gpg_key/tasks/main.yml @@ -0,0 +1,28 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Test code for the github_issue module. +# +# Copyright (c) 2024, Austin Lucas Lake +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create GPG key + github_gpg_key: + token: {{ token }} + name: {{ name }} + armored_gpg_key: {{ armored_gpg_key }} + register: created_gpg_key + +- name: Delete GPG key + github_gpg_key: + state: absent + token: {{ token }} + name: {{ name }} + register: deleted_gpg_key + +- assert: + that: + - created_gpg_key.id == deleted_gpg_key[0].id diff --git a/tests/integration/targets/github_gpg_key/vars/main.yml b/tests/integration/targets/github_gpg_key/vars/main.yml new file mode 100644 index 0000000000..cca21095ef --- /dev/null +++ b/tests/integration/targets/github_gpg_key/vars/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +token: # TODO +name: # TODO +armored_public_key: # TODO From c6e14d7ad6276d5a78423e1178f88634fe9ba34d Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:24:12 -0700 Subject: [PATCH 03/19] github_gpg_key module unit tests --- .../plugins/modules/test_github_gpg_key.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 tests/unit/plugins/modules/test_github_gpg_key.py diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py new file mode 100644 index 0000000000..cd1f2c1d29 --- /dev/null +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -0,0 +1,179 @@ +# Copyright (c) 2024 Austin Lucas Lake +# Based on tests/unit/plugins/modules/test_github_repo.py by Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re +import json +import sys +from httmock import with_httmock, urlmatch, response +from ansible_collections.community.general.tests.unit.compat import unittest +from ansible_collections.community.general.plugins.modules import github_gpg_key + +GITHUB_MINIMUM_PYTHON_VERSION = (2, 7) + + +@urlmatch(netloc=r'.*') +def debug_mock(url, request): + print(request.original.__dict__) + + +@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="get") +def list_gpg_keys_mock(url, request): + # https://docs.github.com/en/rest/reference/repos#get-a-repository + headers = {'content-type': 'application/json'} + content = [{ + "id": 3, + "name": "Octocat's GPG Key", + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [{ + "email": "octocat@users.noreply.github.com", + "verified": True + }], + "subkeys": [{ + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": False, + "can_encrypt_comms": True, + "can_encrypt_storage": True, + "can_certify": False, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False + }], + "can_sign": True, + "can_encrypt_comms": False, + "can_encrypt_storage": False, + "can_certify": True, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False, + "raw_key": "string" + }] + + content = json.dumps(content).encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="post") +def create_gpg_key_mock(url, request): + gpg_key = json.loads(request.body) + + now_t = datetime.datetime.now() + + headers = {'content-type': 'application/json'} + # https://docs.github.com/en/rest/reference/repos#create-a-repository-for-the-authenticated-user + content = { + "id": 3, + "name": gpg_key["name"], + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [{ + "email": "octocat@users.noreply.github.com", + "verified": True + }], + "subkeys": [{ + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": False, + "can_encrypt_comms": True, + "can_encrypt_storage": True, + "can_certify": False, + "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "expires_at": None, + "revoked": False + }], + "can_sign": True, + "can_encrypt_comms": False, + "can_encrypt_storage": False, + "can_certify": True, + "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "expires_at": None, + "revoked": False, + "raw_key": gpg_key.get("armored_public_key") + } + content = json.dumps(content).encode("utf-8") + return response(201, content, headers, None, 5, request) + + +@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") +def delete_repo_mock(url, request): + # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user + return response(204, None, None, None, 5, request) + + +@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") +def delete_gpg_key_notfound_mock(url, request): + # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user + return response(404, "{\"message\": \"Not Found\"}", "", "Not Found", 5, request) + + +class TestGithubRepo(unittest.TestCase): + def setUp(self): + argument_spec = { + 'token': {'required': True, 'no_log': True}, + 'name': {'required': True}, + 'armored_public_key': {}, + 'state': {'choices': ['present', 'absent'], 'default': 'present'}, + 'force': {'default': True, 'type': 'bool'}, + } + self.module = AnsibleModule( + argument_spec=argument_spec, + support_check_mode=True + ) + + @with_httmock(list_gpg_keys_mock) + @with_httmock(create_gpg_key_mock) + def test_create_gpg_key_repo(self): + result = github_gpg_key.run_module( + module=self.module + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="present", + force=True + ) + self.assertEqual(result['changed'], True) + self.assertEqual(result['key']['name'], 'GPG public key') + + @with_httmock(list_gpg_keys_mock) + @with_httmock(delete_gpg_key_mock) + def test_delete_user_repo(self): + result = github_gpg_key.run_module( + module=self.module + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="present", + force=True + ) + self.assertEqual(result['changed'], True) + + @with_httmock(list_gpg_keys_mock) + @with_httmock(delete_gpg_notfound_mock) + def test_delete_gpg_key_notfound(self): + result = github_gpg_key.run_module( + module=self.module + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="present", + force=True + ) + self.assertEqual(result['changed'], False) + + +if __name__ == "__main__": + unittest.main() From 6760591c546f222a995d1f91e6886723c47c7f67 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:24:51 -0700 Subject: [PATCH 04/19] added github_gpg_key to maintainers list --- .github/BOTMETA.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 6063403911..29a78f29d0 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -611,6 +611,8 @@ files: maintainers: stpierre $modules/github_deploy_key.py: maintainers: bincyber + $modules/github_gpg_key.py: + maintainers: austinlucaslake $modules/github_issue.py: maintainers: Akasurde $modules/github_key.py: From 90a8906d3eefefd97fd9d33e00c01f7d6689eb4d Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Wed, 1 May 2024 11:03:10 -0700 Subject: [PATCH 05/19] fixed syntax errors --- plugins/modules/github_gpg_key.py | 19 ++++++++++--------- .../plugins/modules/test_github_gpg_key.py | 10 +++++----- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index e03b045134..d820eda901 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -13,9 +13,9 @@ __metaclass__ = type DOCUMENTATION = ''' module: github_gpg_key -short_description: Manage GitHub access keys +short_description: Manage GitHub GPG keys description: - - Creates, removes, or updates GitHub GPG keys. + - Creates, removes, or list GitHub GPG keys. extends_documentation_fragment: - community.general.attributes attributes: @@ -224,7 +224,7 @@ class GitHubSession(object): headers = { 'Authorization': 'token %s' % self.token, 'Content-Type': 'application/json', - 'Accept': 'application/vnd.github.v3+json', + 'Accept': 'application/vnd.github.v3+json' } response, info = fetch_url( self.module, url, method=method, data=data, headers=headers) @@ -268,7 +268,7 @@ def create_key(session, name, armored_public_key, check_mode): "can_encrypt_comms": True, "can_encrypt_storage": True, "can_certify": False, - "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "created_at": now_t.strftime("%Y-%m-%dT%H:%M:%SZ"), "expires_at": None, "revoked": False }], @@ -276,7 +276,7 @@ def create_key(session, name, armored_public_key, check_mode): "can_encrypt_comms": False, "can_encrypt_storage": False, "can_certify": True, - "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "created_at": now_t.strftime("%Y-%m-%dT%H:%M:%SZ"), "expires_at": None, "revoked": False, "raw_key": armored_public_key @@ -334,14 +334,14 @@ def ensure_key_present(module, session, name, armored_public_key, force, check_m } -def run_module(module, token, name, armored_public_key, state, force, check_mode) +def run_module(module, token, name, armored_public_key, state, force, check_mode): session = GitHubSession(module, token) if state == 'present': ensure_key_present(module, session, name, armored_public_key, force=force, check_mode=check_mode) elif state == 'absent': result = ensure_key_absent(session, name, check_mode=check_mode) - + return result @@ -355,7 +355,7 @@ def main(): } module = AnsibleModule( argument_spec=argument_spec, - supports_check_mode=True, + supports_check_mode=True ) armored_public_key = module.params.get('armored_public_key') @@ -365,7 +365,7 @@ def main(): armored_public_key_start = "-----BEGIN PGP PUBLIC KEY BLOCK-----" armored_public_key_end = "-----END PGP PUBLIC KEY BLOCK-----" if armored_public_key_parts[0] != armored_public_key_start or \ - armored_public_key_parts[-1] != armored_public_key_end: + armored_public_key_parts[-1] != armored_public_key_end: module.fail_json(msg='"armored_public_key" parameter has an invalid format') elif state == "present": module.fail_json(msg='"armored_public_key" is required when state=present') @@ -381,5 +381,6 @@ def main(): ) module.exit_json(**result) + if __name__ == "__main__": main() diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py index cd1f2c1d29..9d64bc216b 100644 --- a/tests/unit/plugins/modules/test_github_gpg_key.py +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -68,7 +68,7 @@ def create_gpg_key_mock(url, request): gpg_key = json.loads(request.body) now_t = datetime.datetime.now() - + headers = {'content-type': 'application/json'} # https://docs.github.com/en/rest/reference/repos#create-a-repository-for-the-authenticated-user content = { @@ -91,7 +91,7 @@ def create_gpg_key_mock(url, request): "can_encrypt_comms": True, "can_encrypt_storage": True, "can_certify": False, - "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "created_at": now_t.strftime("%Y-%m-%dT%H:%M:%SZ"), "expires_at": None, "revoked": False }], @@ -99,7 +99,7 @@ def create_gpg_key_mock(url, request): "can_encrypt_comms": False, "can_encrypt_storage": False, "can_certify": True, - "created_at": datetime.strftime(now_t, "%Y-%m-%dT%H:%M:%SZ"), + "created_at": now_T.strftime("%Y-%m-%dT%H:%M:%SZ"), "expires_at": None, "revoked": False, "raw_key": gpg_key.get("armored_public_key") @@ -127,7 +127,7 @@ class TestGithubRepo(unittest.TestCase): 'name': {'required': True}, 'armored_public_key': {}, 'state': {'choices': ['present', 'absent'], 'default': 'present'}, - 'force': {'default': True, 'type': 'bool'}, + 'force': {'default': True, 'type': 'bool'} } self.module = AnsibleModule( argument_spec=argument_spec, @@ -138,7 +138,7 @@ class TestGithubRepo(unittest.TestCase): @with_httmock(create_gpg_key_mock) def test_create_gpg_key_repo(self): result = github_gpg_key.run_module( - module=self.module + module=self.module, token="github_access_token", name="GPG public key", armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", From b1db74bb726f47aa931128917dd05a32446fb3cc Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 9 May 2024 21:40:49 -0700 Subject: [PATCH 06/19] require armored_public_key if state=present Co-authored-by: Felix Fontein --- plugins/modules/github_gpg_key.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index d820eda901..7a19549ddb 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -355,7 +355,10 @@ def main(): } module = AnsibleModule( argument_spec=argument_spec, - supports_check_mode=True + supports_check_mode=True, + required_if=[ + ('state', 'present', ['armored_public_key']), + ], ) armored_public_key = module.params.get('armored_public_key') From ee5c34b6ca45605fd9f5a215ee7f1314c4d58052 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 9 May 2024 21:43:23 -0700 Subject: [PATCH 07/19] added directives in documentation for GitHub token scopes Co-authored-by: Felix Fontein --- plugins/modules/github_gpg_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index 7a19549ddb..c4f2c80b77 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -26,7 +26,7 @@ attributes: options: token: description: - - GitHub OAuth or personal access token (classic) with the read:gpg_key, write:gpg_key, and admin:gpg_key scopes needed to manage GPG keys. + - GitHub OAuth or personal access token (classic) with the C(read:gpg_key), C(write:gpg_key), and C(admin:gpg_key) scopes needed to manage GPG keys. required: true type: str name: From f2b71ac54743be52f9ac7418a963c0b23e80a9aa Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 9 May 2024 21:46:46 -0700 Subject: [PATCH 08/19] added elements qualifier to deleted_keys Co-authored-by: Felix Fontein --- plugins/modules/github_gpg_key.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index c4f2c80b77..679f48bcd0 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -59,6 +59,7 @@ RETURN = ''' deleted_keys: description: An array of key objects that were deleted. Only present on state=absent type: list + elements: dict returned: When state=absent sample: [{ "id": 3, From 0cc57047c01eee7093583f6a65e0fd8b20844368 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 9 May 2024 21:48:27 -0700 Subject: [PATCH 09/19] change booleans to true and false for YAML Co-authored-by: Felix Fontein --- plugins/modules/github_gpg_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index 679f48bcd0..41f252e5d9 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -78,7 +78,7 @@ deleted_keys: "public_key": "zsBNBFayYZ...", "emails": [], "can_sign": False, - "can_encrypt_comms": True, + "can_encrypt_comms": true, "can_encrypt_storage": True, "can_certify": False, "created_at": "2016-03-24T11:31:04-06:00", From 0efdeb9c5309ef68b61defb68b51418a6966bac0 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 9 May 2024 21:48:59 -0700 Subject: [PATCH 10/19] added version_added (9.0.0) to documentation Co-authored-by: Felix Fontein --- plugins/modules/github_gpg_key.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index 41f252e5d9..7b8890598d 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -14,6 +14,7 @@ __metaclass__ = type DOCUMENTATION = ''' module: github_gpg_key short_description: Manage GitHub GPG keys +version_added: 9.0.0 description: - Creates, removes, or list GitHub GPG keys. extends_documentation_fragment: From eb2c3892ed5736d0686ea464ebb5b2e2b494b915 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 9 May 2024 21:49:37 -0700 Subject: [PATCH 11/19] added directives for state parameter in documentation Co-authored-by: Felix Fontein --- plugins/modules/github_gpg_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index 7b8890598d..afbf6d81f4 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -61,7 +61,7 @@ deleted_keys: description: An array of key objects that were deleted. Only present on state=absent type: list elements: dict - returned: When state=absent + returned: When O(state=absent) sample: [{ "id": 3, "name": "Octocat's GPG Key", From eafe80c92474b5341aa6890cd8062979f0c323a5 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> Date: Thu, 9 May 2024 23:58:11 -0700 Subject: [PATCH 12/19] updated documentation and unit testing --- plugins/modules/github_gpg_key.py | 352 ++++++++---------- .../plugins/modules/test_github_gpg_key.py | 56 +-- 2 files changed, 197 insertions(+), 211 deletions(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index afbf6d81f4..5b68a1f1f9 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -3,7 +3,6 @@ # Copyright 2024, Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> # Based on community.general.github_key module by Ansible Project - # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -13,10 +12,11 @@ __metaclass__ = type DOCUMENTATION = ''' module: github_gpg_key +author: Austin Lucas Lake (@austinlucaslake) short_description: Manage GitHub GPG keys version_added: 9.0.0 description: - - Creates, removes, or list GitHub GPG keys. + - Creates or removes GitHub GPG keys for an authenticated user. extends_documentation_fragment: - community.general.attributes attributes: @@ -42,7 +42,7 @@ options: state: description: - Whether to remove a key, ensure that it exists, or update its value. - choices: ['present', 'absent'] + choices: [ 'present', 'absent' ] default: 'present' type: str force: @@ -52,8 +52,6 @@ options: set if no key with the given O(name) exists. type: bool default: true - -author: Austin Lucas Lake (@austinlucaslake) ''' RETURN = ''' @@ -63,140 +61,133 @@ deleted_keys: elements: dict returned: When O(state=absent) sample: [{ - "id": 3, - "name": "Octocat's GPG Key", - "primary_key_id": 2, - "key_id": "3262EFF25BA0D270", - "public_key": "xsBNBFayYZ...", - "emails": [{ - "email": "octocat@users.noreply.github.com", - "verified": True + 'id': 3, + 'name': 'Octocat's GPG Key', + 'primary_key_id': 2, + 'key_id': '3262EFF25BA0D270', + 'public_key': 'xsBNBFayYZ...', + 'emails': [{ + 'email': 'octocat@users.noreply.github.com', + 'verified': true }], - "subkeys": [{ - "id": 4, - "primary_key_id": 3, - "key_id": "4A595D4C72EE49C7", - "public_key": "zsBNBFayYZ...", - "emails": [], - "can_sign": False, - "can_encrypt_comms": true, - "can_encrypt_storage": True, - "can_certify": False, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False + 'subkeys': [{ + 'id': 4, + 'primary_key_id': 3, + 'key_id': '4A595D4C72EE49C7', + 'public_key': 'zsBNBFayYZ...', + 'emails': [], + 'can_sign': false, + 'can_encrypt_comms': true, + 'can_encrypt_storage': true, + 'can_certify': false, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false }], - "can_sign": True, - "can_encrypt_comms": False, - "can_encrypt_storage": False, - "can_certify": True, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False, - "raw_key": "string" + 'can_sign': true, + 'can_encrypt_comms': false, + 'can_encrypt_storage': false, + 'can_certify': true, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false, + 'raw_key': 'string' }] matching_keys: description: An array of keys matching the specified name. Only present on state=present type: list - returned: When state=present + elements: dict + returned: When O(state=present) sample: [{ - "id": 3, - "name": "Octocat's GPG Key", - "primary_key_id": 2, - "key_id": "3262EFF25BA0D270", - "public_key": "xsBNBFayYZ...", - "emails": [{ - "email": "octocat@users.noreply.github.com", - "verified": True + 'id': 3, + 'name': 'Octocat's GPG Key', + 'primary_key_id': 2, + 'key_id': '3262EFF25BA0D270', + 'public_key': 'xsBNBFayYZ...', + 'emails': [{ + 'email': 'octocat@users.noreply.github.com', + 'verified': true }], - "subkeys": [{ - "id": 4, - "primary_key_id": 3, - "key_id": "4A595D4C72EE49C7", - "public_key": "zsBNBFayYZ...", - "emails": [], - "can_sign": False, - "can_encrypt_comms": True, - "can_encrypt_storage": True, - "can_certify": False, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False + 'subkeys': [{ + 'id': 4, + 'primary_key_id': 3, + 'key_id': '4A595D4C72EE49C7', + 'public_key': 'zsBNBFayYZ...', + 'emails': [], + 'can_sign': false, + 'can_encrypt_comms': true, + 'can_encrypt_storage': true, + 'can_certify': false, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false }], - "can_sign": True, - "can_encrypt_comms": False, - "can_encrypt_storage": False, - "can_certify": True, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False, - "raw_key": "string" + 'can_sign': true, + 'can_encrypt_comms': false, + 'can_encrypt_storage': false, + 'can_certify': true, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false, + 'raw_key': 'string' }] key: description: Metadata about the key just created. Only present on state=present type: dict - returned: success + returned: When O(state=present) sample: { - "id": 3, - "name": "Octocat's GPG Key", - "primary_key_id": 2, - "key_id": "3262EFF25BA0D270", - "public_key": "xsBNBFayYZ...", - "emails": [{ - "email": "octocat@users.noreply.github.com", - "verified": True + 'id': 3, + 'name': 'Octocat's GPG Key', + 'primary_key_id': 2, + 'key_id': '3262EFF25BA0D270', + 'public_key': 'xsBNBFayYZ...', + 'emails': [{ + 'email': 'octocat@users.noreply.github.com', + 'verified': true }], - "subkeys": [{ - "id": 4, - "primary_key_id": 3, - "key_id": "4A595D4C72EE49C7", - "public_key": "zsBNBFayYZ...", - "emails": [], - "can_sign": False, - "can_encrypt_comms": True, - "can_encrypt_storage": True, - "can_certify": False, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False + 'subkeys': [{ + 'id': 4, + 'primary_key_id': 3, + 'key_id': '4A595D4C72EE49C7', + 'public_key': 'zsBNBFayYZ...', + 'emails': [], + 'can_sign': false, + 'can_encrypt_comms': true, + 'can_encrypt_storage': true, + 'can_certify': False, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false }], - "can_sign": True, - "can_encrypt_comms": False, - "can_encrypt_storage": False, - "can_certify": True, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False, - "raw_key": "string" + 'can_sign': true, + 'can_encrypt_comms': false, + 'can_encrypt_storage': false, + 'can_certify': true, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false, + 'raw_key': 'string' } ''' EXAMPLES = ''' -- name: Read ASCII-armored GPG public key to authorize - ansible.builtin.command: - cmd: gpg --armor --export EXAMPLE_KEY_ID - register: gpg_public_key - - name: Authorize key with GitHub community.general.github_gpg_key: - name: Access Key for Some Machine - token: '{{ github_access_token }}' - armored_public_key: '{{ gpg_public_key.stdout }}' + name: My GPG Key + token: '{{ token }}' + armored_public_key: '{{ armored_public_key }}' ''' -import datetime import json import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url -from ansible_collections.community.general.plugins.module_utils.datetime import ( - now, -) +from ansible_collections.community.general.plugins.module_utils.datetime import now -API_BASE = 'https://api.github.com' +API_BASE = 'https://api.github.com/user/gpg_keys' class GitHubResponse(object): @@ -224,16 +215,14 @@ class GitHubSession(object): def request(self, method, url, data=None): headers = { - 'Authorization': 'token %s' % self.token, + 'Authorization': 'token {}'.format(self.token), 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.v3+json' } response, info = fetch_url( self.module, url, method=method, data=data, headers=headers) if not (200 <= info['status'] < 400): - self.module.fail_json( - msg=(" failed to send request %s to %s: %s" - % (method, url, info['msg']))) + self.module.fail_json('failed to send request {0} to {1}: {2}'.format(method, url, info['msg'])) return GitHubResponse(response, info) @@ -249,44 +238,44 @@ def get_all_keys(session): def create_key(session, name, armored_public_key, check_mode): if check_mode: - now_t = datetime.datetime.now() + now_t = now() return { - "id": 3, - "name": name, - "primary_key_id": 2, - "key_id": "3262EFF25BA0D270", - "public_key": "xsBNBFayYZ...", - "emails": [{ - "email": "octocat@users.noreply.github.com", - "verified": True + 'id': 3, + 'name': name, + 'primary_key_id': 2, + 'key_id': '3262EFF25BA0D270', + 'public_key': 'xsBNBFayYZ...', + 'emails': [{ + 'email': 'octocat@users.noreply.github.com', + 'verified': True }], - "subkeys": [{ - "id": 4, - "primary_key_id": 3, - "key_id": "4A595D4C72EE49C7", - "public_key": "zsBNBFayYZ...", - "emails": [], - "can_sign": False, - "can_encrypt_comms": True, - "can_encrypt_storage": True, - "can_certify": False, - "created_at": now_t.strftime("%Y-%m-%dT%H:%M:%SZ"), - "expires_at": None, - "revoked": False + 'subkeys': [{ + 'id': 4, + 'primary_key_id': 3, + 'key_id': '4A595D4C72EE49C7', + 'public_key': 'zsBNBFayYZ...', + 'emails': [], + 'can_sign': False, + 'can_encrypt_comms': True, + 'can_encrypt_storage': True, + 'can_certify': False, + 'created_at': now_t.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'expires_at': None, + 'revoked': False }], - "can_sign": True, - "can_encrypt_comms": False, - "can_encrypt_storage": False, - "can_certify": True, - "created_at": now_t.strftime("%Y-%m-%dT%H:%M:%SZ"), - "expires_at": None, - "revoked": False, - "raw_key": armored_public_key + 'can_sign': True, + 'can_encrypt_comms': False, + 'can_encrypt_storage': False, + 'can_certify': True, + 'created_at': now_t.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'expires_at': None, + 'revoked': False, + 'raw_key': armored_public_key } else: return session.request( 'POST', - API_BASE + '/user/gpg_keys', + API_BASE, data=json.dumps({'name': name, 'raw_key': armored_public_key})).json() @@ -295,7 +284,7 @@ def delete_keys(session, to_delete, check_mode): return for key in to_delete: - session.request('DELETE', API_BASE + '/user/keys/%s' % key["id"]) + session.request('DELETE', API_BASE + '/' + key['id']) def ensure_key_absent(session, name, check_mode): @@ -315,13 +304,11 @@ def ensure_key_present(module, session, name, armored_public_key, force, check_m for key in all_keys: existing_signature = key['raw_key'] if new_signature == existing_signature and key['name'] != name: - module.fail_json(msg=( - "another key with the same content is already registered " - "under the name |{0}|").format(key['title'])) + module.fail_json('Another key with the same content is already registered under the name |{0}|'.format(key['title'])) if matching_keys and force and matching_keys[0]['raw_key'] != new_signature: delete_keys(session, matching_keys, check_mode=check_mode) - (deleted_keys, matching_keys) = (matching_keys, []) + deleted_keys, matching_keys = matching_keys, [] if not matching_keys: key = create_key(session, name, armored_public_key, check_mode=check_mode) @@ -336,56 +323,47 @@ def ensure_key_present(module, session, name, armored_public_key, force, check_m } -def run_module(module, token, name, armored_public_key, state, force, check_mode): - session = GitHubSession(module, token) - if state == 'present': - ensure_key_present(module, session, name, armored_public_key, force=force, - check_mode=check_mode) - elif state == 'absent': - result = ensure_key_absent(session, name, check_mode=check_mode) +def validate_key(module, armored_public_key): + armored_public_key_parts = armored_public_key.splitlines() + if armored_public_key_parts[0] != '-----BEGIN PGP PUBLIC KEY BLOCK-----' or armored_public_key_parts[-1] != '-----END PGP PUBLIC KEY BLOCK-----': + module.fail_json(msg='"armored_public_key" parameter has an invalid format') + +def run_module(module, params, check_mode): + session = GitHubSession(module, params['token']) + if params['state'] == 'present': + validate_key(module, params['armored_public_key']) + result = ensure_key_present(module, session, params['name'], params['armored_public_key'], params['force'], check_mode) + else: + result = ensure_key_absent(session, params['name'], check_mode) return result def main(): - argument_spec = { - 'token': {'required': True, 'no_log': True}, - 'name': {'required': True}, - 'armored_public_key': {}, - 'state': {'choices': ['present', 'absent'], 'default': 'present'}, - 'force': {'default': True, 'type': 'bool'}, - } module = AnsibleModule( - argument_spec=argument_spec, + argument_spec=dict( + state=dict(type='str', default='present', choice=['present', 'absent']), + token=dict(type='str', required=True, no_log=True), + name=dict(type='str', required=True), + armored_public_key=dict(type='str', no_log=True), + force=dict(type='bool', default=True) + ), supports_check_mode=True, required_if=[ - ('state', 'present', ['armored_public_key']), - ], + ['state', 'present', ['armored_public_key']], + ] ) - armored_public_key = module.params.get('armored_public_key') - - if armored_public_key: - armored_public_key_parts = armored_public_key.split("\r\n") - armored_public_key_start = "-----BEGIN PGP PUBLIC KEY BLOCK-----" - armored_public_key_end = "-----END PGP PUBLIC KEY BLOCK-----" - if armored_public_key_parts[0] != armored_public_key_start or \ - armored_public_key_parts[-1] != armored_public_key_end: - module.fail_json(msg='"armored_public_key" parameter has an invalid format') - elif state == "present": - module.fail_json(msg='"armored_public_key" is required when state=present') - - result = run_module( - module=module, - token=module.params["token"], - name=module.params["name"], - armored_public_key=armored_public_key, - state=module.params["state"], - force=module.params["force"], - check_mode=check_mode - ) - module.exit_json(**result) + try: + result = run_module( + module=module, + params=module.params, + check_mode=module.check_mode + ) + module.exit_json(**result) + except Exception as e: + module.fail_json(str(e)) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py index 9d64bc216b..7dc22d1094 100644 --- a/tests/unit/plugins/modules/test_github_gpg_key.py +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -122,16 +122,18 @@ def delete_gpg_key_notfound_mock(url, request): class TestGithubRepo(unittest.TestCase): def setUp(self): - argument_spec = { - 'token': {'required': True, 'no_log': True}, - 'name': {'required': True}, - 'armored_public_key': {}, - 'state': {'choices': ['present', 'absent'], 'default': 'present'}, - 'force': {'default': True, 'type': 'bool'} - } self.module = AnsibleModule( - argument_spec=argument_spec, - support_check_mode=True + argument_spec=dict( + state=dict(type='str', default='present', choice=['present', 'absent']), + token=dict(type='str', required=True, no_log=True), + name=dict(type='str', required=True), + armored_public_key=dict(type='str', no_log=True), + force=dict(type='bool', default=True) + ), + supports_check_mode=True, + required_if=[ + ['state', 'present', ['armored_public_key']], + ] ) @with_httmock(list_gpg_keys_mock) @@ -139,11 +141,13 @@ class TestGithubRepo(unittest.TestCase): def test_create_gpg_key_repo(self): result = github_gpg_key.run_module( module=self.module, - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", - state="present", - force=True + params=dict( + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="present", + force=True + ) ) self.assertEqual(result['changed'], True) self.assertEqual(result['key']['name'], 'GPG public key') @@ -153,11 +157,13 @@ class TestGithubRepo(unittest.TestCase): def test_delete_user_repo(self): result = github_gpg_key.run_module( module=self.module - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", - state="present", - force=True + params=dict( + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="absent", + force=True + ) ) self.assertEqual(result['changed'], True) @@ -166,11 +172,13 @@ class TestGithubRepo(unittest.TestCase): def test_delete_gpg_key_notfound(self): result = github_gpg_key.run_module( module=self.module - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", - state="present", - force=True + params=dict( + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="absent", + force=True + ) ) self.assertEqual(result['changed'], False) From 4a535a22f3908a88b5f1769cf6e2e5238b8bb4d9 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake Date: Fri, 14 Mar 2025 17:42:54 -0700 Subject: [PATCH 13/19] Fixed documentation errors, syntax mistakes, updated author email, and tested --- plugins/modules/github_gpg_key.py | 50 ++++--------------- .../targets/github_gpg_key/tasks/main.yml | 12 ++--- .../plugins/modules/test_github_gpg_key.py | 16 +++--- 3 files changed, 23 insertions(+), 55 deletions(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index 5b68a1f1f9..12fae18402 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2024, Austin Lucas Lake <53884490+austinlucaslake@users.noreply.github.com> +# Copyright (c) 2024-2025, Austin Lucas Lake # Based on community.general.github_key module by Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -14,7 +14,7 @@ DOCUMENTATION = ''' module: github_gpg_key author: Austin Lucas Lake (@austinlucaslake) short_description: Manage GitHub GPG keys -version_added: 9.0.0 +version_added: 10.5.0 description: - Creates or removes GitHub GPG keys for an authenticated user. extends_documentation_fragment: @@ -48,7 +48,7 @@ options: force: description: - The default is V(true), which will replace the existing remote key - if it is different than O(pubkey). If V(false), the key will only be + if it is different than O(armored_public_key). If V(false), the key will only be set if no key with the given O(name) exists. type: bool default: true @@ -62,7 +62,7 @@ deleted_keys: returned: When O(state=absent) sample: [{ 'id': 3, - 'name': 'Octocat's GPG Key', + 'name': "Octocat's GPG Key", 'primary_key_id': 2, 'key_id': '3262EFF25BA0D270', 'public_key': 'xsBNBFayYZ...', @@ -100,7 +100,7 @@ matching_keys: returned: When O(state=present) sample: [{ 'id': 3, - 'name': 'Octocat's GPG Key', + 'name': "Octocat's GPG Key", 'primary_key_id': 2, 'key_id': '3262EFF25BA0D270', 'public_key': 'xsBNBFayYZ...', @@ -137,7 +137,7 @@ key: returned: When O(state=present) sample: { 'id': 3, - 'name': 'Octocat's GPG Key', + 'name': "Octocat's GPG Key", 'primary_key_id': 2, 'key_id': '3262EFF25BA0D270', 'public_key': 'xsBNBFayYZ...', @@ -239,39 +239,7 @@ def get_all_keys(session): def create_key(session, name, armored_public_key, check_mode): if check_mode: now_t = now() - return { - 'id': 3, - 'name': name, - 'primary_key_id': 2, - 'key_id': '3262EFF25BA0D270', - 'public_key': 'xsBNBFayYZ...', - 'emails': [{ - 'email': 'octocat@users.noreply.github.com', - 'verified': True - }], - 'subkeys': [{ - 'id': 4, - 'primary_key_id': 3, - 'key_id': '4A595D4C72EE49C7', - 'public_key': 'zsBNBFayYZ...', - 'emails': [], - 'can_sign': False, - 'can_encrypt_comms': True, - 'can_encrypt_storage': True, - 'can_certify': False, - 'created_at': now_t.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'expires_at': None, - 'revoked': False - }], - 'can_sign': True, - 'can_encrypt_comms': False, - 'can_encrypt_storage': False, - 'can_certify': True, - 'created_at': now_t.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'expires_at': None, - 'revoked': False, - 'raw_key': armored_public_key - } + return {} else: return session.request( 'POST', @@ -342,7 +310,7 @@ def run_module(module, params, check_mode): def main(): module = AnsibleModule( argument_spec=dict( - state=dict(type='str', default='present', choice=['present', 'absent']), + state=dict(type='str', default='present', choices=['present', 'absent']), token=dict(type='str', required=True, no_log=True), name=dict(type='str', required=True), armored_public_key=dict(type='str', no_log=True), @@ -362,7 +330,7 @@ def main(): ) module.exit_json(**result) except Exception as e: - module.fail_json(str(e)) + module.fail_json(msg=str(e)) if __name__ == '__main__': diff --git a/tests/integration/targets/github_gpg_key/tasks/main.yml b/tests/integration/targets/github_gpg_key/tasks/main.yml index 5c63eca9a0..f1faa4d267 100644 --- a/tests/integration/targets/github_gpg_key/tasks/main.yml +++ b/tests/integration/targets/github_gpg_key/tasks/main.yml @@ -5,22 +5,22 @@ # Test code for the github_issue module. # -# Copyright (c) 2024, Austin Lucas Lake +# Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - name: Create GPG key github_gpg_key: - token: {{ token }} - name: {{ name }} - armored_gpg_key: {{ armored_gpg_key }} + token: "{{ token }}" + name: "{{ name }}" + armored_gpg_key: "{{ armored_gpg_key }}" register: created_gpg_key - name: Delete GPG key github_gpg_key: state: absent - token: {{ token }} - name: {{ name }} + token: "{{ token }}" + name: "{{ name }}" register: deleted_gpg_key - assert: diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py index 7dc22d1094..8c816c9ca7 100644 --- a/tests/unit/plugins/modules/test_github_gpg_key.py +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 Austin Lucas Lake +# Copyright (c) 2024-2025, Austin Lucas Lake, # Based on tests/unit/plugins/modules/test_github_repo.py by Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -6,10 +6,10 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import re import json -import sys +import datetime from httmock import with_httmock, urlmatch, response +from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.tests.unit.compat import unittest from ansible_collections.community.general.plugins.modules import github_gpg_key @@ -99,7 +99,7 @@ def create_gpg_key_mock(url, request): "can_encrypt_comms": False, "can_encrypt_storage": False, "can_certify": True, - "created_at": now_T.strftime("%Y-%m-%dT%H:%M:%SZ"), + "created_at": now_t.strftime("%Y-%m-%dT%H:%M:%SZ"), "expires_at": None, "revoked": False, "raw_key": gpg_key.get("armored_public_key") @@ -109,7 +109,7 @@ def create_gpg_key_mock(url, request): @urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") -def delete_repo_mock(url, request): +def delete_gpg_key_mock(url, request): # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user return response(204, None, None, None, 5, request) @@ -156,7 +156,7 @@ class TestGithubRepo(unittest.TestCase): @with_httmock(delete_gpg_key_mock) def test_delete_user_repo(self): result = github_gpg_key.run_module( - module=self.module + module=self.module, params=dict( token="github_access_token", name="GPG public key", @@ -168,10 +168,10 @@ class TestGithubRepo(unittest.TestCase): self.assertEqual(result['changed'], True) @with_httmock(list_gpg_keys_mock) - @with_httmock(delete_gpg_notfound_mock) + @with_httmock(delete_gpg_key_notfound_mock) def test_delete_gpg_key_notfound(self): result = github_gpg_key.run_module( - module=self.module + module=self.module, params=dict( token="github_access_token", name="GPG public key", From b6bba9bdbb41ec4b7dcc5175fbe475ceec3c0560 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake Date: Sat, 15 Mar 2025 06:50:40 -0700 Subject: [PATCH 14/19] condensed two main execution paths and refactored functions --- plugins/modules/github_gpg_key.py | 318 ++++++++++++++---------------- 1 file changed, 151 insertions(+), 167 deletions(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index 12fae18402..e2748dfdef 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -33,34 +33,30 @@ options: name: description: - GPG key name - required: true type: str armored_public_key: description: - ASCII-armored GPG public key value. Required when O(state=present). type: str + gpg_key_id: + description: + - GPG key id. Required when O(state=absent). + type: int state: description: - Whether to remove a key, ensure that it exists, or update its value. choices: [ 'present', 'absent' ] default: 'present' type: str - force: - description: - - The default is V(true), which will replace the existing remote key - if it is different than O(armored_public_key). If V(false), the key will only be - set if no key with the given O(name) exists. - type: bool - default: true ''' RETURN = ''' -deleted_keys: - description: An array of key objects that were deleted. Only present on state=absent +deleted_key: + description: A GPG key that was deleted from GitHub. Only present on O(state=absent). type: list elements: dict - returned: When O(state=absent) - sample: [{ + returned: changed or success + sample: { 'id': 3, 'name': "Octocat's GPG Key", 'primary_key_id': 2, @@ -92,49 +88,48 @@ deleted_keys: 'expires_at': '2016-03-24T11:31:04-07:00', 'revoked': false, 'raw_key': 'string' - }] -matching_keys: - description: An array of keys matching the specified name. Only present on state=present - type: list - elements: dict - returned: When O(state=present) - sample: [{ - 'id': 3, - 'name': "Octocat's GPG Key", - 'primary_key_id': 2, - 'key_id': '3262EFF25BA0D270', - 'public_key': 'xsBNBFayYZ...', - 'emails': [{ - 'email': 'octocat@users.noreply.github.com', - 'verified': true - }], - 'subkeys': [{ - 'id': 4, - 'primary_key_id': 3, - 'key_id': '4A595D4C72EE49C7', - 'public_key': 'zsBNBFayYZ...', - 'emails': [], - 'can_sign': false, - 'can_encrypt_comms': true, - 'can_encrypt_storage': true, - 'can_certify': false, - 'created_at': '2016-03-24T11:31:04-06:00', - 'expires_at': '2016-03-24T11:31:04-07:00', - 'revoked': false - }], - 'can_sign': true, - 'can_encrypt_comms': false, - 'can_encrypt_storage': false, - 'can_certify': true, - 'created_at': '2016-03-24T11:31:04-06:00', - 'expires_at': '2016-03-24T11:31:04-07:00', - 'revoked': false, - 'raw_key': 'string' - }] -key: - description: Metadata about the key just created. Only present on state=present + } +matching_key: + description: A matching GPG key found on GitHub. Only present when O(state=present) and no new key is created. type: dict - returned: When O(state=present) + returned: not changed + sample: { + 'id': 3, + 'name': "Octocat's GPG Key", + 'primary_key_id': 2, + 'key_id': '3262EFF25BA0D270', + 'public_key': 'xsBNBFayYZ...', + 'emails': [{ + 'email': 'octocat@users.noreply.github.com', + 'verified': true + }], + 'subkeys': [{ + 'id': 4, + 'primary_key_id': 3, + 'key_id': '4A595D4C72EE49C7', + 'public_key': 'zsBNBFayYZ...', + 'emails': [], + 'can_sign': false, + 'can_encrypt_comms': true, + 'can_encrypt_storage': true, + 'can_certify': false, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false + }], + 'can_sign': true, + 'can_encrypt_comms': false, + 'can_encrypt_storage': false, + 'can_certify': true, + 'created_at': '2016-03-24T11:31:04-06:00', + 'expires_at': '2016-03-24T11:31:04-07:00', + 'revoked': false, + 'raw_key': 'string' + } +new_key: + description: A new GPG key that was added to GitHub. Only present on O(state=present). + type: dict + returned: changed or success sample: { 'id': 3, 'name': "Octocat's GPG Key", @@ -171,139 +166,127 @@ key: ''' EXAMPLES = ''' -- name: Authorize key with GitHub +- name: Add GitHub GPG key community.general.github_gpg_key: - name: My GPG Key + state: present token: '{{ token }}' + name: My GPG Key armored_public_key: '{{ armored_public_key }}' + +- name: Delete GitHub GPG key + community.general.github_gpg_key: + state: absent + token: '{{ token }}' + gpg_key_id: '{{ gpg_key_id }}' ''' import json -import re - from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.urls import fetch_url - -from ansible_collections.community.general.plugins.module_utils.datetime import now +from ansible.module_utils.urls import open_url -API_BASE = 'https://api.github.com/user/gpg_keys' +GITHUB_GPG_REST_API_URL = 'https://api.github.com/user/gpg_keys' -class GitHubResponse(object): - def __init__(self, response, info): - self.content = response.read() - self.info = info +def ensure_gpg_key_absent(headers, gpg_key_id, check_mode): + changed = False + deleted_key = {} - def json(self): - return json.loads(self.content) - - def links(self): - links = {} - if 'link' in self.info: - link_header = self.info['link'] - matches = re.findall('<([^>]+)>; rel="([^"]+)"', link_header) - for url, rel in matches: - links[rel] = url - return links - - -class GitHubSession(object): - def __init__(self, module, token): - self.module = module - self.token = token - - def request(self, method, url, data=None): - headers = { - 'Authorization': 'token {}'.format(self.token), - 'Content-Type': 'application/json', - 'Accept': 'application/vnd.github.v3+json' - } - response, info = fetch_url( - self.module, url, method=method, data=data, headers=headers) - if not (200 <= info['status'] < 400): - self.module.fail_json('failed to send request {0} to {1}: {2}'.format(method, url, info['msg'])) - return GitHubResponse(response, info) - - -def get_all_keys(session): - url = API_BASE - result = [] - while url: - r = session.request('GET', url) - result.extend(r.json()) - url = r.links().get('next') - return result - - -def create_key(session, name, armored_public_key, check_mode): - if check_mode: - now_t = now() - return {} - else: - return session.request( - 'POST', - API_BASE, - data=json.dumps({'name': name, 'raw_key': armored_public_key})).json() - - -def delete_keys(session, to_delete, check_mode): - if check_mode: - return - - for key in to_delete: - session.request('DELETE', API_BASE + '/' + key['id']) - - -def ensure_key_absent(session, name, check_mode): - to_delete = [key for key in get_all_keys(session) if key['name'] == name] - delete_keys(session, to_delete, check_mode=check_mode) - - return {'changed': bool(to_delete), - 'deleted_keys': to_delete} - - -def ensure_key_present(module, session, name, armored_public_key, force, check_mode): - all_keys = get_all_keys(session) - matching_keys = [k for k in all_keys if k['name'] == name] - deleted_keys = [] - - new_signature = armored_public_key - for key in all_keys: - existing_signature = key['raw_key'] - if new_signature == existing_signature and key['name'] != name: - module.fail_json('Another key with the same content is already registered under the name |{0}|'.format(key['title'])) - - if matching_keys and force and matching_keys[0]['raw_key'] != new_signature: - delete_keys(session, matching_keys, check_mode=check_mode) - deleted_keys, matching_keys = matching_keys, [] - - if not matching_keys: - key = create_key(session, name, armored_public_key, check_mode=check_mode) - else: - key = matching_keys[0] + method = 'GET' if check_mode else 'DELETE' + response = open_url( + url=GITHUB_GPG_REST_API_URL+'/'+gpg_key_id, + method=method, + headers=headers + ) + if response.status == 200: + changed = True + deleted_key = json.loads(response.read()) return { - 'changed': bool(deleted_keys or not matching_keys), - 'deleted_keys': deleted_keys, - 'matching_keys': matching_keys, - 'key': key + 'changed': changed, + 'deleted_key': deleted_key } -def validate_key(module, armored_public_key): +def ensure_gpg_key_present(headers, name, armored_public_key, check_mode): + changed = False + matching_key = {} + new_key = {} + armored_public_key_parts = armored_public_key.splitlines() - if armored_public_key_parts[0] != '-----BEGIN PGP PUBLIC KEY BLOCK-----' or armored_public_key_parts[-1] != '-----END PGP PUBLIC KEY BLOCK-----': - module.fail_json(msg='"armored_public_key" parameter has an invalid format') + if (armored_public_key_parts[0] != '-----BEGIN PGP PUBLIC KEY BLOCK-----') \ + or (armored_public_key_parts[-1] != '-----END PGP PUBLIC KEY BLOCK-----'): + raise Exception('GPG key must have ASCII armor') + response = open_url( + url=GITHUB_GPG_REST_API_URL, + method='GET', + headers=headers + ) + if response.status != 200: + raise Exception( + "Failed to check for matching GPG key: {} {}" + .format(response.status, response.reason) + ) -def run_module(module, params, check_mode): - session = GitHubSession(module, params['token']) + keys = json.loads(response.read()) + for key in keys: + if key['raw_key'] == armored_public_key: + matching_key = key + break + + if not matching_key: + response = open_url( + url=GITHUB_GPG_REST_API_URL, + method='POST', + data={'name': name, 'armored_public_key': armored_public_key}, + headers=headers + ) + if response.status != 201: + raise Exception( + "Failed to create GPG key: {} {}" + .format(response.status, response.reason) + ) + + changed = True + new_key = json.loads(response.json()) + if check_mode and new_key: + response = open_url( + url=GITHUB_GPG_REST_API_URL+'/'+new_key['key_id'], + method='DELETE', + headers=headers + ) + if response.status != 200: + raise Exception( + "Failed to undo changes (check_mode=true): {} {}" + .format(response.status, response.reason) + ) + + return { + 'changed': changed, + 'matching_key': matching_key, + 'new_key': new_key + } + +def run_module(params, check_mode): + headers = { + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer {}'.format(params['token']), + 'X-GitHub-Api-Version': '2022-11-28', + } if params['state'] == 'present': - validate_key(module, params['armored_public_key']) - result = ensure_key_present(module, session, params['name'], params['armored_public_key'], params['force'], check_mode) + result = ensure_gpg_key_present( + headers, + params['name'], + params['armored_public_key'], + check_mode + ) else: - result = ensure_key_absent(session, params['name'], check_mode) + result = ensure_gpg_key_absent( + headers, + params['gpg_key_id'], + check_mode + ) return result @@ -312,13 +295,14 @@ def main(): argument_spec=dict( state=dict(type='str', default='present', choices=['present', 'absent']), token=dict(type='str', required=True, no_log=True), - name=dict(type='str', required=True), + name=dict(type='str', no_log=True), armored_public_key=dict(type='str', no_log=True), - force=dict(type='bool', default=True) + gpg_key_id=dict(type='int', no_log=True) ), supports_check_mode=True, required_if=[ ['state', 'present', ['armored_public_key']], + ['state', 'absent', ['gpg_key_id']], ] ) From b3bad9804fafef067db35bd512d8fde7ccb688ff Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake Date: Sat, 15 Mar 2025 06:51:56 -0700 Subject: [PATCH 15/19] updated tests --- .../targets/github_gpg_key/aliases | 3 +- .../targets/github_gpg_key/tasks/main.yml | 14 ++- .../targets/github_gpg_key/vars/main.yml | 5 +- .../plugins/modules/test_github_gpg_key.py | 114 +++--------------- 4 files changed, 31 insertions(+), 105 deletions(-) diff --git a/tests/integration/targets/github_gpg_key/aliases b/tests/integration/targets/github_gpg_key/aliases index 428e8289dc..f767ca0d6d 100644 --- a/tests/integration/targets/github_gpg_key/aliases +++ b/tests/integration/targets/github_gpg_key/aliases @@ -1,6 +1,7 @@ -# Copyright (c) Ansible Project +# Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later +unsupported azp/posix/1 destructive diff --git a/tests/integration/targets/github_gpg_key/tasks/main.yml b/tests/integration/targets/github_gpg_key/tasks/main.yml index f1faa4d267..025934470c 100644 --- a/tests/integration/targets/github_gpg_key/tasks/main.yml +++ b/tests/integration/targets/github_gpg_key/tasks/main.yml @@ -3,7 +3,7 @@ # and should not be used as examples of how to write Ansible roles # #################################################################### -# Test code for the github_issue module. +# Test code for the github_gpg_key module. # # Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -13,16 +13,18 @@ github_gpg_key: token: "{{ token }}" name: "{{ name }}" - armored_gpg_key: "{{ armored_gpg_key }}" - register: created_gpg_key + armored_public_key: "{{ armored_public_key }}" + register: key1 - name: Delete GPG key github_gpg_key: state: absent token: "{{ token }}" - name: "{{ name }}" - register: deleted_gpg_key + gpg_key_id: "{{ gpg_key_id }}" + register: key2 - assert: that: - - created_gpg_key.id == deleted_gpg_key[0].id + - key1.created_key.name == key2.deleted_key.name + - key1.created_key.key_id == key2.deleted_key.key_id + - key1.created_key.raw_key == key2.deleted_key.raw_key diff --git a/tests/integration/targets/github_gpg_key/vars/main.yml b/tests/integration/targets/github_gpg_key/vars/main.yml index cca21095ef..3ef20fc9ea 100644 --- a/tests/integration/targets/github_gpg_key/vars/main.yml +++ b/tests/integration/targets/github_gpg_key/vars/main.yml @@ -1,8 +1,9 @@ --- -# Copyright (c) Ansible Project +# Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later token: # TODO -name: # TODO +name: My GPG Key armored_public_key: # TODO +gpg_key_id: # TODO diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py index 8c816c9ca7..707ecc40ba 100644 --- a/tests/unit/plugins/modules/test_github_gpg_key.py +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, Austin Lucas Lake, +# Copyright (c) 2024-2025, Austin Lucas Lake # Based on tests/unit/plugins/modules/test_github_repo.py by Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -9,7 +9,6 @@ __metaclass__ = type import json import datetime from httmock import with_httmock, urlmatch, response -from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.tests.unit.compat import unittest from ansible_collections.community.general.plugins.modules import github_gpg_key @@ -21,48 +20,6 @@ def debug_mock(url, request): print(request.original.__dict__) -@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="get") -def list_gpg_keys_mock(url, request): - # https://docs.github.com/en/rest/reference/repos#get-a-repository - headers = {'content-type': 'application/json'} - content = [{ - "id": 3, - "name": "Octocat's GPG Key", - "primary_key_id": 2, - "key_id": "3262EFF25BA0D270", - "public_key": "xsBNBFayYZ...", - "emails": [{ - "email": "octocat@users.noreply.github.com", - "verified": True - }], - "subkeys": [{ - "id": 4, - "primary_key_id": 3, - "key_id": "4A595D4C72EE49C7", - "public_key": "zsBNBFayYZ...", - "emails": [], - "can_sign": False, - "can_encrypt_comms": True, - "can_encrypt_storage": True, - "can_certify": False, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False - }], - "can_sign": True, - "can_encrypt_comms": False, - "can_encrypt_storage": False, - "can_certify": True, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False, - "raw_key": "string" - }] - - content = json.dumps(content).encode("utf-8") - return response(200, content, headers, None, 5, request) - - @urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="post") def create_gpg_key_mock(url, request): gpg_key = json.loads(request.body) @@ -73,7 +30,7 @@ def create_gpg_key_mock(url, request): # https://docs.github.com/en/rest/reference/repos#create-a-repository-for-the-authenticated-user content = { "id": 3, - "name": gpg_key["name"], + "name": gpg_key.get("name"), "primary_key_id": 2, "key_id": "3262EFF25BA0D270", "public_key": "xsBNBFayYZ...", @@ -111,77 +68,42 @@ def create_gpg_key_mock(url, request): @urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") def delete_gpg_key_mock(url, request): # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user - return response(204, None, None, None, 5, request) + return response(200, None, None, None, 5, request) -@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") -def delete_gpg_key_notfound_mock(url, request): - # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user - return response(404, "{\"message\": \"Not Found\"}", "", "Not Found", 5, request) - - -class TestGithubRepo(unittest.TestCase): +class TestGithubGPGKey(unittest.TestCase): def setUp(self): - self.module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choice=['present', 'absent']), - token=dict(type='str', required=True, no_log=True), - name=dict(type='str', required=True), - armored_public_key=dict(type='str', no_log=True), - force=dict(type='bool', default=True) - ), - supports_check_mode=True, - required_if=[ - ['state', 'present', ['armored_public_key']], - ] - ) + self.token = "github_access_token" + self.name = "GPG public key" + self.armored_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----" + self.gpg_key_id = 123456789 - @with_httmock(list_gpg_keys_mock) @with_httmock(create_gpg_key_mock) - def test_create_gpg_key_repo(self): + def test_create_gpg_key(self): result = github_gpg_key.run_module( - module=self.module, params=dict( - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", state="present", - force=True + token=self.token, + name=self.name, + armored_public_key=self.armored_public_key ) ) self.assertEqual(result['changed'], True) - self.assertEqual(result['key']['name'], 'GPG public key') + self.assertEqual(result['key']['name'], self.name) + self.assertEqual(result['key']['raw_key'], self.armored_public_key) - @with_httmock(list_gpg_keys_mock) @with_httmock(delete_gpg_key_mock) - def test_delete_user_repo(self): + def test_delete_gpg_key(self): result = github_gpg_key.run_module( module=self.module, params=dict( - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", state="absent", - force=True + token=self.token, + gpg_key_id=self.gpg_key_id ) ) self.assertEqual(result['changed'], True) - - @with_httmock(list_gpg_keys_mock) - @with_httmock(delete_gpg_key_notfound_mock) - def test_delete_gpg_key_notfound(self): - result = github_gpg_key.run_module( - module=self.module, - params=dict( - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", - state="absent", - force=True - ) - ) - self.assertEqual(result['changed'], False) - + self.assertEqual(result['key']['gpg_key_id'], self.gpg_key_id) if __name__ == "__main__": unittest.main() From 6b20aad2b6eacb32eee80ef3bf16f6d256d09674 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake Date: Sat, 15 Mar 2025 08:00:17 -0700 Subject: [PATCH 16/19] fixed whitespace in accordance with pep8 sanity checks --- plugins/modules/github_gpg_key.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index e2748dfdef..48c0728422 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -55,7 +55,7 @@ deleted_key: description: A GPG key that was deleted from GitHub. Only present on O(state=absent). type: list elements: dict - returned: changed or success + returned: changed or success sample: { 'id': 3, 'name': "Octocat's GPG Key", @@ -194,11 +194,12 @@ def ensure_gpg_key_absent(headers, gpg_key_id, check_mode): method = 'GET' if check_mode else 'DELETE' response = open_url( - url=GITHUB_GPG_REST_API_URL+'/'+gpg_key_id, + url=GITHUB_GPG_REST_API_URL + '/' + gpg_key_id, method=method, headers=headers ) - if response.status == 200: + if (method == 'DELETE' and response.status == 204) \ + or (method == 'GET' and response.status == 200): changed = True deleted_key = json.loads(response.read()) @@ -215,7 +216,7 @@ def ensure_gpg_key_present(headers, name, armored_public_key, check_mode): armored_public_key_parts = armored_public_key.splitlines() if (armored_public_key_parts[0] != '-----BEGIN PGP PUBLIC KEY BLOCK-----') \ - or (armored_public_key_parts[-1] != '-----END PGP PUBLIC KEY BLOCK-----'): + or (armored_public_key_parts[-1] != '-----END PGP PUBLIC KEY BLOCK-----'): raise Exception('GPG key must have ASCII armor') response = open_url( @@ -252,7 +253,7 @@ def ensure_gpg_key_present(headers, name, armored_public_key, check_mode): new_key = json.loads(response.json()) if check_mode and new_key: response = open_url( - url=GITHUB_GPG_REST_API_URL+'/'+new_key['key_id'], + url=GITHUB_GPG_REST_API_URL + '/' + new_key['key_id'], method='DELETE', headers=headers ) @@ -265,9 +266,10 @@ def ensure_gpg_key_present(headers, name, armored_public_key, check_mode): return { 'changed': changed, 'matching_key': matching_key, - 'new_key': new_key + 'new_key': new_key } + def run_module(params, check_mode): headers = { 'Accept': 'application/vnd.github+json', From 458648f8fb1953fa35096d98342e2cabb692033c Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake Date: Sat, 15 Mar 2025 08:03:34 -0700 Subject: [PATCH 17/19] Revert "updated tests" This reverts commit b3bad9804fafef067db35bd512d8fde7ccb688ff. --- .../targets/github_gpg_key/aliases | 3 +- .../targets/github_gpg_key/tasks/main.yml | 14 +- .../targets/github_gpg_key/vars/main.yml | 5 +- .../plugins/modules/test_github_gpg_key.py | 132 ++++++++++++++---- 4 files changed, 114 insertions(+), 40 deletions(-) diff --git a/tests/integration/targets/github_gpg_key/aliases b/tests/integration/targets/github_gpg_key/aliases index f767ca0d6d..428e8289dc 100644 --- a/tests/integration/targets/github_gpg_key/aliases +++ b/tests/integration/targets/github_gpg_key/aliases @@ -1,7 +1,6 @@ -# Copyright (c) 2024-2025, Austin Lucas Lake +# Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later -unsupported azp/posix/1 destructive diff --git a/tests/integration/targets/github_gpg_key/tasks/main.yml b/tests/integration/targets/github_gpg_key/tasks/main.yml index 025934470c..f1faa4d267 100644 --- a/tests/integration/targets/github_gpg_key/tasks/main.yml +++ b/tests/integration/targets/github_gpg_key/tasks/main.yml @@ -3,7 +3,7 @@ # and should not be used as examples of how to write Ansible roles # #################################################################### -# Test code for the github_gpg_key module. +# Test code for the github_issue module. # # Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -13,18 +13,16 @@ github_gpg_key: token: "{{ token }}" name: "{{ name }}" - armored_public_key: "{{ armored_public_key }}" - register: key1 + armored_gpg_key: "{{ armored_gpg_key }}" + register: created_gpg_key - name: Delete GPG key github_gpg_key: state: absent token: "{{ token }}" - gpg_key_id: "{{ gpg_key_id }}" - register: key2 + name: "{{ name }}" + register: deleted_gpg_key - assert: that: - - key1.created_key.name == key2.deleted_key.name - - key1.created_key.key_id == key2.deleted_key.key_id - - key1.created_key.raw_key == key2.deleted_key.raw_key + - created_gpg_key.id == deleted_gpg_key[0].id diff --git a/tests/integration/targets/github_gpg_key/vars/main.yml b/tests/integration/targets/github_gpg_key/vars/main.yml index 3ef20fc9ea..cca21095ef 100644 --- a/tests/integration/targets/github_gpg_key/vars/main.yml +++ b/tests/integration/targets/github_gpg_key/vars/main.yml @@ -1,9 +1,8 @@ --- -# Copyright (c) 2024-2025, Austin Lucas Lake +# Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later token: # TODO -name: My GPG Key +name: # TODO armored_public_key: # TODO -gpg_key_id: # TODO diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py index 707ecc40ba..8c816c9ca7 100644 --- a/tests/unit/plugins/modules/test_github_gpg_key.py +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, Austin Lucas Lake +# Copyright (c) 2024-2025, Austin Lucas Lake, # Based on tests/unit/plugins/modules/test_github_repo.py by Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -9,6 +9,7 @@ __metaclass__ = type import json import datetime from httmock import with_httmock, urlmatch, response +from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.tests.unit.compat import unittest from ansible_collections.community.general.plugins.modules import github_gpg_key @@ -20,6 +21,48 @@ def debug_mock(url, request): print(request.original.__dict__) +@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="get") +def list_gpg_keys_mock(url, request): + # https://docs.github.com/en/rest/reference/repos#get-a-repository + headers = {'content-type': 'application/json'} + content = [{ + "id": 3, + "name": "Octocat's GPG Key", + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [{ + "email": "octocat@users.noreply.github.com", + "verified": True + }], + "subkeys": [{ + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": False, + "can_encrypt_comms": True, + "can_encrypt_storage": True, + "can_certify": False, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False + }], + "can_sign": True, + "can_encrypt_comms": False, + "can_encrypt_storage": False, + "can_certify": True, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": False, + "raw_key": "string" + }] + + content = json.dumps(content).encode("utf-8") + return response(200, content, headers, None, 5, request) + + @urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="post") def create_gpg_key_mock(url, request): gpg_key = json.loads(request.body) @@ -30,7 +73,7 @@ def create_gpg_key_mock(url, request): # https://docs.github.com/en/rest/reference/repos#create-a-repository-for-the-authenticated-user content = { "id": 3, - "name": gpg_key.get("name"), + "name": gpg_key["name"], "primary_key_id": 2, "key_id": "3262EFF25BA0D270", "public_key": "xsBNBFayYZ...", @@ -68,42 +111,77 @@ def create_gpg_key_mock(url, request): @urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") def delete_gpg_key_mock(url, request): # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user - return response(200, None, None, None, 5, request) + return response(204, None, None, None, 5, request) -class TestGithubGPGKey(unittest.TestCase): +@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") +def delete_gpg_key_notfound_mock(url, request): + # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user + return response(404, "{\"message\": \"Not Found\"}", "", "Not Found", 5, request) + + +class TestGithubRepo(unittest.TestCase): def setUp(self): - self.token = "github_access_token" - self.name = "GPG public key" - self.armored_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----" - self.gpg_key_id = 123456789 - - @with_httmock(create_gpg_key_mock) - def test_create_gpg_key(self): - result = github_gpg_key.run_module( - params=dict( - state="present", - token=self.token, - name=self.name, - armored_public_key=self.armored_public_key - ) + self.module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choice=['present', 'absent']), + token=dict(type='str', required=True, no_log=True), + name=dict(type='str', required=True), + armored_public_key=dict(type='str', no_log=True), + force=dict(type='bool', default=True) + ), + supports_check_mode=True, + required_if=[ + ['state', 'present', ['armored_public_key']], + ] ) - self.assertEqual(result['changed'], True) - self.assertEqual(result['key']['name'], self.name) - self.assertEqual(result['key']['raw_key'], self.armored_public_key) - @with_httmock(delete_gpg_key_mock) - def test_delete_gpg_key(self): + @with_httmock(list_gpg_keys_mock) + @with_httmock(create_gpg_key_mock) + def test_create_gpg_key_repo(self): result = github_gpg_key.run_module( module=self.module, params=dict( - state="absent", - token=self.token, - gpg_key_id=self.gpg_key_id + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="present", + force=True ) ) self.assertEqual(result['changed'], True) - self.assertEqual(result['key']['gpg_key_id'], self.gpg_key_id) + self.assertEqual(result['key']['name'], 'GPG public key') + + @with_httmock(list_gpg_keys_mock) + @with_httmock(delete_gpg_key_mock) + def test_delete_user_repo(self): + result = github_gpg_key.run_module( + module=self.module, + params=dict( + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="absent", + force=True + ) + ) + self.assertEqual(result['changed'], True) + + @with_httmock(list_gpg_keys_mock) + @with_httmock(delete_gpg_key_notfound_mock) + def test_delete_gpg_key_notfound(self): + result = github_gpg_key.run_module( + module=self.module, + params=dict( + token="github_access_token", + name="GPG public key", + armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", + state="absent", + force=True + ) + ) + self.assertEqual(result['changed'], False) + if __name__ == "__main__": unittest.main() From 13b4cd78ad528b9929aaa598ff0023d5ba8e2560 Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake Date: Sat, 15 Mar 2025 08:04:58 -0700 Subject: [PATCH 18/19] fixed whitespace in accordance with pep8 sanity checks --- .../plugins/modules/test_github_gpg_key.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py index 8c816c9ca7..e8ec9fc77e 100644 --- a/tests/unit/plugins/modules/test_github_gpg_key.py +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -122,6 +122,7 @@ def delete_gpg_key_notfound_mock(url, request): class TestGithubRepo(unittest.TestCase): def setUp(self): +<<<<<<< Updated upstream self.module = AnsibleModule( argument_spec=dict( state=dict(type='str', default='present', choice=['present', 'absent']), @@ -134,20 +135,44 @@ class TestGithubRepo(unittest.TestCase): required_if=[ ['state', 'present', ['armored_public_key']], ] +======= + self.token = "github_access_token" + self.name = "GPG public key" + self.armored_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----" + self.gpg_key_id = 123456789 + + @with_httmock(create_gpg_key_mock) + def test_create_gpg_key(self): + result = github_gpg_key.run_module( + params=dict( + state="present", + token=self.token, + name=self.name, + armored_public_key=self.armored_public_key + ), + check_mode=False +>>>>>>> Stashed changes ) @with_httmock(list_gpg_keys_mock) @with_httmock(create_gpg_key_mock) def test_create_gpg_key_repo(self): result = github_gpg_key.run_module( - module=self.module, params=dict( +<<<<<<< Updated upstream token="github_access_token", name="GPG public key", armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", state="present", force=True ) +======= + state="absent", + token=self.token, + gpg_key_id=self.gpg_key_id + ), + check_mode=False +>>>>>>> Stashed changes ) self.assertEqual(result['changed'], True) self.assertEqual(result['key']['name'], 'GPG public key') @@ -183,5 +208,6 @@ class TestGithubRepo(unittest.TestCase): self.assertEqual(result['changed'], False) + if __name__ == "__main__": unittest.main() From f62dc3b44fde834e54d8f38e2f66c6670696169b Mon Sep 17 00:00:00 2001 From: Austin Lucas Lake Date: Sat, 15 Mar 2025 12:51:01 -0700 Subject: [PATCH 19/19] updated tests --- plugins/modules/github_gpg_key.py | 2 +- .../targets/github_gpg_key/aliases | 3 +- .../targets/github_gpg_key/tasks/main.yml | 10 +- .../targets/github_gpg_key/vars/main.yml | 5 +- .../plugins/modules/test_github_gpg_key.py | 128 ++---------------- 5 files changed, 25 insertions(+), 123 deletions(-) diff --git a/plugins/modules/github_gpg_key.py b/plugins/modules/github_gpg_key.py index 48c0728422..ffd7855bc2 100644 --- a/plugins/modules/github_gpg_key.py +++ b/plugins/modules/github_gpg_key.py @@ -253,7 +253,7 @@ def ensure_gpg_key_present(headers, name, armored_public_key, check_mode): new_key = json.loads(response.json()) if check_mode and new_key: response = open_url( - url=GITHUB_GPG_REST_API_URL + '/' + new_key['key_id'], + url=GITHUB_GPG_REST_API_URL + '/' + new_key['id'], method='DELETE', headers=headers ) diff --git a/tests/integration/targets/github_gpg_key/aliases b/tests/integration/targets/github_gpg_key/aliases index 428e8289dc..f767ca0d6d 100644 --- a/tests/integration/targets/github_gpg_key/aliases +++ b/tests/integration/targets/github_gpg_key/aliases @@ -1,6 +1,7 @@ -# Copyright (c) Ansible Project +# Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later +unsupported azp/posix/1 destructive diff --git a/tests/integration/targets/github_gpg_key/tasks/main.yml b/tests/integration/targets/github_gpg_key/tasks/main.yml index f1faa4d267..43b83427ba 100644 --- a/tests/integration/targets/github_gpg_key/tasks/main.yml +++ b/tests/integration/targets/github_gpg_key/tasks/main.yml @@ -3,7 +3,7 @@ # and should not be used as examples of how to write Ansible roles # #################################################################### -# Test code for the github_issue module. +# Test code for the github_gpg_key module. # # Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -14,15 +14,17 @@ token: "{{ token }}" name: "{{ name }}" armored_gpg_key: "{{ armored_gpg_key }}" - register: created_gpg_key + register: key1 - name: Delete GPG key github_gpg_key: state: absent token: "{{ token }}" name: "{{ name }}" - register: deleted_gpg_key + register: key2 - assert: that: - - created_gpg_key.id == deleted_gpg_key[0].id + - key1.new_key.name == key2.deleted_key.name + - key1.new_key.key_id == key2.deleted_key.key_id + - key1.new_key.raw_key == key2.deleted_key.raw_key diff --git a/tests/integration/targets/github_gpg_key/vars/main.yml b/tests/integration/targets/github_gpg_key/vars/main.yml index cca21095ef..343b4c1a11 100644 --- a/tests/integration/targets/github_gpg_key/vars/main.yml +++ b/tests/integration/targets/github_gpg_key/vars/main.yml @@ -1,8 +1,9 @@ --- -# Copyright (c) Ansible Project +# Copyright (c) 2024-2025, Austin Lucas Lake # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later token: # TODO -name: # TODO +name: My GPG key armored_public_key: # TODO +gpg_key_id: # TODO diff --git a/tests/unit/plugins/modules/test_github_gpg_key.py b/tests/unit/plugins/modules/test_github_gpg_key.py index e8ec9fc77e..229b21fb35 100644 --- a/tests/unit/plugins/modules/test_github_gpg_key.py +++ b/tests/unit/plugins/modules/test_github_gpg_key.py @@ -9,60 +9,12 @@ __metaclass__ = type import json import datetime from httmock import with_httmock, urlmatch, response -from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.tests.unit.compat import unittest from ansible_collections.community.general.plugins.modules import github_gpg_key GITHUB_MINIMUM_PYTHON_VERSION = (2, 7) -@urlmatch(netloc=r'.*') -def debug_mock(url, request): - print(request.original.__dict__) - - -@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="get") -def list_gpg_keys_mock(url, request): - # https://docs.github.com/en/rest/reference/repos#get-a-repository - headers = {'content-type': 'application/json'} - content = [{ - "id": 3, - "name": "Octocat's GPG Key", - "primary_key_id": 2, - "key_id": "3262EFF25BA0D270", - "public_key": "xsBNBFayYZ...", - "emails": [{ - "email": "octocat@users.noreply.github.com", - "verified": True - }], - "subkeys": [{ - "id": 4, - "primary_key_id": 3, - "key_id": "4A595D4C72EE49C7", - "public_key": "zsBNBFayYZ...", - "emails": [], - "can_sign": False, - "can_encrypt_comms": True, - "can_encrypt_storage": True, - "can_certify": False, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False - }], - "can_sign": True, - "can_encrypt_comms": False, - "can_encrypt_storage": False, - "can_certify": True, - "created_at": "2016-03-24T11:31:04-06:00", - "expires_at": "2016-03-24T11:31:04-07:00", - "revoked": False, - "raw_key": "string" - }] - - content = json.dumps(content).encode("utf-8") - return response(200, content, headers, None, 5, request) - - @urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys', method="post") def create_gpg_key_mock(url, request): gpg_key = json.loads(request.body) @@ -73,7 +25,7 @@ def create_gpg_key_mock(url, request): # https://docs.github.com/en/rest/reference/repos#create-a-repository-for-the-authenticated-user content = { "id": 3, - "name": gpg_key["name"], + "name": gpg_key.get("name"), "primary_key_id": 2, "key_id": "3262EFF25BA0D270", "public_key": "xsBNBFayYZ...", @@ -111,31 +63,15 @@ def create_gpg_key_mock(url, request): @urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") def delete_gpg_key_mock(url, request): # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user - return response(204, None, None, None, 5, request) - - -@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/gpg_keys/.*', method="delete") -def delete_gpg_key_notfound_mock(url, request): - # https://docs.github.com/en/rest/users/gpg-keys#delete-a-gpg-key-for-the-authenticated-user - return response(404, "{\"message\": \"Not Found\"}", "", "Not Found", 5, request) + headers = {'content-type': 'application/json'} + content = { + "id": json.loads(request.read()).get("gpg_key_id") + } + return response(204, content, headers, None, 5, request) class TestGithubRepo(unittest.TestCase): def setUp(self): -<<<<<<< Updated upstream - self.module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choice=['present', 'absent']), - token=dict(type='str', required=True, no_log=True), - name=dict(type='str', required=True), - armored_public_key=dict(type='str', no_log=True), - force=dict(type='bool', default=True) - ), - supports_check_mode=True, - required_if=[ - ['state', 'present', ['armored_public_key']], - ] -======= self.token = "github_access_token" self.name = "GPG public key" self.armored_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----" @@ -151,62 +87,24 @@ class TestGithubRepo(unittest.TestCase): armored_public_key=self.armored_public_key ), check_mode=False ->>>>>>> Stashed changes ) - @with_httmock(list_gpg_keys_mock) - @with_httmock(create_gpg_key_mock) - def test_create_gpg_key_repo(self): + self.assertEqual(result['changed'], False) + self.assertEqual(result['name'], self.name) + self.assertEqual(result['raw_key'], self.armored_public_key) + + @with_httmock(delete_gpg_key_mock) + def test_delete_gpg_key(self): result = github_gpg_key.run_module( params=dict( -<<<<<<< Updated upstream - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", - state="present", - force=True - ) -======= state="absent", token=self.token, gpg_key_id=self.gpg_key_id ), check_mode=False ->>>>>>> Stashed changes - ) - self.assertEqual(result['changed'], True) - self.assertEqual(result['key']['name'], 'GPG public key') - - @with_httmock(list_gpg_keys_mock) - @with_httmock(delete_gpg_key_mock) - def test_delete_user_repo(self): - result = github_gpg_key.run_module( - module=self.module, - params=dict( - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", - state="absent", - force=True - ) - ) - self.assertEqual(result['changed'], True) - - @with_httmock(list_gpg_keys_mock) - @with_httmock(delete_gpg_key_notfound_mock) - def test_delete_gpg_key_notfound(self): - result = github_gpg_key.run_module( - module=self.module, - params=dict( - token="github_access_token", - name="GPG public key", - armored_public_key="-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\n\nMy ASCII-armored GPG public key\r\n-----END PGP PUBLIC KEY BLOCK-----", - state="absent", - force=True - ) ) self.assertEqual(result['changed'], False) - + self.assertEqual(result['id'], self.gpg_key_id) if __name__ == "__main__":