mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-06-11 02:25:36 +00:00
[PR #12084/8468fea3 backport][stable-12] composer: config file hash to evaluate whether a change occurred (#12131)
composer: config file hash to evaluate whether a change occurred (#12084)
* composer: Use config file hash to evaluate whether a change occurred (instead of unreliable output). Ensure `--working-dir` option consistently comes first (sudo-friendly)
* Update plugins/modules/composer.py
* whitespace fixes + changelog fragment
* Update changelogs/fragments/composer-working-dir-and-config-sha256.yaml
* Update changelogs/fragments/composer-working-dir-and-config-sha256.yaml
* Update plugins/modules/composer.py
* fragment
* ruff format plugins/modules/composer.py
---------
(cherry picked from commit 8468fea3b0)
Co-authored-by: Raphaël Droz <raphael@droz.eu>
Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
fee35e6ec2
commit
410383e95f
4 changed files with 143 additions and 8 deletions
|
|
@ -0,0 +1,12 @@
|
|||
# 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
|
||||
|
||||
bugfixes:
|
||||
- composer - the ``--working-dir`` option is now always placed first on the command line, before the
|
||||
subcommand, to ensure consistent behavior across all composer commands
|
||||
(https://github.com/ansible-collections/community.general/issues/5204, https://github.com/ansible-collections/community.general/pull/12084).
|
||||
- composer - use file checksum to determine ``changed`` status when ``command=config``,
|
||||
instead of relying on the command return output. The module now compares SHA256 checksums of
|
||||
relevant configuration files (``composer.json``, ``auth.json``, or their global equivalents)
|
||||
before and after running the command
|
||||
(https://github.com/ansible-collections/community.general/pull/12084).
|
||||
|
|
@ -186,9 +186,10 @@ def composer_command(module, command, arguments=None, options=None):
|
|||
|
||||
if global_command:
|
||||
global_arg = ["global"]
|
||||
working_dir_option = []
|
||||
else:
|
||||
global_arg = []
|
||||
options.extend(["--working-dir", module.params["working_dir"]])
|
||||
working_dir_option = ["--working-dir", module.params["working_dir"]]
|
||||
|
||||
if module.params["executable"] is None:
|
||||
php_path = module.get_bin_path("php", True, ["/usr/local/bin"])
|
||||
|
|
@ -200,10 +201,50 @@ def composer_command(module, command, arguments=None, options=None):
|
|||
else:
|
||||
composer_path = module.params["composer_executable"]
|
||||
|
||||
cmd = [php_path, composer_path] + global_arg + command + options + arguments
|
||||
cmd = [php_path, composer_path] + working_dir_option + global_arg + command + options + arguments
|
||||
return module.run_command(cmd)
|
||||
|
||||
|
||||
def get_composer_home(module):
|
||||
"""Get the composer home directory by running 'composer config --global home'."""
|
||||
if module.params["executable"] is None:
|
||||
php_path = module.get_bin_path("php", True, ["/usr/local/bin"])
|
||||
else:
|
||||
php_path = module.params["executable"]
|
||||
|
||||
if module.params["composer_executable"] is None:
|
||||
composer_path = module.get_bin_path("composer", True, ["/usr/local/bin"])
|
||||
else:
|
||||
composer_path = module.params["composer_executable"]
|
||||
|
||||
cmd = [php_path, composer_path, "config", "--global", "--no-interaction", "home"]
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
return out.strip()
|
||||
return None
|
||||
|
||||
|
||||
def get_config_files(module):
|
||||
"""Get list of config files that might be changed by 'composer config'."""
|
||||
files = ["composer.json", "auth.json"]
|
||||
global_command = module.params["global_command"]
|
||||
working_dir = module.params["working_dir"]
|
||||
|
||||
if global_command:
|
||||
composer_home = get_composer_home(module)
|
||||
if composer_home:
|
||||
files = [os.path.join(composer_home, f) for f in files]
|
||||
elif working_dir:
|
||||
files = [os.path.join(working_dir, f) for f in files]
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def hash_config_files(module, files):
|
||||
"""Return a dict mapping file path to its sha256 hash (or None if the file does not exist)."""
|
||||
return {f: (module.sha256(f) if os.path.isfile(f) else None) for f in files}
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
|
|
@ -280,6 +321,12 @@ def main():
|
|||
else:
|
||||
module.exit_json(skipped=True, msg=f"command '{command}' does not support check mode, skipping")
|
||||
|
||||
# For 'config' command in non-check mode, use sha256 hashing to detect changes
|
||||
use_hash_detection = command == "config" and not module.check_mode
|
||||
if use_hash_detection:
|
||||
config_files = get_config_files(module)
|
||||
hashes_before = hash_config_files(module, config_files)
|
||||
|
||||
rc, out, err = composer_command(module, [command], arguments, options)
|
||||
|
||||
if rc != 0:
|
||||
|
|
@ -288,7 +335,12 @@ def main():
|
|||
else:
|
||||
# Composer version > 1.0.0-alpha9 now use stderr for standard notification messages
|
||||
output = parse_out(out + err)
|
||||
module.exit_json(changed=has_changed(output), msg=output, stdout=out + err)
|
||||
if use_hash_detection:
|
||||
hashes_after = hash_config_files(module, config_files)
|
||||
changed = hashes_before != hashes_after
|
||||
else:
|
||||
changed = has_changed(output)
|
||||
module.exit_json(changed=changed, msg=output, stdout=out + err)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -22,4 +22,31 @@ class OsPathExistsMock(TestCaseMock):
|
|||
pass
|
||||
|
||||
|
||||
UTHelper.from_module(composer, __name__, mocks=[RunCommandMock, OsPathExistsMock])
|
||||
class OsPathIsfileMock(TestCaseMock):
|
||||
name = "os_path_isfile"
|
||||
|
||||
def setup(self, mocker):
|
||||
mocker.patch("os.path.isfile", return_value=self.mock_specs.get("return_value", False))
|
||||
|
||||
def check(self, test_case, results):
|
||||
pass
|
||||
|
||||
|
||||
class Sha256Mock(TestCaseMock):
|
||||
name = "sha256"
|
||||
|
||||
def setup(self, mocker):
|
||||
values = list(self.mock_specs.get("return_values", []))
|
||||
|
||||
def _sha256_side_effect(path):
|
||||
if values:
|
||||
return values.pop(0)
|
||||
return "default_hash"
|
||||
|
||||
mocker.patch("ansible.module_utils.basic.AnsibleModule.sha256", side_effect=_sha256_side_effect)
|
||||
|
||||
def check(self, test_case, results):
|
||||
pass
|
||||
|
||||
|
||||
UTHelper.from_module(composer, __name__, mocks=[RunCommandMock, OsPathExistsMock, OsPathIsfileMock, Sha256Mock])
|
||||
|
|
|
|||
|
|
@ -5,22 +5,28 @@
|
|||
---
|
||||
anchors:
|
||||
help_install: &help_install
|
||||
command: ["/testbin/php", "/testbin/composer", "help", "install", "--working-dir", "/var/www/foo", "--no-interaction", "--format=json"]
|
||||
command: ["/testbin/php", "/testbin/composer", "--working-dir", "/var/www/foo", "help", "install", "--no-interaction", "--format=json"]
|
||||
rc: 0
|
||||
out: |
|
||||
{"definition": {"options": ["a", "b", "c"]}}
|
||||
err: ''
|
||||
help_create_project: &help_create_project
|
||||
command: ["/testbin/php", "/testbin/composer", "help", "create-project", "--working-dir", "/var/www/foo", "--no-interaction", "--format=json"]
|
||||
command: ["/testbin/php", "/testbin/composer", "--working-dir", "/var/www/foo", "help", "create-project", "--no-interaction", "--format=json"]
|
||||
rc: 0
|
||||
out: |
|
||||
{"definition": {"options": ["a", "b", "c"]}}
|
||||
err: ''
|
||||
run_create_project: &run_create_project
|
||||
command: ["/testbin/php", "/testbin/composer", "create-project", "--working-dir", "/var/www/foo", "vendor/package"]
|
||||
command: ["/testbin/php", "/testbin/composer", "--working-dir", "/var/www/foo", "create-project", "vendor/package"]
|
||||
rc: 0
|
||||
out: ''
|
||||
err: ''
|
||||
help_config: &help_config
|
||||
command: ["/testbin/php", "/testbin/composer", "--working-dir", "/var/www/foo", "help", "config", "--no-interaction", "--format=json"]
|
||||
rc: 0
|
||||
out: |
|
||||
{"definition": {"options": ["a", "b", "c"]}}
|
||||
err: ''
|
||||
|
||||
test_cases:
|
||||
- id: composer
|
||||
|
|
@ -32,7 +38,7 @@ test_cases:
|
|||
mocks:
|
||||
run_command:
|
||||
- *help_install
|
||||
- command: ["/testbin/php", "/testbin/composer", "install", "--working-dir", "/var/www/foo"]
|
||||
- command: ["/testbin/php", "/testbin/composer", "--working-dir", "/var/www/foo", "install"]
|
||||
rc: 0
|
||||
out: ''
|
||||
err: ''
|
||||
|
|
@ -78,3 +84,41 @@ test_cases:
|
|||
run_command:
|
||||
- *help_create_project
|
||||
- *run_create_project
|
||||
|
||||
- id: config_no_change
|
||||
input:
|
||||
command: config
|
||||
arguments: "setting-key setting-value"
|
||||
working_dir: "/var/www/foo"
|
||||
output:
|
||||
changed: false
|
||||
mocks:
|
||||
os_path_isfile:
|
||||
return_value: true
|
||||
sha256:
|
||||
return_values: ["hash1", "hash2", "hash1", "hash2"]
|
||||
run_command:
|
||||
- *help_config
|
||||
- command: ["/testbin/php", "/testbin/composer", "--working-dir", "/var/www/foo", "config", "setting-key", "setting-value"]
|
||||
rc: 0
|
||||
out: ''
|
||||
err: ''
|
||||
|
||||
- id: config_changed
|
||||
input:
|
||||
command: config
|
||||
arguments: "setting-key setting-value"
|
||||
working_dir: "/var/www/foo"
|
||||
output:
|
||||
changed: true
|
||||
mocks:
|
||||
os_path_isfile:
|
||||
return_value: true
|
||||
sha256:
|
||||
return_values: ["hash1", "hash2", "hash1_changed", "hash2"]
|
||||
run_command:
|
||||
- *help_config
|
||||
- command: ["/testbin/php", "/testbin/composer", "--working-dir", "/var/www/foo", "config", "setting-key", "setting-value"]
|
||||
rc: 0
|
||||
out: ''
|
||||
err: ''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue