From 1b9d437be8d085b714f2c38208b30401e7dcf2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Grill?= Date: Wed, 29 Nov 2023 08:37:50 +0100 Subject: [PATCH] New module git config info (#7587) Add new module git_config_info --- .github/BOTMETA.yml | 2 + plugins/modules/git_config.py | 2 +- plugins/modules/git_config_info.py | 187 ++++++++++++++++++ tests/.gitignore | 1 + .../targets/git_config_info/aliases | 7 + .../targets/git_config_info/files/gitconfig | 11 ++ .../targets/git_config_info/meta/main.yml | 7 + .../git_config_info/tasks/error_handling.yml | 26 +++ .../git_config_info/tasks/get_all_values.yml | 19 ++ .../git_config_info/tasks/get_multi_value.yml | 20 ++ .../tasks/get_simple_value.yml | 38 ++++ .../targets/git_config_info/tasks/main.yml | 33 ++++ .../targets/git_config_info/tasks/setup.yml | 15 ++ .../git_config_info/tasks/setup_file.yml | 16 ++ .../git_config_info/tasks/setup_global.yml | 16 ++ .../targets/git_config_info/vars/main.yml | 7 + 16 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 plugins/modules/git_config_info.py create mode 100644 tests/integration/targets/git_config_info/aliases create mode 100644 tests/integration/targets/git_config_info/files/gitconfig create mode 100644 tests/integration/targets/git_config_info/meta/main.yml create mode 100644 tests/integration/targets/git_config_info/tasks/error_handling.yml create mode 100644 tests/integration/targets/git_config_info/tasks/get_all_values.yml create mode 100644 tests/integration/targets/git_config_info/tasks/get_multi_value.yml create mode 100644 tests/integration/targets/git_config_info/tasks/get_simple_value.yml create mode 100644 tests/integration/targets/git_config_info/tasks/main.yml create mode 100644 tests/integration/targets/git_config_info/tasks/setup.yml create mode 100644 tests/integration/targets/git_config_info/tasks/setup_file.yml create mode 100644 tests/integration/targets/git_config_info/tasks/setup_global.yml create mode 100644 tests/integration/targets/git_config_info/vars/main.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index c117e290da..dc7c990ea0 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -526,6 +526,8 @@ files: maintainers: russoz $modules/git_config.py: maintainers: djmattyg007 mgedmin + $modules/git_config_info.py: + maintainers: guenhter $modules/github_: maintainers: stpierre $modules/github_deploy_key.py: diff --git a/plugins/modules/git_config.py b/plugins/modules/git_config.py index 2c6a679f3f..46ea7d4f59 100644 --- a/plugins/modules/git_config.py +++ b/plugins/modules/git_config.py @@ -20,7 +20,7 @@ author: requirements: ['git'] short_description: Read and write git configuration description: - - The M(community.general.git_config) module changes git configuration by invoking 'git config'. + - The M(community.general.git_config) module changes git configuration by invoking C(git config). This is needed if you do not want to use M(ansible.builtin.template) for the entire git config file (for example because you need to change just C(user.email) in /etc/.git/config). Solutions involving M(ansible.builtin.command) are cumbersome or diff --git a/plugins/modules/git_config_info.py b/plugins/modules/git_config_info.py new file mode 100644 index 0000000000..147201fff3 --- /dev/null +++ b/plugins/modules/git_config_info.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Guenther Grill +# +# 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: git_config_info +author: + - Guenther Grill (@guenhter) +version_added: 8.1.0 +requirements: ['git'] +short_description: Read git configuration +description: + - The M(community.general.git_config_info) module reads the git configuration + by invoking C(git config). +extends_documentation_fragment: + - community.general.attributes + - community.general.attributes.info_module +options: + name: + description: + - The name of the setting to read. + - If not provided, all settings will be returned as RV(config_values). + type: str + path: + description: + - Path to a git repository or file for reading values from a specific repo. + - If O(scope) is V(local), this must point to a repository to read from. + - If O(scope) is V(file), this must point to specific git config file to read from. + - Otherwise O(path) is ignored if set. + type: path + scope: + description: + - Specify which scope to read values from. + - If set to V(global), the global git config is used. O(path) is ignored. + - If set to V(system), the system git config is used. O(path) is ignored. + - If set to V(local), O(path) must be set to the repo to read from. + - If set to V(file), O(path) must be set to the config file to read from. + choices: [ "global", "system", "local", "file" ] + default: "system" + type: str +''' + +EXAMPLES = ''' +- name: Read a system wide config + community.general.git_config_info: + name: core.editor + register: result + +- name: Show value of core.editor + ansible.builtin.debug: + msg: "{{ result.config_value | default('(not set)', true) }}" + +- name: Read a global config from ~/.gitconfig + community.general.git_config_info: + name: alias.remotev + scope: global + +- name: Read a project specific config + community.general.git_config_info: + name: color.ui + scope: local + path: /etc + +- name: Read all global values + community.general.git_config_info: + scope: global + +- name: Read all system wide values + community.general.git_config_info: + +- name: Read all values of a specific file + community.general.git_config_info: + scope: file + path: /etc/gitconfig +''' + +RETURN = ''' +--- +config_value: + description: > + When O(name) is set, a string containing the value of the setting in name. If O(name) is not set, empty. + If a config key such as V(push.pushoption) has more then one entry, just the first one is returned here. + returned: success if O(name) is set + type: str + sample: "vim" + +config_values: + description: + - This is a dictionary mapping a git configuration setting to a list of its values. + - When O(name) is not set, all configuration settings are returned here. + - When O(name) is set, only the setting specified in O(name) is returned here. + If that setting is not set, the key will still be present, and its value will be an empty list. + returned: success + type: dict + sample: + core.editor: ["vim"] + color.ui: ["auto"] + push.pushoption: ["merge_request.create", "merge_request.draft"] + alias.remotev: ["remote -v"] +''' + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type="str"), + path=dict(type="path"), + scope=dict(required=False, type="str", default="system", choices=["global", "system", "local", "file"]), + ), + required_if=[ + ("scope", "local", ["path"]), + ("scope", "file", ["path"]), + ], + required_one_of=[], + supports_check_mode=True, + ) + + # We check error message for a pattern, so we need to make sure the messages appear in the form we're expecting. + # Set the locale to C to ensure consistent messages. + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + + name = module.params["name"] + path = module.params["path"] + scope = module.params["scope"] + + run_cwd = path if scope == "local" else "/" + args = build_args(module, name, path, scope) + + (rc, out, err) = module.run_command(args, cwd=run_cwd, expand_user_and_vars=False) + + if rc == 128 and "unable to read config file" in err: + # This just means nothing has been set at the given scope + pass + elif rc >= 2: + # If the return code is 1, it just means the option hasn't been set yet, which is fine. + module.fail_json(rc=rc, msg=err, cmd=" ".join(args)) + + output_lines = out.strip("\0").split("\0") if out else [] + + if name: + first_value = output_lines[0] if output_lines else "" + config_values = {name: output_lines} + module.exit_json(changed=False, msg="", config_value=first_value, config_values=config_values) + else: + config_values = text_to_dict(output_lines) + module.exit_json(changed=False, msg="", config_value="", config_values=config_values) + + +def build_args(module, name, path, scope): + git_path = module.get_bin_path("git", True) + args = [git_path, "config", "--includes", "--null", "--" + scope] + + if scope == "file": + args.append(path) + + if name: + args.extend(["--get-all", name]) + else: + args.append("--list") + + return args + + +def text_to_dict(text_lines): + config_values = {} + for value in text_lines: + k, v = value.split("\n", 1) + if k in config_values: + config_values[k].append(v) + else: + config_values[k] = [v] + return config_values + + +if __name__ == "__main__": + main() diff --git a/tests/.gitignore b/tests/.gitignore index 6edf5dc10c..0d36555dd7 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -3,3 +3,4 @@ # SPDX-License-Identifier: GPL-3.0-or-later output/ +integration/inventory diff --git a/tests/integration/targets/git_config_info/aliases b/tests/integration/targets/git_config_info/aliases new file mode 100644 index 0000000000..7b8c653de6 --- /dev/null +++ b/tests/integration/targets/git_config_info/aliases @@ -0,0 +1,7 @@ +# 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/3 +skip/aix +destructive diff --git a/tests/integration/targets/git_config_info/files/gitconfig b/tests/integration/targets/git_config_info/files/gitconfig new file mode 100644 index 0000000000..d0590b3f80 --- /dev/null +++ b/tests/integration/targets/git_config_info/files/gitconfig @@ -0,0 +1,11 @@ +# 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 + +[credential "https://some.com"] + username = yolo +[user] + name = foobar +[push] + pushoption = merge_request.create + pushoption = merge_request.draft diff --git a/tests/integration/targets/git_config_info/meta/main.yml b/tests/integration/targets/git_config_info/meta/main.yml new file mode 100644 index 0000000000..982de6eb03 --- /dev/null +++ b/tests/integration/targets/git_config_info/meta/main.yml @@ -0,0 +1,7 @@ +--- +# 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 + +dependencies: + - setup_remote_tmp_dir diff --git a/tests/integration/targets/git_config_info/tasks/error_handling.yml b/tests/integration/targets/git_config_info/tasks/error_handling.yml new file mode 100644 index 0000000000..1b84fee509 --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/error_handling.yml @@ -0,0 +1,26 @@ +--- +# 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 + +- import_tasks: "setup_file.yml" + +- name: getting all system configs + git_config_info: + scope: system + register: get_result1 + +- name: getting all system configs for a key + git_config_info: + name: user.name + scope: system + register: get_result2 + +- name: assert value is correct + assert: + that: + - get_result1.config_value == "" + - 'get_result1.config_values == {}' + - get_result2.config_value == "" + - 'get_result2.config_values == {"user.name": []}' +... diff --git a/tests/integration/targets/git_config_info/tasks/get_all_values.yml b/tests/integration/targets/git_config_info/tasks/get_all_values.yml new file mode 100644 index 0000000000..301051a42c --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/get_all_values.yml @@ -0,0 +1,19 @@ +--- +# 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 + +- include_tasks: "{{ item.import_file }}" + +- name: getting all values (as list) for a file config + git_config_info: + scope: "{{ item.git_scope }}" + path: "{{ item.git_file | default(omit) }}" + register: get_result + +- name: assert value is correct + assert: + that: + - get_result.config_value == "" + - 'get_result.config_values == {"credential.https://some.com.username": ["yolo"], "user.name": ["foobar"], "push.pushoption": ["merge_request.create", "merge_request.draft"]}' +... diff --git a/tests/integration/targets/git_config_info/tasks/get_multi_value.yml b/tests/integration/targets/git_config_info/tasks/get_multi_value.yml new file mode 100644 index 0000000000..14fa2800cb --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/get_multi_value.yml @@ -0,0 +1,20 @@ +--- +# 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 + +- include_tasks: "{{ item.import_file }}" + +- name: getting only a single value (as string) from an option with multiple values in the git config file + git_config_info: + name: push.pushoption + scope: "{{ item.git_scope }}" + path: "{{ item.git_file | default(omit) }}" + register: get_result + +- name: assert value is correct + assert: + that: + - get_result.config_value == "merge_request.create" + - 'get_result.config_values == {"push.pushoption": ["merge_request.create", "merge_request.draft"]}' +... diff --git a/tests/integration/targets/git_config_info/tasks/get_simple_value.yml b/tests/integration/targets/git_config_info/tasks/get_simple_value.yml new file mode 100644 index 0000000000..83cd19a0b9 --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/get_simple_value.yml @@ -0,0 +1,38 @@ +--- +# 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 + +- include_tasks: "{{ item.import_file }}" + +- name: getting simple file value1 + git_config_info: + name: user.name + scope: "{{ item.git_scope }}" + path: "{{ item.git_file | default(omit) }}" + register: get_result1 + +- name: getting simple file value2 + git_config_info: + name: "credential.https://some.com.username" + scope: "{{ item.git_scope }}" + path: "{{ item.git_file | default(omit) }}" + register: get_result2 + +- name: getting not existing value + git_config_info: + name: "user.email" + scope: "{{ item.git_scope }}" + path: "{{ item.git_file | default(omit) }}" + register: get_result3 + +- name: assert value is correct + assert: + that: + - get_result1.config_value == "foobar" + - 'get_result1.config_values == {"user.name": ["foobar"]}' + - get_result2.config_value == "yolo" + - 'get_result2.config_values == {"credential.https://some.com.username": ["yolo"]}' + - get_result3.config_value == "" + - 'get_result3.config_values == {"user.email": []}' +... diff --git a/tests/integration/targets/git_config_info/tasks/main.yml b/tests/integration/targets/git_config_info/tasks/main.yml new file mode 100644 index 0000000000..993238805e --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/main.yml @@ -0,0 +1,33 @@ +--- +#################################################################### +# 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 git_config_info module +# 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 + +- name: setup + import_tasks: setup.yml + +- block: + - include_tasks: get_simple_value.yml + loop: + - { import_file: setup_global.yml, git_scope: 'global' } + - { import_file: setup_file.yml, git_scope: 'file', git_file: "{{ remote_tmp_dir }}/gitconfig_file" } + + - include_tasks: get_multi_value.yml + loop: + - { import_file: setup_global.yml, git_scope: 'global' } + - { import_file: setup_file.yml, git_scope: 'file', git_file: "{{ remote_tmp_dir }}/gitconfig_file" } + + - include_tasks: get_all_values.yml + loop: + - { import_file: setup_global.yml, git_scope: 'global' } + - { import_file: setup_file.yml, git_scope: 'file', git_file: "{{ remote_tmp_dir }}/gitconfig_file" } + + - include_tasks: error_handling.yml + when: git_installed is succeeded and git_version.stdout is version(git_version_supporting_includes, ">=") +... diff --git a/tests/integration/targets/git_config_info/tasks/setup.yml b/tests/integration/targets/git_config_info/tasks/setup.yml new file mode 100644 index 0000000000..6e5516da57 --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/setup.yml @@ -0,0 +1,15 @@ +--- +# 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 + +- name: verify that git is installed so this test can continue + command: which git + register: git_installed + ignore_errors: true + +- name: get git version, only newer than {{git_version_supporting_includes}} has includes option + shell: "git --version | grep 'git version' | sed 's/git version //'" + register: git_version + ignore_errors: true +... diff --git a/tests/integration/targets/git_config_info/tasks/setup_file.yml b/tests/integration/targets/git_config_info/tasks/setup_file.yml new file mode 100644 index 0000000000..854b10997f --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/setup_file.yml @@ -0,0 +1,16 @@ +--- +# 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 + +# ------ +# set up : set gitconfig with value +- name: delete global config + file: + path: ~/.gitconfig + state: absent + +- name: set up file config + copy: + src: gitconfig + dest: "{{ remote_tmp_dir }}/gitconfig_file" diff --git a/tests/integration/targets/git_config_info/tasks/setup_global.yml b/tests/integration/targets/git_config_info/tasks/setup_global.yml new file mode 100644 index 0000000000..a9e045a572 --- /dev/null +++ b/tests/integration/targets/git_config_info/tasks/setup_global.yml @@ -0,0 +1,16 @@ +--- +# 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 + +# ------ +# set up : set gitconfig with value +- name: delete file config + file: + path: "{{ remote_tmp_dir }}/gitconfig_file" + state: absent + +- name: setup global config + copy: + src: gitconfig + dest: ~/.gitconfig diff --git a/tests/integration/targets/git_config_info/vars/main.yml b/tests/integration/targets/git_config_info/vars/main.yml new file mode 100644 index 0000000000..55c3d1738f --- /dev/null +++ b/tests/integration/targets/git_config_info/vars/main.yml @@ -0,0 +1,7 @@ +--- +# 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 + +git_version_supporting_includes: 1.7.10 +...