Compare commits

15 Commits
6.2.0 ... 6.3.0

Author SHA1 Message Date
Bianca Henderson
80c33c5573 Prep 6.3.0 release (#1084)
SUMMARY

Prep kubernetes.core 6.3.0 release

COMPONENT NAME

Multiple

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
Reviewed-by: Bikouo Aubin
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Alina Buzachis
Reviewed-by: Yuriy Novostavskiy <yuriy@novostavskiy.kyiv.ua>
Reviewed-by: Rahmanim Benny <brahmani@redhat.com>
2026-02-03 17:03:41 +00:00
patchback[bot]
73dc94be68 Add idempotency to helm_pull module (#1055) (#1080)
This is a backport of PR #1055 as merged into main (34beacf).
SUMMARY
This PR implements idempotency for the helm_pull module, addressing issue #889.

New force parameter with defaults to False.
implemented chart_exists() function
checks chart existence before downloading, returns changed=False when chart exists

ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
helm_pull
ADDITIONAL INFORMATION
Force parameter added for backward compatibility and edge cases.
Implemented with the partial support of GitHub Copilot with Claude Sonnet 4.5 model

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2026-01-29 18:22:52 +00:00
patchback[bot]
1bc65230bb Fix incorrect assertion in helm_pull integration test (#1077) (#1079)
This is a backport of PR #1077 as merged into main (23b6cec).
SUMMARY

The error message emitted for incorrect helm version has changed since the merge of #1039. This PR updates the related assertion in the helm_pull integration test

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2026-01-29 17:44:05 +00:00
patchback[bot]
91427a5b6a chore(CI): prevent patchback bot from labeling new PRs (#1062) (#1064)
This is a backport of PR #1062 as merged into main (987c029).
SUMMARY
Currently, the patchback bot creates PRs with cherry-picks of PRs that are labeled with stable-* label, and this PR (as any new PR) gets labeled as needs_triage. This is unnecessary as it intentional PR created by the CI job.
This pull request introduces a small update to ensure that the workflow only runs if the pull request is not created by the patchback[bot] user to avoid unnecessary labels.
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
.github/workflows/label-new-prs.yaml
ADDITIONAL INFORMATION
No changelog is required 

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2026-01-29 16:00:40 +00:00
patchback[bot]
1d1a23b1ab Replace passing `warnings to exit_json with AnsibleModule.warn` for the few modules (#1033) (#1073)
This is a backport of PR #1033 as merged into main (3e32c12).
SUMMARY
Using exit_json or fail_json for warnings is deprecated in ansible-core>=2.19.0 and will be removed in ansible-core>=2.23.0
Tested with ansible-core 2.19.3 as the latest released version at the time of the start of this PR and with 2.16.0 as the lowest version supported by kubernetes.core 6.x
Resolves: #1031
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
k8s_drain
k8s_rollback
k8s_scale
ADDITIONAL INFORMATION
The initial version of this PR covers only the module k8s_drain, with the following commits extended to k8s_rollback
k8s_scale

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
Reviewed-by: Mike Graves <mgraves@redhat.com>
2026-01-26 21:14:44 +00:00
patchback[bot]
6edc84678d Limit compatibility to Helm =>v3.0.0,<4.0.0 (#1039) (#1071)
This is a backport of PR #1039 as merged into main (13791ec).
SUMMARY
Helm v4 is a major version with backward-incompatible changes, including to the flags and output of the Helm CLI and to the SDK. This version is currently not supported in the kubernetes.core. This PR is related to #1038 and is a short-term solution to mark compatibility explicitly
ISSUE TYPE

Bugfix Pull Request
Docs Pull Request

COMPONENT NAME

helm
helm_template
helm_info
helm_repository
helm_pull
helm_registry_auth
helm_plugin
helm_plugin_info

ADDITIONAL INFORMATION
Added `validate_helm_version()`` method to AnsibleHelmModule that enforces version constraint >=3.0.0,<4.0.0.
Fails fast with clear error message: "Helm version must be >=3.0.0,<4.0.0, current version is {version}"
Some modules (i.e. helm_registry_auth) technically is compatible with Helm v4, but validation was added to all helm modules.
Partially coauthored by GitHub Copilot with Claude Sonnet 4 model.
Addresses issue #1038

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
Reviewed-by: Mike Graves <mgraves@redhat.com>
2026-01-26 19:47:46 +00:00
patchback[bot]
66bd8620e4 Replace deprecated ansible.module_utils._text imports (#1053) (#1070)
This is a backport of PR #1053 as merged into main (452fb3d).
SUMMARY
Importing from ansible.module_utils._text is deprecated in ansible-core 2.20 and removed in 2.24. All imports of to_bytes, to_native, and to_text now use ansible.module_utils.common.text.converters.
Before:
from ansible.module_utils._text import to_bytes, to_native, to_text

After:
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text

ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
plugins/module_utils/common.py
plugins/action/k8s_info.py
plugins/connection/kubectl.py
plugins/module_utils/{copy.py, k8s/runner.py}
plugins/modules/{k8s_cp.py, k8s_drain.py, k8s_exec.py, k8s_json_patch.py, k8s_scale.py, k8s_taint.py}
ADDITIONAL INFORMATION
It's not an actual Bugfix, more a lifecycle management to ensure compatibility with future Ansible versions.
Tested with ansible-core 2.20 to ensure no deprecation warnings are raised and with ansible-core 2.16 to ensure backward compatibility.
Patrially coauthored-by: GitHub Copilot with Claude Code 4.5 model.
Addresses issue #1052.

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2026-01-26 15:22:45 +00:00
patchback[bot]
94b43dc582 Fix K8S_AUTH_VERIFY_SSL environment value handling in kubectl connection plugin (#1049) (#1069)
This is a backport of PR #1049 as merged into main (12abc9b).
SUMMARY
Fixed a bug where setting K8S_AUTH_VERIFY_SSL=true (or any string value) caused the value to be treated as a separate kubectl command argument instead of being properly converted to a boolean.
The option key name is validate_certs, which does NOT end with "verify_ssl", so the original condition key.endswith("verify_ssl") at line 327 failed. This caused the code to fall through to the else block which added the value as separate
arguments: ["--insecure-skip-tls-verify", "true"], making "true" appear as a kubectl command.
Fixes #1021
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
kubernetes.core.kubectl
ADDITIONAL INFORMATION
Changes Made

Changed condition from key.endswith("verify_ssl") to key == "validate_certs"
Added import of boolean function from ansible.module_utils.parsing.convert_bool
Added proper boolean conversion using boolean(self.get_option(key), strict=False)

Partially used LLM (GitHub Copilot with Claude Sonnet 4).
Before Fix
K8S_AUTH_VERIFY_SSL=true
Command: ['/usr/bin/kubectl', '--insecure-skip-tls-verify', 'true', 'exec', ...]

                                                            ^^^^^ treated as kubectl command (BUG!)

After Fix
K8S_AUTH_VERIFY_SSL=true
Command: ['/usr/bin/kubectl', '--insecure-skip-tls-verify=false', 'exec', ...]
                                                           ^^^^^ properly converted (FIXED!)

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2026-01-23 20:24:44 +00:00
patchback[bot]
3c4f9cee60 chore(doc): update typos (#1059) (#1061)
This is a backport of PR #1059 as merged into main (6c00f7c).
SUMMARY
This trivial documentation-only pull request correcting a few errors in README.md.

Documentation corrections:

Fixed a broken Markdown link for the GitHub repository in the support request section.
Corrected the reference from LICENCE to LICENSE to match the actual file name.



ISSUE TYPE

Docs Pull Request

COMPONENT NAME
README.md
ADDITIONAL INFORMATION
Trivial documentation-only change, no changelog is required.

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2026-01-15 21:30:22 +00:00
patchback[bot]
a54b4a13b6 address sanity issues (#1056) (#1058)
This is a backport of PR #1056 as merged into main (bd1cacc).
SUMMARY


helm/helm_info - Deprecate some parameters and add new ones to resolve sanity issues.
k8s - the return block doc is not aligned with what the module returns


ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

helm, helm_info, k8s
Fixes: #1046

Reviewed-by: Bikouo Aubin
Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2026-01-14 17:08:35 +00:00
patchback[bot]
e16b3c46b0 Add new workflow to label prs with needs_triage (#1045) (#1048)
This is a backport of PR #1045 as merged into main (9cfa903).
SUMMARY
This pr adds a new workflow for labeling new and reopened prs that are not marked as draft. The needs_triage label will be removed if the pr is marked as draft during development and re-added once the pr is marked as ready for review.
After consulting with the team, we decided to label prs in a new workflow to allow for the prs and issues to have different labels in the future.
ACA-2362
ISSUE TYPE

Feature Pull Request

COMPONENT NAME
github workflow
Reported CI Issues
Sanity tests: #1046

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2025-12-18 14:29:05 +00:00
patchback[bot]
bc60523372 Add 2.21 ignore file for sanity tests (#1032) (#1036)
SUMMARY

Resolves #1027

Reviewed-by: Yuriy Novostavskiy <yuriy@novostavskiy.kyiv.ua>
Reviewed-by: GomathiselviS <gomathiselvi@gmail.com>
(cherry picked from commit 1c16a2d2b5)

Co-authored-by: Bianca Henderson <bianca@redhat.com>
2025-10-24 09:28:46 -04:00
patchback[bot]
64f3f07c5a Extend k8s action group (#992) (#1026)
This is a backport of PR #992 as merged into main (798f549).
SUMMARY


Add all k8s_* modules to the action group in order to esaily set kubeconfig parameter
ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

meta
ADDITIONAL INFORMATION

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2025-10-14 20:47:03 +00:00
patchback[bot]
d6492b66d9 Selectively redact sensitive kubeconfig data from logs (#1014) (#1024)
This is a backport of PR #1014 as merged into main (4fa3648).
SUMMARY

Resolves #782

ISSUE TYPE


Bugfix Pull Request

ADDITIONAL INFORMATION


The proper redaction of kubeconfig data can be seen by running this example playbook with verbosity of -vvv against the code in this PR.
Prior to these changes, all info was redacted (as shown in the example below):
ok: [local] => {
    "changed": false,
    "invocation": {
        "module_args": {
            "api_key": null,
            "binary_path": null,
            "ca_cert": null,
            "context": null,
            "get_all_values": false,
            "host": null,
            "kubeconfig": {
                "apiVersion": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                "clusters": [
                    {
                        "cluster": {
                            "insecure-skip-tls-verify": true,
                            "server": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                        },
                        "name": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                    },
                    {
                        "cluster": {
                            "certificate-authority-data": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                            "server": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                        },
                        "name": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                    },
                    {
                        "cluster": {
                            "certificate-authority": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                            "extensions": [
                                {
                                    "extension": {
                                        "last-update": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                                        "provider": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                                        "version": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                                    },
                                    "name": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                                }
                            ],
                            "server": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                        },
                        "name": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                    }
                ],
                "contexts": [
                    {
                        "context": {
                            "cluster": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                            "user": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                        },
                        "name": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                    },
                    {
                        "context": {
                            "cluster": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                            "user": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                        },
                        "name": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
                    },
[output shortened]

With the changes in this PR, only sensitive data is redacted:
ok: [local] => {
    "changed": false,
    "invocation": {
        "module_args": {
            "api_key": null,
            "binary_path": null,
            "ca_cert": null,
            "context": null,
            "get_all_values": false,
            "host": null,
            "kubeconfig": {
                "apiVersion": "v1",
                "clusters": [
                    {
                        "cluster": {
                            "insecure-skip-tls-verify": true,
                            "server": "<server address>"
                        },
                        "name": "exercise"
                    },
                    {
                        "cluster": {
                            "certificate-authority-data": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
                            "server": "<server address>"
                        },
                        "name": "kind-drain-test"
                    },
                    {
                        "cluster": {
                            "certificate-authority": "<path to .crt>",
                            "extensions": [
                                {
                                    "extension": {
                                        "last-update": "Tue, 07 Oct 2025 11:25:54 EDT",
                                        "provider": "minikube.sigs.k8s.io",
                                        "version": "v1.35.0"
                                    },
                                    "name": "cluster_info"
                                }
                            ],
                            "server": "<server address>"
                        },
                        "name": "minikube"
                    }
                ],
                "contexts": [
                    {
                        "context": {
                            "cluster": "exercise-pod",
                            "user": "bianca"
                        },
                        "name": "exercise"
                    },
                    {
                        "context": {
                            "cluster": "kind-drain-test",
                            "user": "kind-drain-test"
                        },
                        "name": "kind-drain-test"
                    },
[output shortened]

Reviewed-by: GomathiselviS <gomathiselvi@gmail.com>
2025-10-14 19:16:05 +00:00
patchback[bot]
4d5388ddf8 775 document proxy configurations (#1018) (#1020)
This is a backport of PR #1018 as merged into main (ae624cf).
SUMMARY
Added documentation for no_proxy, proxy, and proxy_headers parameters that were missing from the k8s lookup plugin. These parameters are already implemented in the codebase but were not documented.
no_proxy: Comma separated list of hosts that shouldn't use proxy
proxy: HTTP proxy URL for connections
proxy_headers: Dictionary of proxy headers with suboptions for proxy_basic_auth, basic_auth, and user_agent
Fixes #775
ISSUE TYPE

Docs Pull Request

COMPONENT NAME
k8s lookup
Additional comment
This is a reissue of #993, which was lost during the latest release due to me PR incorrectly from my fork's main branch. Linter errors on the previous PR should already be resolved.
This had the backport-5, backport-6 and skip-changelog labels.

Reviewed-by: Bianca Henderson <beeankha@gmail.com>
2025-10-10 17:30:10 +00:00
62 changed files with 1728 additions and 505 deletions

28
.github/workflows/label-new-prs.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: label new prs
on:
pull_request_target:
types:
- opened
- reopened
- converted_to_draft
- ready_for_review
jobs:
add_label:
if: github.actor != 'patchback[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add 'needs_triage' label if the pr is not a draft
uses: actions-ecosystem/action-add-labels@v1
if: github.event.pull_request.draft == false
with:
labels: needs_triage
- name: Remove 'needs_triage' label if the pr is a draft
uses: actions-ecosystem/action-remove-labels@v1
if: github.event.pull_request.draft == true
with:
labels: needs_triage

View File

@@ -4,6 +4,42 @@ Kubernetes Collection Release Notes
.. contents:: Topics
v6.3.0
======
Release Summary
---------------
This release includes bugfixes such as implementing idempotency for the ``helm_pull`` module as well as a security update for selectively redacting sensitive information from kubeconfig.
Minor Changes
-------------
- Remove deprecated import from ``ansible.module_utils._text`` (https://github.com/ansible-collections/kubernetes.core/pull/1053).
- helm - add ``release_values`` key to ``status`` return value that can be accessed using Jinja2 dot notation (https://github.com/ansible-collections/kubernetes.core/pull/1056).
- helm_info - add ``release_values`` key to ``status`` return value that can be accessed using Jinja2 dot notation (https://github.com/ansible-collections/kubernetes.core/pull/1056).
Deprecated Features
-------------------
- helm - the ``status.values`` return value has been deprecated and will be removed in a release after 2027-01-08. Use ``status.release_values`` instead (https://github.com/ansible-collections/kubernetes.core/pull/1056).
- helm_info - the ``status.values`` return value has been deprecated and will be removed in a release after 2027-01-08. Use ``status.release_values`` instead (https://github.com/ansible-collections/kubernetes.core/pull/1056).
Security Fixes
--------------
- Selectively redact sensitive info from kubeconfig instead of applying blanket ``no_log=True`` (https://github.com/ansible-collections/kubernetes.core/pull/1014).
Bugfixes
--------
- Add idempotency for ``helm_pull`` module (https://github.com/ansible-collections/kubernetes.core/pull/1055).
- Fixed a bug where setting ``K8S_AUTH_VERIFY_SSL=true`` (or any string value) caused the value to be treated as a separate ``kubectl`` command argument. (https://github.com/ansible-collections/kubernetes.core/pull/1049).
- Limit supported versions of Helm to <4.0.0 (https://github.com/ansible-collections/kubernetes.core/pull/1039).
- Replace passing ``warnings`` to ``exit_json`` with ``AnsibleModule.warn`` in the ``k8s_drain``, ``k8s_rollback.py`` and ``k8s_scale.py`` modules as it deprecated in ``ansible-core>=2.19.0`` and will be removed in ``ansible-core>=2.23.0`` (https://github.com/ansible-collections/kubernetes.core/pull/1033).
- k8s - Fix return block from the module documentation (https://github.com/ansible-collections/kubernetes.core/pull/1056).
- meta - Add ``k8s_cluster_info``, ``k8s_json_patch`` and ``k8s_rollback`` to k8s action group (https://github.com/ansible-collections/kubernetes.core/pull/992).
v6.2.0
======

View File

@@ -1,5 +1,5 @@
# Also needs to be updated in galaxy.yml
VERSION = 6.2.0
VERSION = 6.3.0
TEST_ARGS ?= ""
PYTHON_VERSION ?= `python -c 'import platform; print(".".join(platform.python_version_tuple()[0:2]))'`

View File

@@ -30,6 +30,10 @@ A collection may contain metadata that identifies these versions.
PEP440 is the schema used to describe the versions of Ansible.
<!--end requires_ansible-->
### Helm Version Compatibility
Helm modules in this collection are compatible with Helm v3.x and are not yet compatible with Helm v4. Individual modules and their parameters may support a more specific range of Helm versions.
### Python Support
* Collection supports 3.9+
@@ -99,7 +103,7 @@ You can also include it in a `requirements.yml` file and install it via `ansible
---
collections:
- name: kubernetes.core
version: 6.2.0
version: 6.3.0
```
### Installing the Kubernetes Python Library
@@ -178,7 +182,6 @@ For documentation on how to use individual modules and other content included in
## Ansible Turbo Mode Tech Preview
The ``kubernetes.core`` collection supports Ansible Turbo mode as a tech preview via the ``cloud.common`` collection. By default, this feature is disabled. To enable Turbo mode for modules, set the environment variable `ENABLE_TURBO_MODE=1` on the managed node. For example:
```yaml
@@ -217,7 +220,7 @@ You can run the collection's test suites with the commands:
### Testing with `molecule`
There are also integration tests in the `molecule` directory which are meant to be run against a local Kubernetes cluster, e.g. using [KinD](https://kind.sigs.k8s.io) or [Minikube](https://minikube.sigs.k8s.io). To setup a local cluster using KinD and run Molecule:
There are also integration tests in the `molecule` directory which are meant to be run against a local Kubernetes cluster, e.g. using [KinD](https://kind.sigs.k8s.io) or [Minikube](https://minikube.sigs.k8s.io). To set up a local cluster using KinD and run Molecule:
kind create cluster
make test-molecule
@@ -256,7 +259,7 @@ For more information about communication, refer to the [Ansible Communication gu
For the latest supported versions, refer to the release notes below.
If you encounter issues or have questions, you can submit a support request through the following channels:
- GitHub Issues: Report bugs, request features, or ask questions by opening an issue in the [GitHub repository]((https://github.com/ansible-collections/kubernetes.core/).
- GitHub Issues: Report bugs, request features, or ask questions by opening an issue in the [GitHub repository](https://github.com/ansible-collections/kubernetes.core/).
## Release Notes
@@ -268,9 +271,8 @@ We follow the [Ansible Code of Conduct](https://docs.ansible.com/ansible/devel/c
If you encounter abusive behavior, please refer to the [policy violations](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html#policy-violations) section of the Code for information on how to raise a complaint.
## License
GNU General Public License v3.0 or later
See LICENCE to see the full text.
See LICENSE to see the full text.

View File

@@ -1047,9 +1047,11 @@ releases:
5.4.0:
changes:
minor_changes:
- Module ``helm_registry_auth`` does not support idempotency with ``helm >= 3.18.0``
(https://github.com/ansible-collections/kubernetes.core/pull/946).
release_summary: This release updates the ``helm_registry_auth`` module to match the behavior of ``helm >= 3.18.0`` which reports a successful logout regardless of the current state (i.e., no idempotency).
- Module ``helm_registry_auth`` does not support idempotency with ``helm >=
3.18.0`` (https://github.com/ansible-collections/kubernetes.core/pull/946).
release_summary: This release updates the ``helm_registry_auth`` module to match
the behavior of ``helm >= 3.18.0`` which reports a successful logout regardless
of the current state (i.e., no idempotency).
fragments:
- 20250411-kubeconfig-no_log-revert.yaml
- 20250503-fix-unit-tests.yml
@@ -1117,9 +1119,8 @@ releases:
- Add support of skip-schema-validation in ``helm`` module (https://github.com/ansible-collections/kubernetes.core/pull/995)
- kustomize - Add support of local environ (https://github.com/ansible-collections/kubernetes.core/pull/786).
release_summary: This release adds minor changes and bugfixes, including support
of skip-schema-validation in ``helm`` module and removing deprecated
``ansible.module_utils.six`` imports.
of skip-schema-validation in ``helm`` module and removing deprecated ``ansible.module_utils.six``
imports.
fragments:
- 20241030-support-of-evrion-for-kustomize-lookup-plugin.yaml
- 20250731-fix-k8s_cp-initcontainers.yaml
@@ -1127,3 +1128,48 @@ releases:
- 20250922-remove-ansible-six-imports.yaml
- 6_2_0.yml
release_date: '2025-10-07'
6.3.0:
changes:
bugfixes:
- Add idempotency for ``helm_pull`` module (https://github.com/ansible-collections/kubernetes.core/pull/1055).
- Fixed a bug where setting ``K8S_AUTH_VERIFY_SSL=true`` (or any string value)
caused the value to be treated as a separate ``kubectl`` command argument.
(https://github.com/ansible-collections/kubernetes.core/pull/1049).
- Limit supported versions of Helm to <4.0.0 (https://github.com/ansible-collections/kubernetes.core/pull/1039).
- Replace passing ``warnings`` to ``exit_json`` with ``AnsibleModule.warn``
in the ``k8s_drain``, ``k8s_rollback.py`` and ``k8s_scale.py`` modules as
it deprecated in ``ansible-core>=2.19.0`` and will be removed in ``ansible-core>=2.23.0``
(https://github.com/ansible-collections/kubernetes.core/pull/1033).
- k8s - Fix return block from the module documentation (https://github.com/ansible-collections/kubernetes.core/pull/1056).
- meta - Add ``k8s_cluster_info``, ``k8s_json_patch`` and ``k8s_rollback`` to
k8s action group (https://github.com/ansible-collections/kubernetes.core/pull/992).
deprecated_features:
- helm - the ``status.values`` return value has been deprecated and will be
removed in a release after 2027-01-08. Use ``status.release_values`` instead
(https://github.com/ansible-collections/kubernetes.core/pull/1056).
- helm_info - the ``status.values`` return value has been deprecated and will
be removed in a release after 2027-01-08. Use ``status.release_values`` instead
(https://github.com/ansible-collections/kubernetes.core/pull/1056).
minor_changes:
- Remove deprecated import from ``ansible.module_utils._text`` (https://github.com/ansible-collections/kubernetes.core/pull/1053).
- helm - add ``release_values`` key to ``status`` return value that can be accessed
using Jinja2 dot notation (https://github.com/ansible-collections/kubernetes.core/pull/1056).
- helm_info - add ``release_values`` key to ``status`` return value that can
be accessed using Jinja2 dot notation (https://github.com/ansible-collections/kubernetes.core/pull/1056).
release_summary: This release includes bugfixes such as implementing idempotency
for the ``helm_pull`` module as well as a security update for selectively
redacting sensitive information from kubeconfig.
security_fixes:
- Selectively redact sensitive info from kubeconfig instead of applying blanket
``no_log=True`` (https://github.com/ansible-collections/kubernetes.core/pull/1014).
fragments:
- 1033-warnings-deprecations.yaml
- 20251002-fix-k8s-actiongroup.yaml
- 20251007-selective-kubeconfig-redaction.yaml
- 20251115-limit-versions-of-helm.yaml
- 20251220-fix-K8S_AUTH_VERIFY_SSL-in-kubectl-connecton-plugion.yaml
- 20251228-1053-remove-deprecated-import.yaml
- 20260107-add-idempodency-for-helm-pull.yaml
- 20260108-fix-sanity-failures.yml
- 6-3-0.yaml
release_date: '2026-02-03'

View File

@@ -27,7 +27,7 @@ Requirements
------------
The below requirements are needed on the host that executes this module.
- helm >= 3.0 (https://github.com/helm/helm/releases)
- helm >= 3.0, <4.0.0 (https://github.com/helm/helm/releases)
Parameters
@@ -174,6 +174,28 @@ Parameters
<div>location to write the chart.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>force</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">boolean</span>
</div>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 6.3.0</div>
</td>
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li><div style="color: blue"><b>no</b>&nbsp;&larr;</div></li>
<li>yes</li>
</ul>
</td>
<td>
<div>Force download of the chart even if it already exists in the destination directory.</div>
<div>By default, the module will skip downloading if the chart with the same version already exists for idempotency.</div>
<div>When used with O(untar_chart=true), will remove any existing chart directory before extracting.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -397,11 +419,28 @@ Examples
username: myuser
password: mypassword123
- name: Download Chart (force re-download even if exists)
kubernetes.core.helm_pull:
chart_ref: redis
repo_url: https://charts.bitnami.com/bitnami
chart_version: '17.0.0'
destination: /path/to/chart
force: yes
- name: Download and untar chart (force re-extraction even if directory exists)
kubernetes.core.helm_pull:
chart_ref: redis
repo_url: https://charts.bitnami.com/bitnami
chart_version: '17.0.0'
destination: /path/to/chart
untar_chart: yes
force: yes
Return Values
-------------
Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
Common return values are documented `here <https://docs.ansible.com/projects/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
.. raw:: html
@@ -428,6 +467,23 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late
<div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">helm pull --repo test ...</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="return-"></div>
<b>msg</b>
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
</div>
</td>
<td>when chart already exists</td>
<td>
<div>A message indicating the result of the operation.</div>
<br/>
<div style="font-size: smaller"><b>Sample:</b></div>
<div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">Chart redis version 17.0.0 already exists in destination directory</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="return-"></div>

View File

@@ -25,7 +25,7 @@ Requirements
------------
The below requirements are needed on the host that executes this module.
- helm (https://github.com/helm/helm/releases) => 3.8.0
- helm (https://github.com/helm/helm/releases) >= 3.8.0, <4.0.0
Parameters
@@ -215,7 +215,7 @@ Examples
Return Values
-------------
Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
Common return values are documented `here <https://docs.ansible.com/projects/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
.. raw:: html

View File

@@ -25,7 +25,7 @@ tags:
- openshift
- okd
- cluster
version: 6.2.0
version: 6.3.0
build_ignore:
- .DS_Store
- "*.tar.gz"

View File

@@ -8,13 +8,16 @@ action_groups:
- helm_repository
k8s:
- k8s
- k8s_exec
- k8s_info
- k8s_log
- k8s_scale
- k8s_service
- k8s_cluster_info
- k8s_cp
- k8s_drain
- k8s_exec
- k8s_info
- k8s_json_patch
- k8s_log
- k8s_rollback
- k8s_scale
- k8s_service
plugin_routing:
inventory:

View File

@@ -20,7 +20,7 @@ from ansible.errors import (
AnsibleError,
AnsibleFileNotFound,
)
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase

View File

@@ -264,7 +264,8 @@ import subprocess
import tempfile
from ansible.errors import AnsibleError, AnsibleFileNotFound
from ansible.module_utils._text import to_bytes
from ansible.module_utils.common.text.converters import to_bytes
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.six.moves import shlex_quote
from ansible.parsing.yaml.loader import AnsibleLoader
from ansible.plugins.connection import BUFSIZE, ConnectionBase
@@ -324,9 +325,12 @@ class Connection(ConnectionBase):
# Build command options based on doc string
doc_yaml = AnsibleLoader(self.documentation).get_single_data()
for key in doc_yaml.get("options"):
if key.endswith("verify_ssl") and self.get_option(key) != "":
# Translate verify_ssl to skip_verify_ssl, and output as string
skip_verify_ssl = not self.get_option(key)
if key == "validate_certs" and self.get_option(key) != "":
# Translate validate_certs to --insecure-skip-tls-verify flag
# validate_certs=True means verify certs (don't skip verification)
# validate_certs=False means don't verify certs (skip verification)
validate_certs_value = boolean(self.get_option(key), strict=False)
skip_verify_ssl = not validate_certs_value
local_cmd.append(
"{0}={1}".format(
self.connection_options[key], str(skip_verify_ssl).lower()

View File

@@ -86,14 +86,48 @@ DOCUMENTATION = """
description:
- Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME environment
variable.
no_proxy:
description:
- The comma separated list of hosts/domains/IP/CIDR that shouldn't go through proxy.
Can also be specified via K8S_AUTH_NO_PROXY environment variable.
- Please note that this module does not pick up typical proxy settings from the environment (e.g. NO_PROXY).
- This feature requires kubernetes>=19.15.0.
When kubernetes library is less than 19.15.0, it fails even if no_proxy is set correctly.
type: str
password:
description:
- Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD environment
variable.
proxy:
description:
- The URL of an HTTP proxy to use for the connection. Can also be specified via K8S_AUTH_PROXY environment variable.
- Please note that this module does not pick up typical proxy settings from the environment (e.g. HTTP_PROXY).
type: str
proxy_headers:
description:
- The Header used for the HTTP proxy.
- Documentation can be found here
U(https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html?highlight=proxy_headers#urllib3.util.make_headers).
type: dict
suboptions:
proxy_basic_auth:
type: str
description:
- Colon-separated username:password for proxy basic authentication header.
- Can also be specified via K8S_AUTH_PROXY_HEADERS_PROXY_BASIC_AUTH environment.
basic_auth:
type: str
description:
- Colon-separated username:password for basic authentication header.
- Can also be specified via K8S_AUTH_PROXY_HEADERS_BASIC_AUTH environment.
user_agent:
type: str
description:
- String representing the user-agent you want, such as foo/1.0.
- Can also be specified via K8S_AUTH_PROXY_HEADERS_USER_AGENT environment.
client_cert:
description:
- Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE
environment
- Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE environment
variable.
aliases: [ cert_file ]
client_key:

View File

@@ -2,6 +2,8 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
import warnings
def list_dict_str(value):
if isinstance(value, (list, dict, str)):
@@ -9,6 +11,54 @@ def list_dict_str(value):
raise TypeError
def extract_sensitive_values_from_kubeconfig(kubeconfig_data):
"""
Extract only sensitive string values from kubeconfig data for no_log_values.
:arg kubeconfig_data: Dictionary containing kubeconfig data
:returns: Set of sensitive string values to be added to no_log_values
"""
values = set()
sensitive_fields = {
"token",
"password",
"secret",
"client-key-data",
"client-certificate-data",
"certificate-authority-data",
"api_key",
"access-token",
"refresh-token",
}
# Check API version and warn if not v1
if isinstance(kubeconfig_data, dict):
api_version = kubeconfig_data.get("apiVersion", "v1")
if api_version != "v1":
warnings.warn(
f"Kubeconfig API version '{api_version}' is not 'v1'. "
f"Sensitive field redaction is only guaranteed for API version 'v1'. "
f"Some sensitive data may not be properly redacted from the logs.",
UserWarning,
)
def _extract_recursive(data, current_path=""):
if isinstance(data, dict):
for key, value in data.items():
path = f"{current_path}.{key}" if current_path else key
if key in sensitive_fields:
if isinstance(value, str):
values.add(value)
else:
_extract_recursive(value, path)
elif isinstance(data, list):
for i, item in enumerate(data):
_extract_recursive(item, f"{current_path}[{i}]")
_extract_recursive(kubeconfig_data)
return values
AUTH_PROXY_HEADERS_SPEC = dict(
proxy_basic_auth=dict(type="str", no_log=True),
basic_auth=dict(type="str", no_log=True),
@@ -16,7 +66,7 @@ AUTH_PROXY_HEADERS_SPEC = dict(
)
AUTH_ARG_SPEC = {
"kubeconfig": {"type": "raw", "no_log": True},
"kubeconfig": {"type": "raw"},
"context": {},
"host": {},
"api_key": {"no_log": True},

View File

@@ -23,7 +23,7 @@ import base64
import os
from tempfile import NamedTemporaryFile
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.urls import Request
try:

View File

@@ -23,7 +23,7 @@ from abc import ABCMeta, abstractmethod
from select import select
from tempfile import NamedTemporaryFile, TemporaryFile
from ansible.module_utils._text import to_native
from ansible.module_utils.common.text.converters import to_native
# from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
@@ -278,11 +278,15 @@ class K8SCopyFromPod(K8SCopy):
def run(self):
self.files_to_copy = self.list_remote_files()
if self.files_to_copy == []:
# Using warn method instead of passing warnings to exit_json as it is
# deprecated in ansible-core>=2.19.0
self._module.warn(
"No file found from directory '{0}' into remote Pod.".format(
self.remote_path
)
)
self.module.exit_json(
changed=False,
warning="No file found from directory '{0}' into remote Pod.".format(
self.remote_path
),
)
self.copy()

View File

@@ -15,6 +15,9 @@ import tempfile
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
extract_sensitive_values_from_kubeconfig,
)
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
LooseVersion,
)
@@ -118,6 +121,13 @@ class AnsibleHelmModule(object):
elif isinstance(kubeconfig, dict):
kubeconfig_content = kubeconfig
# Redact sensitive fields from kubeconfig for logging purposes
if kubeconfig_content:
# Add original sensitive values to no_log_values to prevent them from appearing in logs
self._module.no_log_values.update(
extract_sensitive_values_from_kubeconfig(kubeconfig_content)
)
if self.params.get("ca_cert"):
ca_cert = self.params.get("ca_cert")
if LooseVersion(self.get_helm_version()) < LooseVersion("3.5.0"):
@@ -192,6 +202,24 @@ class AnsibleHelmModule(object):
return m.group(1)
return None
def validate_helm_version(self):
"""
Validate that Helm version is >=3.0.0 and <4.0.0.
Helm 4 is not yet supported.
"""
helm_version = self.get_helm_version()
if helm_version is None:
self.fail_json(msg="Unable to determine Helm version")
if (LooseVersion(helm_version) < LooseVersion("3.0.0")) or (
LooseVersion(helm_version) >= LooseVersion("4.0.0")
):
self.fail_json(
msg="Helm version must be >=3.0.0,<4.0.0, current version is {0}".format(
helm_version
)
)
def get_values(self, release_name, get_all=False):
"""
Get Values from deployed release

View File

@@ -16,7 +16,6 @@ HELM_AUTH_ARG_SPEC = dict(
type="raw",
aliases=["kubeconfig_path"],
fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]),
no_log=True,
),
host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])),
ca_cert=dict(

View File

@@ -3,6 +3,9 @@ from typing import Optional
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
extract_sensitive_values_from_kubeconfig,
)
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
LooseVersion,
)
@@ -33,6 +36,15 @@ class AnsibleK8SModule:
self._module = self.settings["module_class"](**kwargs)
# Apply kubeconfig redaction for logging purposes
if hasattr(self._module, "params") and hasattr(self._module, "no_log_values"):
kubeconfig = self._module.params.get("kubeconfig")
if kubeconfig and isinstance(kubeconfig, dict):
# Add sensitive values to no_log_values to prevent them from appearing in logs
self._module.no_log_values.update(
extract_sensitive_values_from_kubeconfig(kubeconfig)
)
if self.settings["check_k8s"]:
self.requires("kubernetes")
self.has_at_least("kubernetes", "24.2.0", warn=True)

View File

@@ -3,7 +3,7 @@
from typing import Dict
from ansible.module_utils._text import to_native
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
get_api_client,
)

View File

@@ -419,9 +419,18 @@ status:
returned: always
description: The Date of last update
values:
type: str
type: dict
returned: always
description: Dict of Values used to deploy
description:
- Dict of Values used to deploy.
- This return value has been deprecated and will be removed in a release after
2027-01-08. Use RV(status.release_values) instead.
release_values:
type: dict
returned: always
description:
- Dict of Values used to deploy.
version_added: 6.3.0
stdout:
type: str
description: Full `helm` command stdout, in case you want to display it or examine the event log
@@ -501,7 +510,8 @@ def get_release_status(module, release_name, all_status=False):
if release is None: # not install
return None
release["values"] = module.get_values(release_name)
release["release_values"] = module.get_values(release_name)
release["values"] = release["release_values"]
return release
@@ -918,6 +928,9 @@ def main():
if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
changed = False
chart_ref = module.params.get("chart_ref")
@@ -1153,10 +1166,15 @@ def main():
changed = True
if module.check_mode:
check_status = {"values": {"current": {}, "declared": {}}}
check_status = {
"values": {"current": {}, "declared": {}},
"release_values": {"current": {}, "declared": {}},
}
if release_status:
check_status["values"]["current"] = release_status["values"]
check_status["values"]["current"] = release_status["release_values"]
check_status["values"]["declared"] = release_status
check_status["release_values"]["current"] = release_status["release_values"]
check_status["release_values"]["declared"] = release_status
module.exit_json(
changed=changed,

View File

@@ -115,9 +115,18 @@ status:
returned: always
description: The Date of last update
values:
type: str
type: dict
returned: always
description: Dict of Values used to deploy
description:
- Dict of Values used to deploy
- This return value has been deprecated and will be removed in a release after
2027-01-08. Use RV(status.release_values) instead.
release_values:
type: dict
returned: always
description:
- Dict of Values used to deploy.
version_added: 6.3.0
hooks:
type: list
elements: dict
@@ -202,7 +211,8 @@ def get_release_status(module, release_name, release_state, get_all_values=False
if release is None: # not install
return None
release["values"] = module.get_values(release_name, get_all_values)
release["release_values"] = module.get_values(release_name, get_all_values)
release["values"] = release["release_values"]
release["manifest"] = module.get_manifest(release_name)
release["notes"] = module.get_notes(release_name)
release["hooks"] = module.get_hooks(release_name)
@@ -235,6 +245,9 @@ def main():
if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
release_name = module.params.get("release_name")
release_state = module.params.get("release_state")
get_all_values = module.params.get("get_all_values")

View File

@@ -161,6 +161,9 @@ def main():
mutually_exclusive=mutually_exclusive(),
)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
state = module.params.get("state")
helm_cmd_common = module.get_helm_binary() + " plugin"

View File

@@ -98,6 +98,9 @@ def main():
supports_check_mode=True,
)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
plugin_name = module.params.get("plugin_name")
plugin_list = []

View File

@@ -21,7 +21,7 @@ description:
- There are options for unpacking the chart after download.
requirements:
- "helm >= 3.0 (https://github.com/helm/helm/releases)"
- "helm >= 3.0, <4.0.0 (https://github.com/helm/helm/releases)"
options:
chart_ref:
@@ -89,6 +89,14 @@ options:
- if set to true, will untar the chart after downloading it.
type: bool
default: False
force:
description:
- Force download of the chart even if it already exists in the destination directory.
- By default, the module will skip downloading if the chart with the same version already exists for idempotency.
- When used with O(untar_chart=true), will remove any existing chart directory before extracting.
type: bool
default: False
version_added: 6.3.0
destination:
description:
- location to write the chart.
@@ -152,6 +160,23 @@ EXAMPLES = r"""
destination: /path/to/chart
username: myuser
password: mypassword123
- name: Download Chart (force re-download even if exists)
kubernetes.core.helm_pull:
chart_ref: redis
repo_url: https://charts.bitnami.com/bitnami
chart_version: '17.0.0'
destination: /path/to/chart
force: yes
- name: Download and untar chart (force re-extraction even if directory exists)
kubernetes.core.helm_pull:
chart_ref: redis
repo_url: https://charts.bitnami.com/bitnami
chart_version: '17.0.0'
destination: /path/to/chart
untar_chart: yes
force: yes
"""
RETURN = r"""
@@ -170,6 +195,11 @@ command:
description: Full `helm pull` command built by this module, in case you want to re-run the command outside the module or debug a problem.
returned: always
sample: helm pull --repo test ...
msg:
type: str
description: A message indicating the result of the operation.
returned: when chart already exists
sample: Chart redis version 17.0.0 already exists in destination directory
rc:
type: int
description: Helm pull command return code
@@ -177,6 +207,18 @@ rc:
sample: 1
"""
import os
import shutil
import tarfile
import uuid
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
AnsibleHelmModule,
)
@@ -185,6 +227,115 @@ from ansible_collections.kubernetes.core.plugins.module_utils.version import (
)
def extract_chart_name(chart_ref):
"""
Extract chart name from chart reference.
Args:
chart_ref (str): Chart reference (name, URL, or OCI reference)
Returns:
str: Extracted chart name
"""
chart_name = chart_ref.split("/")[-1]
# Remove any query parameters or fragments from URL-based refs
if "?" in chart_name:
chart_name = chart_name.split("?")[0]
if "#" in chart_name:
chart_name = chart_name.split("#")[0]
# Remove .tgz extension if present
if chart_name.endswith(".tgz"):
chart_name = chart_name[:-4]
return chart_name
def chart_exists(destination, chart_ref, chart_version, untar_chart):
"""
Check if the chart already exists in the destination directory.
For untarred charts: check if directory exists with Chart.yaml matching version
For tarred charts: check if .tgz file exists and contains matching version
Args:
destination (str): Destination directory path
chart_ref (str): Chart reference (name or URL)
chart_version (str): Chart version to check for
untar_chart (bool): Whether to check for untarred or tarred chart
Returns:
bool: True if chart with matching version exists, False otherwise
"""
# YAML is required for version checking
if not HAS_YAML:
return False
# Without version, we can't reliably check
if not chart_version:
return False
# Extract chart name from chart_ref using shared helper
chart_name = extract_chart_name(chart_ref)
if untar_chart:
# Check for extracted directory
chart_dir = os.path.join(destination, chart_name)
chart_yaml_path = os.path.join(chart_dir, "Chart.yaml")
if os.path.isdir(chart_dir) and os.path.isfile(chart_yaml_path):
try:
with open(chart_yaml_path, "r", encoding="utf-8") as chart_file:
chart_metadata = yaml.safe_load(chart_file)
# Ensure chart_metadata is a dict and has a version that matches
if (
chart_metadata
and isinstance(chart_metadata, dict)
and chart_metadata.get("version") == chart_version
and chart_metadata.get("name") == chart_name
):
return True
except (yaml.YAMLError, IOError, OSError, TypeError):
# If we can't read or parse the file, treat as non-existent
pass
else:
# Check for .tgz file
chart_file = os.path.join(destination, f"{chart_name}-{chart_version}.tgz")
if os.path.isfile(chart_file):
try:
# Verify it's a valid tarball with matching version
with tarfile.open(chart_file, "r:gz") as tar:
# Try to extract Chart.yaml to verify version
# Look for Chart.yaml at the expected path: <chart-name>/Chart.yaml
expected_chart_yaml = f"{chart_name}/Chart.yaml"
try:
member = tar.getmember(expected_chart_yaml)
chart_yaml_file = tar.extractfile(member)
if chart_yaml_file:
try:
chart_metadata = yaml.safe_load(chart_yaml_file)
# Ensure chart_metadata is a dict and has a version that matches
if (
chart_metadata
and isinstance(chart_metadata, dict)
and chart_metadata.get("version") == chart_version
and chart_metadata.get("name") == chart_name
):
return True
except (yaml.YAMLError, TypeError):
# If we can't parse the YAML, treat as non-existent
pass
finally:
chart_yaml_file.close()
except KeyError:
# Chart.yaml not found at expected path
pass
except (tarfile.TarError, yaml.YAMLError, IOError, OSError, TypeError):
# If we can't read or parse the tarball, treat as non-existent
pass
return False
def main():
argspec = dict(
chart_ref=dict(type="str", required=True),
@@ -203,6 +354,7 @@ def main():
),
chart_devel=dict(type="bool"),
untar_chart=dict(type="bool", default=False),
force=dict(type="bool", default=False),
destination=dict(type="path", required=True),
chart_ca_cert=dict(type="path"),
chart_ssl_cert_file=dict(type="path"),
@@ -220,13 +372,10 @@ def main():
mutually_exclusive=[("chart_version", "chart_devel")],
)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
helm_version = module.get_helm_version()
if LooseVersion(helm_version) < LooseVersion("3.0.0"):
module.fail_json(
msg="This module requires helm >= 3.0.0, current version is {0}".format(
helm_version
)
)
helm_pull_opt_versionning = dict(
skip_tls_certs_check="3.3.0",
@@ -286,8 +435,72 @@ def main():
module.params.get("chart_ref"),
" ".join(helm_pull_opts),
)
# Check if chart already exists (idempotency)
if module.params.get("chart_version") and not module.params.get("force"):
chart_exists_locally = chart_exists(
module.params.get("destination"),
module.params.get("chart_ref"),
module.params.get("chart_version"),
module.params.get("untar_chart"),
)
if chart_exists_locally:
module.exit_json(
failed=False,
changed=False,
msg="Chart {0} version {1} already exists in destination directory".format(
module.params.get("chart_ref"), module.params.get("chart_version")
),
command="",
stdout="",
stderr="",
rc=0,
)
# When both untar_chart and force are enabled, we need to remove the existing chart directory
# BEFORE running helm pull to prevent helm's "directory already exists" error.
# We do this by:
# 1. Renaming the existing directory to a temporary name (if it exists)
# 2. Running helm pull
# 3. On success: remove the temporary directory
# 4. On failure: restore the temporary directory and report the error
chart_dir_renamed = False
chart_dir = None
chart_dir_backup = None
if module.params.get("untar_chart") and module.params.get("force"):
chart_name = extract_chart_name(module.params.get("chart_ref"))
chart_dir = os.path.join(module.params.get("destination"), chart_name)
# Check if directory exists and contains a Chart.yaml (to be safe)
if os.path.isdir(chart_dir):
chart_yaml_path = os.path.join(chart_dir, "Chart.yaml")
# Only rename if it looks like a Helm chart directory (have Chart.yaml)
if os.path.isfile(chart_yaml_path):
if not module.check_mode:
# Rename to temporary backup name using uuid for uniqueness
backup_suffix = uuid.uuid4().hex[:8]
chart_dir_backup = os.path.join(
module.params.get("destination"),
f".{chart_name}_backup_{backup_suffix}",
)
os.rename(chart_dir, chart_dir_backup)
chart_dir_renamed = True
if not module.check_mode:
rc, out, err = module.run_helm_command(helm_cmd_common, fails_on_error=False)
# Handle cleanup/restore based on helm command result
if chart_dir_renamed:
if rc == 0:
# Success: remove the backup directory
if os.path.isdir(chart_dir_backup):
shutil.rmtree(chart_dir_backup)
else:
# Failure: restore the backup directory
if os.path.isdir(chart_dir_backup) and not os.path.exists(chart_dir):
os.rename(chart_dir_backup, chart_dir)
else:
rc, out, err = (0, "", "")

View File

@@ -20,7 +20,7 @@ author:
- Yuriy Novostavskiy (@yurnov)
requirements:
- "helm (https://github.com/helm/helm/releases) => 3.8.0"
- "helm (https://github.com/helm/helm/releases) >= 3.8.0, <4.0.0"
description:
- Helm registry authentication module allows you to login C(helm registry login) and logout C(helm registry logout) from a Helm registry.
@@ -194,6 +194,9 @@ def main():
supports_check_mode=True,
)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
changed = False
host = module.params.get("host")

View File

@@ -295,6 +295,9 @@ def main():
if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
changed = False
repo_name = module.params.get("repo_name")

View File

@@ -347,6 +347,9 @@ def main():
if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
# Validate Helm version >=3.0.0,<4.0.0
module.validate_helm_version()
helm_cmd = module.get_helm_binary()
if plain_http:

View File

@@ -383,28 +383,24 @@ result:
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: success
returned: when O(resource_definition) or O(src) contains a single object.
type: str
kind:
description: Represents the REST resource this object represents.
returned: success
returned: when O(resource_definition) or O(src) contains a single object.
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
returned: when O(resource_definition) or O(src) contains a single object.
type: complex
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: success
returned: when O(resource_definition) or O(src) contains a single object.
type: complex
status:
description: Current status details for the object.
returned: success
returned: when O(resource_definition) or O(src) contains a single object.
type: complex
items:
description: Returned only when multiple yaml documents are passed to src or resource_definition
returned: when resource_definition or src contains list of objects
type: list
duration:
description: elapsed time of task in seconds
returned: when C(wait) is true
@@ -414,6 +410,46 @@ result:
description: error while trying to create/delete the object.
returned: error
type: complex
results:
description: An array of created, patched, or otherwise present objects.
returned: when O(resource_definition) or O(src) contains a list of objects.
type: complex
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: when O(resource_definition) or O(src) contains a single object.
type: str
kind:
description: Represents the REST resource this object represents.
returned: when O(resource_definition) or O(src) contains a single object.
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: when O(resource_definition) or O(src) contains a single object.
type: complex
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: when O(resource_definition) or O(src) contains a single object.
type: complex
status:
description: Current status details for the object.
returned: when O(resource_definition) or O(src) contains a single object.
type: complex
duration:
description: elapsed time of task in seconds
returned: when C(wait) is true
type: int
sample: 48
error:
description: error while trying to create/delete the object.
returned: error
type: complex
method:
description:
- The method used to deploy the resource.
returned: success
type: str
sample: create
"""
import copy

View File

@@ -140,7 +140,7 @@ result:
import copy
from ansible.module_utils._text import to_native
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)

View File

@@ -149,7 +149,7 @@ import time
import traceback
from datetime import datetime
from ansible.module_utils._text import to_native
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
@@ -441,7 +441,8 @@ class K8sDrainAnsible(object):
warnings.append(warn)
result.append("{0} Pod(s) deleted from node.".format(number_pod))
if warnings:
return dict(result=" ".join(result), warnings=warnings)
for warning in warnings:
self._module.warn(warning)
return dict(result=" ".join(result))
def patch_node(self, unschedulable):

View File

@@ -131,7 +131,7 @@ except ImportError:
# ImportError are managed by the common module already.
pass
from ansible.module_utils._text import to_native
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)

View File

@@ -135,8 +135,8 @@ error:
import copy
import traceback
from ansible.module_utils._text import to_native
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)

View File

@@ -168,7 +168,9 @@ def perform_action(svc, resource):
module.params["kind"],
resource["metadata"]["name"],
)
result = {"changed": False, "warnings": [warn]}
if warn:
module.warn(warn)
result = {"changed": False}
return result
if module.params["kind"] == "Deployment":

View File

@@ -149,7 +149,7 @@ except ImportError:
# Handled in module setup
pass
from ansible.module_utils._text import to_native
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)
@@ -243,10 +243,12 @@ def execute_module(client, module):
module.fail_json(msg=error, **return_attributes)
def _continue_or_exit(warn):
if warn:
module.warn(warn)
if multiple_scale:
return_attributes["results"].append({"warning": warn, "changed": False})
return_attributes["results"].append({"changed": False})
else:
module.exit_json(warning=warn, **return_attributes)
module.exit_json(**return_attributes)
for existing in existing_items:
if kind.lower() == "job":

View File

@@ -126,7 +126,7 @@ result:
import copy
from ansible.module_utils._text import to_native
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
AnsibleModule,
)

View File

@@ -7,3 +7,4 @@
- "v3.15.4"
- "v3.16.0"
- "v3.17.0"
- "v4.0.0"

View File

@@ -13,45 +13,53 @@
include_role:
name: install_helm
- name: "Ensure we honor the environment variables"
include_tasks: test_read_envvars.yml
- name: Main helm tests with Helm v3
when: helm_version != "v4.0.0"
block:
- name: Deploy charts
include_tasks: "tests_chart/{{ test_chart_type }}.yml"
loop_control:
loop_var: test_chart_type
with_items:
- from_local_path
- from_repository
- from_url
- name: "Ensure we honor the environment variables"
include_tasks: test_read_envvars.yml
when: helm_version != "v4.0.0"
- name: test helm upgrade with reuse_values
include_tasks: test_helm_reuse_values.yml
- name: Deploy charts
include_tasks: "tests_chart/{{ test_chart_type }}.yml"
loop_control:
loop_var: test_chart_type
with_items:
- from_local_path
- from_repository
- from_url
- name: test helm upgrade with reset_then_reuse_values
include_tasks: test_helm_reset_then_reuse_values.yml
- name: test helm upgrade with reuse_values
include_tasks: test_helm_reuse_values.yml
- name: test helm dependency update
include_tasks: test_up_dep.yml
- name: test helm upgrade with reset_then_reuse_values
include_tasks: test_helm_reset_then_reuse_values.yml
- name: Test helm uninstall
include_tasks: test_helm_uninstall.yml
- name: test helm dependency update
include_tasks: test_up_dep.yml
- name: Test helm install with chart name containing space
include_tasks: test_helm_with_space_into_chart_name.yml
- name: Test helm uninstall
include_tasks: test_helm_uninstall.yml
# https://github.com/ansible-collections/community.kubernetes/issues/296
- name: Test Skip CRDS feature in helm chart install
include_tasks: test_crds.yml
- name: Test helm install with chart name containing space
include_tasks: test_helm_with_space_into_chart_name.yml
- name: Test insecure registry flag feature
include_tasks: test_helm_insecure.yml
# https://github.com/ansible-collections/community.kubernetes/issues/296
- name: Test Skip CRDS feature in helm chart install
include_tasks: test_crds.yml
- name: Test take ownership flag feature
include_tasks: test_helm_take_ownership.yml
- name: Test insecure registry flag feature
include_tasks: test_helm_insecure.yml
- name: Test helm skip_schema_validation
include_tasks: test_skip_schema_validation.yml
- name: Test take ownership flag feature
include_tasks: test_helm_take_ownership.yml
- name: Test helm skip_schema_validation
include_tasks: test_skip_schema_validation.yml
- name: Test helm version
include_tasks: test_helm_version.yml
- name: Clean helm install
file:

View File

@@ -36,7 +36,7 @@
that:
- install is changed
- '"--reset-then-reuse-values" not in install.command'
- release_value["status"]["values"] == chart_release_values
- release_value["status"]["release_values"] == chart_release_values
- name: Upgrade chart using reset_then_reuse_values=true
helm:
@@ -64,7 +64,7 @@
- '"--reset-then-reuse-values" in upgrade.command'
- '"--reuse-values " not in upgrade.command'
- '"--reset-values" not in upgrade.command'
- release_value["status"]["values"] == chart_release_values | combine(chart_reset_then_reuse_values, recursive=true)
- release_value["status"]["release_values"] == chart_release_values | combine(chart_reset_then_reuse_values, recursive=true)
always:
- name: Remove helm namespace

View File

@@ -36,7 +36,7 @@
that:
- install is changed
- '"--reuse-values=True" not in install.command'
- release_value["status"]["values"] == chart_release_values
- release_value["status"]["release_values"] == chart_release_values
- name: Upgrade chart using reuse_values=true
helm:
@@ -62,7 +62,7 @@
- upgrade is changed
- '"--reuse-values=True" in upgrade.command'
- '"--reset-values" not in upgrade.command'
- release_value["status"]["values"] == chart_release_values | combine(chart_reuse_values, recursive=true)
- release_value["status"]["release_values"] == chart_release_values | combine(chart_reuse_values, recursive=true)
always:
- name: Remove helm namespace

View File

@@ -0,0 +1,47 @@
---
- name: Test helm reuse_values
vars:
helm_namespace: "{{ test_namespace[14] }}"
chart_release_values:
replica:
replicaCount: 3
master:
count: 1
kind: Deployment
chart_reuse_values:
replica:
replicaCount: 1
master:
count: 3
block:
- name: Initial chart installation
helm:
binary_path: "{{ helm_binary }}"
chart_ref: oci://registry-1.docker.io/bitnamicharts/redis
release_name: test-redis
release_namespace: "{{ helm_namespace }}"
create_namespace: true
release_values: "{{ chart_release_values }}"
register: install
ignore_errors: true
when: helm_version == "v4.0.0"
- name: Debug install result
debug:
var: install
when: helm_version == "v4.0.0"
- name: Ensure helm installation was failed for v4.0.0
assert:
that:
- install is failed
- "'Helm version must be >=3.0.0,<4.0.0' in install.msg"
when: helm_version == "v4.0.0"
always:
- name: Remove helm namespace
k8s:
api_version: v1
kind: Namespace
name: "{{ helm_namespace }}"
state: absent

View File

@@ -57,7 +57,7 @@
that:
- install_check_mode is changed
- install_check_mode.status is defined
- install_check_mode.status.values is defined
- install_check_mode.status.release_values is defined
- name: "Install {{ chart_test }} from {{ source }}"
helm:
@@ -131,7 +131,7 @@
- install is changed
- install.status.status | lower == 'deployed'
- install.status.chart == chart_test+"-"+chart_test_version
- "install.status['values'].revisionHistoryLimit == 0"
- "install.status['release_values'].revisionHistoryLimit == 0"
- name: Check idempotency after adding vars
helm:
@@ -149,7 +149,7 @@
- install is not changed
- install.status.status | lower == 'deployed'
- install.status.chart == chart_test+"-"+chart_test_version
- "install.status['values'].revisionHistoryLimit == 0"
- "install.status['release_values'].revisionHistoryLimit == 0"
- name: "Remove Vars to {{ chart_test }} from {{ source }}"
helm:
@@ -166,7 +166,7 @@
- install is changed
- install.status.status | lower == 'deployed'
- install.status.chart == chart_test+"-"+chart_test_version
- install.status['values'] == {}
- install.status['release_values'] == {}
- name: Check idempotency after removing vars
helm:
@@ -183,7 +183,7 @@
- install is not changed
- install.status.status | lower == 'deployed'
- install.status.chart == chart_test+"-"+chart_test_version
- install.status['values'] == {}
- install.status['release_values'] == {}
- name: "Upgrade {{ chart_test }} from {{ source }}"
helm:
@@ -317,7 +317,7 @@
- install is changed
- install.status.status | lower == 'deployed'
- install.status.chart == chart_test+"-"+chart_test_version
- "install.status['values'].revisionHistoryLimit == 0"
- "install.status['release_values'].revisionHistoryLimit == 0"
- name: "Install {{ chart_test }} from {{ source }} with values_files (again)"
helm:
@@ -402,7 +402,7 @@
namespace: "{{ helm_namespace }}"
create_namespace: true
context: does-not-exist
ignore_errors: yes
ignore_errors: true
register: result
- name: Assert that release fails with non-existent context

View File

@@ -47,7 +47,7 @@
assert:
that:
- _result is failed
- _result.msg == "This module requires helm >= 3.0.0, current version is 2.3.0"
- _result.msg == "Helm version must be >=3.0.0,<4.0.0, current version is 2.3.0"
vars:
helm_path: "{{ temp_dir }}/2.3.0/linux-amd64/helm"
@@ -221,6 +221,101 @@
- _chart.stat.exists
- _chart.stat.isdir
# Test idempotency with tarred chart
- name: Download chart with version (first time)
helm_pull:
binary_path: "{{ helm_path }}"
chart_ref: "oci://registry-1.docker.io/bitnamicharts/redis"
destination: "{{ destination }}"
chart_version: "24.1.0"
register: _result_first
- name: Download chart with version (second time - should be idempotent)
helm_pull:
binary_path: "{{ helm_path }}"
chart_ref: "oci://registry-1.docker.io/bitnamicharts/redis"
destination: "{{ destination }}"
chart_version: "24.1.0"
register: _result_second
- name: Validate idempotency for tarred chart
assert:
that:
- _result_first is changed
- _result_second is not changed
# Test force parameter with tarred chart
- name: Download chart with force=true (should always download)
helm_pull:
binary_path: "{{ helm_path }}"
chart_ref: "oci://registry-1.docker.io/bitnamicharts/redis"
destination: "{{ destination }}"
chart_version: "24.1.0"
force: true
register: _result_force
- name: Validate force parameter causes download
assert:
that:
- _result_force is changed
# Test idempotency with untarred chart in the separate folder
- name: Create separate directory for untar test under {{ temp_dir }}
ansible.builtin.file:
path: "{{ destination }}/untar_test"
state: directory
mode: '0755'
- name: Download and untar chart (first time)
helm_pull:
binary_path: "{{ helm_path }}"
chart_ref: "oci://registry-1.docker.io/bitnamicharts/redis"
destination: "{{ destination }}/untar_test"
chart_version: "24.0.0"
untar_chart: true
register: _result_untar_first
- name: Download and untar chart (second time - should be idempotent)
helm_pull:
binary_path: "{{ helm_path }}"
chart_ref: "oci://registry-1.docker.io/bitnamicharts/redis"
destination: "{{ destination }}/untar_test"
chart_version: "24.0.0"
untar_chart: true
register: _result_untar_second
- name: Validate idempotency for untarred chart
assert:
that:
- _result_untar_first is changed
- _result_untar_second is not changed
- name: Download and untar chart with force=true (should remove existing directory and re-extract)
helm_pull:
binary_path: "{{ helm_path }}"
chart_ref: "oci://registry-1.docker.io/bitnamicharts/redis"
destination: "{{ destination }}/untar_test"
chart_version: "24.0.0"
untar_chart: true
force: true
register: _result_untar_force
- name: Validate first force extraction works
assert:
that:
- _result_untar_force is changed
- name: Verify chart directory still exists after force re-extraction
stat:
path: "{{ destination }}/untar_test/redis"
register: _chart_after_force
- name: Validate chart directory exists
assert:
that:
- _chart_after_force.stat.exists
- _chart_after_force.stat.isdir
vars:
helm_path: "{{ temp_dir }}/3.8.0/linux-amd64/helm"

View File

@@ -20,10 +20,10 @@
- name: Assert that release was created with user-defined variables
assert:
that:
- '"phase" in user_values.status["values"]'
- '"versioned" in user_values.status["values"]'
- user_values.status["values"]["phase"] == "integration"
- user_values.status["values"]["versioned"] is false
- '"phase" in user_values.status["release_values"]'
- '"versioned" in user_values.status["release_values"]'
- user_values.status["release_values"]["phase"] == "integration"
- user_values.status["release_values"]["versioned"] is false
# install chart using set_values and release_values
- name: Install helm binary (> 3.10.0) requires to use set-json
@@ -55,10 +55,10 @@
- name: Assert that release was created with user-defined variables
assert:
that:
- values.status["values"].replicaCount == 3
- values.status["values"].master.image.registry == "docker.io"
- values.status["values"].master.image.repository == "bitnami/apache"
- values.status["values"].master.image.tag == "2.4.54-debian-11-r74"
- values.status["release_values"].replicaCount == 3
- values.status["release_values"].master.image.registry == "docker.io"
- values.status["release_values"].master.image.repository == "bitnami/apache"
- values.status["release_values"].master.image.tag == "2.4.54-debian-11-r74"
# install chart using set_values and values_files
- name: create temporary file to save values in
@@ -96,8 +96,8 @@
- name: Assert that release was created with user-defined variables
assert:
that:
- values.status["values"].mode == "distributed"
- values.status["values"].disableWebUI is true
- values.status["release_values"].mode == "distributed"
- values.status["release_values"].disableWebUI is true
always:
- name: Delete temporary file

View File

@@ -1,42 +1,4 @@
---
k8s_pod_metadata:
labels:
app: "{{ k8s_pod_name }}"
k8s_pod_spec:
serviceAccount: "{{ k8s_pod_service_account }}"
containers:
- image: "{{ k8s_pod_image }}"
imagePullPolicy: Always
name: "{{ k8s_pod_name }}"
command: "{{ k8s_pod_command }}"
readinessProbe:
initialDelaySeconds: 15
exec:
command:
- /bin/true
resources: "{{ k8s_pod_resources }}"
ports: "{{ k8s_pod_ports }}"
env: "{{ k8s_pod_env }}"
k8s_pod_service_account: default
k8s_pod_resources:
limits:
cpu: "100m"
memory: "100Mi"
k8s_pod_command: []
k8s_pod_ports: []
k8s_pod_env: []
k8s_pod_template:
metadata: "{{ k8s_pod_metadata }}"
spec: "{{ k8s_pod_spec }}"
test_namespace: "apply"
k8s_wait_timeout: 240

View File

@@ -292,36 +292,34 @@
- name: Add a deployment
k8s:
namespace: "{{ test_namespace }}"
definition:
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: apply-deploy
namespace: "{{ test_namespace }}"
labels:
app: apply-deploy
spec:
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
app: apply-deploy
template:
metadata:
labels:
app: apply-deploy
spec:
serviceAccount: apply-deploy
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
wait: yes
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
apply: yes
vars:
k8s_pod_name: apply-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-green
k8s_pod_service_account: apply-deploy
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
k8s_pod_resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 100m
memory: 100Mi
- name: Update the earlier deployment in check mode
k8s:
@@ -335,33 +333,29 @@
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
app: apply-deploy
template:
metadata:
labels:
app: apply-deploy
spec:
serviceAccount: apply-deploy
containers:
- name: nginx-2
image: nginx:latest
ports:
- containerPort: 80
wait: yes
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
apply: yes
check_mode: yes
vars:
k8s_pod_name: apply-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-purple
k8s_pod_service_account: apply-deploy
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
k8s_pod_resources:
requests:
cpu: 50m
limits:
cpu: 50m
memory: 50Mi
register: update_deploy_check_mode
- name: Ensure check mode change took
assert:
that:
- update_deploy_check_mode is changed
- "update_deploy_check_mode.result.spec.template.spec.containers[0].image == 'gcr.io/kuar-demo/kuard-amd64:v0.10.0-purple'"
- "update_deploy_check_mode.result.spec.template.spec.containers[0].name == 'nginx-2'"
- name: Update the earlier deployment
k8s:
@@ -375,32 +369,28 @@
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
app: apply-deploy
template:
metadata:
labels:
app: apply-deploy
spec:
serviceAccount: apply-deploy
containers:
- name: nginx-2
image: nginx:latest
ports:
- containerPort: 80
wait: yes
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
apply: yes
vars:
k8s_pod_name: apply-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-purple
k8s_pod_service_account: apply-deploy
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
k8s_pod_resources:
requests:
cpu: 50m
limits:
cpu: 50m
memory: 50Mi
register: update_deploy_for_real
- name: Ensure change took
assert:
that:
- update_deploy_for_real is changed
- "update_deploy_for_real.result.spec.template.spec.containers[0].image == 'gcr.io/kuar-demo/kuard-amd64:v0.10.0-purple'"
- "update_deploy_for_real.result.spec.template.spec.containers[0].name == 'nginx-2'"
- name: Remove the serviceaccount
k8s:
@@ -424,27 +414,23 @@
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
app: apply-deploy
template:
metadata:
labels:
app: apply-deploy
spec:
serviceAccount: apply-deploy
containers:
- name: nginx-3
image: nginx:latest
ports:
- containerPort: 80
wait: yes
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
apply: yes
vars:
k8s_pod_name: apply-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-green
k8s_pod_service_account: apply-deploy
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
k8s_pod_resources:
requests:
cpu: 50m
limits:
cpu: 50m
memory: 50Mi
ignore_errors: true
register: deploy_after_serviceaccount_removal
ignore_errors: yes
- name: Ensure that updating deployment after service account removal failed
assert:

View File

@@ -69,49 +69,51 @@
ignore_errors: true
register: _result
- name: Validate that 'find' executable is missing from Pod
assert:
that:
- _result is failed
fail_msg: "Pod contains 'find' executable, therefore we cannot run the next tasks."
# - name: Validate that 'find' executable is missing from Pod
# assert:
# that:
# - _result is failed
# fail_msg: "Pod contains 'find' executable, therefore we cannot run the next tasks."
- name: Copy files into container
k8s_cp:
namespace: "{{ copy_namespace }}"
pod: '{{ pod_without_executable_find.name }}'
remote_path: '{{ item.path }}'
content: '{{ item.content }}'
state: to_pod
with_items:
- path: /ansible/root.txt
content: this file is located at the root directory
- path: /ansible/.hidden_root.txt
content: this hidden file is located at the root directory
- path: /ansible/.sudir/root.txt
content: this file is located at the root of the sub directory
- path: /ansible/.sudir/.hidden_root.txt
content: this hidden file is located at the root of the sub directory
- name: Copy directory into Pod without 'find' executable
block:
- name: Copy files into container
k8s_cp:
namespace: "{{ copy_namespace }}"
pod: '{{ pod_without_executable_find.name }}'
remote_path: '{{ item.path }}'
content: '{{ item.content }}'
state: to_pod
with_items:
- path: /ansible/root.txt
content: this file is located at the root directory
- path: /ansible/.hidden_root.txt
content: this hidden file is located at the root directory
- path: /ansible/.sudir/root.txt
content: this file is located at the root of the sub directory
- path: /ansible/.sudir/.hidden_root.txt
content: this hidden file is located at the root of the sub directory
- name: Delete existing directory
file:
path: /tmp/openjdk-files
state: absent
ignore_errors: true
- name: Delete existing directory
file:
path: /tmp/openjdk-files
state: absent
ignore_errors: true
- name: copy directory from Pod into local filesystem (new directory to create)
k8s_cp:
namespace: '{{ copy_namespace }}'
pod: '{{ pod_without_executable_find.name }}'
remote_path: /ansible
local_path: /tmp/openjdk-files
state: from_pod
- name: copy directory from Pod into local filesystem (new directory to create)
k8s_cp:
namespace: '{{ copy_namespace }}'
pod: '{{ pod_without_executable_find.name }}'
remote_path: /ansible
local_path: /tmp/openjdk-files
state: from_pod
- name: Compare directories
kubectl_file_compare:
namespace: '{{ copy_namespace }}'
pod: '{{ pod_without_executable_find.name }}'
remote_path: /ansible
local_path: /tmp/openjdk-files
- name: Compare directories
kubectl_file_compare:
namespace: '{{ copy_namespace }}'
pod: '{{ pod_without_executable_find.name }}'
remote_path: /ansible
local_path: /tmp/openjdk-files
always:
- name: Remove directories created into remote Pod

View File

@@ -10,9 +10,13 @@
path: "{{ test_directory }}"
state: directory
- name: Create a large text file
ansible.builtin.shell:
cmd: base64 /dev/random | head -c 150M > {{ test_directory }}/large_text_file.txt
- name: Create a text file with specific content
ansible.builtin.copy:
dest: "{{ test_directory }}/large_text_file.txt"
content: |
This is a large text file
{{ 'Repeat this line 1000 times\n' * 1000 }}
mode: '0644'
- name: Create a large binary file
ansible.builtin.command:

View File

@@ -6,7 +6,7 @@ metadata:
spec:
containers:
- name: '{{ pod_with_one_container.container }}'
image: busybox
image: busybox:latest
command:
- /bin/sh
- -c
@@ -19,13 +19,13 @@ metadata:
spec:
containers:
- name: '{{ pod_with_two_container.container[0] }}'
image: busybox:1.32.0
image: busybox:latest
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
- name: '{{ pod_with_two_container.container[1] }}'
image: busybox:1.33.0
image: busybox:latest
command:
- /bin/sh
- -c
@@ -37,8 +37,8 @@ metadata:
name: '{{ pod_without_executable_find.name }}'
spec:
containers:
- name: openjdk17
image: openjdk:17
- name: openjdk
image: openjdk:27-ea
command:
- /bin/sh
- -c

View File

@@ -24,7 +24,7 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
name: openjdk-d
name: busybox-d
labels:
context: ansible
spec:
@@ -38,8 +38,8 @@ spec:
context: ansible
spec:
containers:
- name: openjdk
image: openjdk:17
- name: busybox
image: busybox:latest
command:
- /bin/sh
- -c

View File

@@ -17,7 +17,7 @@
wait_timeout: 400
vars:
k8s_pod_name: delete-ds
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1
k8s_pod_image: docker.io/nginx:latest
register: ds
- name: Check that daemonset wait worked

View File

@@ -81,6 +81,7 @@
replicas: 1
current_replicas: 3
wait: true
wait_timeout: 60
register: scale
- name: Read deployment

View File

@@ -402,12 +402,16 @@
namespace: "{{ namespace }}"
register: result
- name: Debug result
debug:
var: result
- name: Assert warning is returned for no rollout history
assert:
that:
- not result.changed
- result.rollback_info[0].warnings is defined
- "'No rollout history found' in result.rollback_info[0].warnings[0]"
- result.warnings is defined
- "'No rollout history found' in result.warnings[0]"
- name: Create a service for unsupported resource test
k8s:

View File

@@ -1,42 +1,4 @@
---
k8s_pod_metadata:
labels:
app: "{{ k8s_pod_name }}"
k8s_pod_spec:
serviceAccount: "{{ k8s_pod_service_account }}"
containers:
- image: "{{ k8s_pod_image }}"
imagePullPolicy: Always
name: "{{ k8s_pod_name }}"
command: "{{ k8s_pod_command }}"
readinessProbe:
initialDelaySeconds: 15
exec:
command:
- /bin/true
resources: "{{ k8s_pod_resources }}"
ports: "{{ k8s_pod_ports }}"
env: "{{ k8s_pod_env }}"
k8s_pod_service_account: default
k8s_pod_resources:
limits:
cpu: "100m"
memory: "100Mi"
k8s_pod_command: []
k8s_pod_ports: []
k8s_pod_env: []
k8s_pod_template:
metadata: "{{ k8s_pod_metadata }}"
spec: "{{ k8s_pod_spec }}"
test_namespace: "scale"
k8s_wait_timeout: 400

View File

@@ -5,28 +5,32 @@
- name: Add a deployment
k8s:
namespace: "{{ scale_namespace }}"
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: scale-deploy
namespace: "{{ scale_namespace }}"
labels:
app: scale-deploy
spec:
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
wait: yes
app: scale-deploy
template:
metadata:
labels:
app: scale-deploy
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
wait: true
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
apply: yes
vars:
k8s_pod_name: scale-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-green
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
apply: true
- name: Get pods in scale-deploy
k8s_info:
@@ -44,7 +48,8 @@
name: scale-deploy
namespace: "{{ scale_namespace }}"
replicas: 0
wait: yes
wait: true
wait_timeout: 60
register: scale_down
check_mode: true
@@ -75,7 +80,8 @@
name: scale-deploy
namespace: "{{ scale_namespace }}"
replicas: 0
wait: yes
wait: true
wait_timeout: 60
register: scale_down
check_mode: true
@@ -106,7 +112,7 @@
name: scale-deploy
namespace: "{{ scale_namespace }}"
replicas: 0
wait: yes
wait: true
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
register: scale_down
diff: true
@@ -138,7 +144,8 @@
name: scale-deploy
namespace: "{{ scale_namespace }}"
replicas: 0
wait: yes
wait: true
wait_timeout: 60
register: scale_down_idempotency
diff: true
@@ -159,18 +166,20 @@
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
wait: yes
app: scale-deploy
template:
metadata:
labels:
app: scale-deploy
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
wait: true
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
apply: yes
vars:
k8s_pod_name: scale-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-green
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
apply: true
register: reapply_after_scale
- name: Get pods in scale-deploy
@@ -199,7 +208,7 @@
wait: yes
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
register: scale_up
diff: no
diff: false
- name: Get pods in scale-deploy
k8s_info:
@@ -227,8 +236,9 @@
namespace: "{{ scale_namespace }}"
replicas: 2
wait: yes
wait_timeout: 60
register: scale_up_noop
diff: no
diff: false
- name: Get pods in scale-deploy
k8s_info:
@@ -255,7 +265,7 @@
name: scale-deploy
namespace: "{{ scale_namespace }}"
replicas: 1
wait: no
wait: false
register: scale_down_no_wait
diff: true
@@ -302,12 +312,12 @@
resource_version: 0
label_selectors:
- app=nginx
wait_timeout: 60
register: scale_out
- assert:
that:
- not scale_out.changed
- scale_out.results | selectattr('warning', 'defined') | list | length == 2
- name: scale deployment using current replicas (wrong value)
kubernetes.core.k8s_scale:
@@ -322,7 +332,6 @@
- assert:
that:
- not scale_out.changed
- scale_out.results | selectattr('warning', 'defined') | list | length == 2
- name: scale deployment using current replicas (right value)
kubernetes.core.k8s_scale:

View File

@@ -40,7 +40,7 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-ds
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1
k8s_pod_image: docker.io/busybox:latest
k8s_pod_command:
- sleep
- "600"
@@ -71,7 +71,7 @@
wait_timeout: 180
vars:
k8s_pod_name: wait-ds
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2
k8s_pod_image: docker.io/alpine:latest
k8s_pod_command:
- sleep
- "600"
@@ -82,7 +82,7 @@
assert:
that:
- update_ds_check_mode is changed
- "update_ds_check_mode.result.spec.template.spec.containers[0].image == 'gcr.io/kuar-demo/kuard-amd64:2'"
- "update_ds_check_mode.result.spec.template.spec.containers[0].image == 'docker.io/alpine:latest'"
- name: Update a daemonset
k8s:
@@ -104,7 +104,7 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-ds
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:3
k8s_pod_image: docker.io/busybox:latest
k8s_pod_command:
- sleep
- "600"
@@ -125,7 +125,7 @@
assert:
that:
- ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled
- updated_ds_pods.resources[0].spec.containers[0].image.endswith(":3")
- updated_ds_pods.resources[0].spec.containers[0].image == 'docker.io/busybox:latest'
- name: Create daemonset with nodeSelector and not existing label
k8s:
@@ -145,7 +145,7 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-daemonset-not-existing-label
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1
k8s_pod_image: docker.io/busybox:latest
k8s_pod_command:
- sleep
- "600"
@@ -187,7 +187,7 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-sts
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1
k8s_pod_image: docker.io/busybox:latest
k8s_pod_command:
- sleep
- "600"
@@ -251,7 +251,7 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-sts
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:3
k8s_pod_image: docker.io/alpine:latest
k8s_pod_command:
- sleep
- "600"
@@ -272,7 +272,7 @@
assert:
that:
- sts.result.spec.replicas == sts.result.status.readyReplicas
- updated_sts_pods.resources[0].spec.containers[0].image.endswith(":3")
- updated_sts_pods.resources[0].spec.containers[0].image == 'docker.io/alpine:latest'
- name: Add a crashing pod
k8s:
@@ -288,11 +288,11 @@
wait_timeout: 30
vars:
k8s_pod_name: wait-crash-pod
k8s_pod_image: alpine:3.8
k8s_pod_image: busybox:latest
k8s_pod_command:
- /bin/false
register: crash_pod
ignore_errors: yes
ignore_errors: true
- name: Check that task failed
assert:
@@ -315,7 +315,7 @@
k8s_pod_name: wait-no-image-pod
k8s_pod_image: i_made_this_up:and_this_too
register: no_image_pod
ignore_errors: yes
ignore_errors: true
- name: Check that task failed
assert:
@@ -340,12 +340,11 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1
k8s_pod_image: docker.io/nginx:latest
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
register: deploy
- name: Check that deployment wait worked
@@ -371,7 +370,7 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2
k8s_pod_image: docker.io/nginx:stable-alpine
k8s_pod_ports:
- containerPort: 8080
name: http
@@ -394,7 +393,7 @@
field_selectors:
- status.phase=Running
register: updated_deploy_pods
until: updated_deploy_pods.resources[0].spec.containers[0].image.endswith(':2')
until: updated_deploy_pods.resources[0].spec.containers[0].image == 'docker.io/nginx:stable-alpine'
retries: 6
delay: 5
@@ -474,11 +473,11 @@
wait_timeout: "{{ k8s_wait_timeout | default(omit) }}"
vars:
k8s_pod_name: wait-crash-deploy
k8s_pod_image: alpine:3.8
k8s_pod_image: docker.io/nginx:latest
k8s_pod_command:
- /bin/false
register: wait_crash_deploy
ignore_errors: yes
ignore_errors: true
- name: Check that task failed
assert:
@@ -495,7 +494,7 @@
wait: yes
wait_sleep: 2
wait_timeout: 5
ignore_errors: yes
ignore_errors: true
register: short_wait_remove_pod
- name: Check that task failed
@@ -509,4 +508,4 @@
kind: Namespace
name: "{{ wait_namespace }}"
state: absent
ignore_errors: yes
ignore_errors: true

View File

@@ -1,12 +1,15 @@
plugins/module_utils/client/discovery.py import-3.11!skip
plugins/module_utils/client/discovery.py import-3.12!skip
plugins/module_utils/client/discovery.py import-3.13!skip
plugins/module_utils/client/discovery.py import-3.14!skip
plugins/module_utils/client/resource.py import-3.11!skip
plugins/module_utils/client/resource.py import-3.12!skip
plugins/module_utils/client/resource.py import-3.13!skip
plugins/module_utils/client/resource.py import-3.14!skip
plugins/module_utils/k8sdynamicclient.py import-3.11!skip
plugins/module_utils/k8sdynamicclient.py import-3.12!skip
plugins/module_utils/k8sdynamicclient.py import-3.13!skip
plugins/module_utils/k8sdynamicclient.py import-3.14!skip
plugins/module_utils/version.py pylint!skip
plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc
plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc

View File

@@ -0,0 +1,36 @@
plugins/module_utils/client/discovery.py import-3.11!skip
plugins/module_utils/client/discovery.py import-3.12!skip
plugins/module_utils/client/discovery.py import-3.13!skip
plugins/module_utils/client/discovery.py import-3.14!skip
plugins/module_utils/client/resource.py import-3.11!skip
plugins/module_utils/client/resource.py import-3.12!skip
plugins/module_utils/client/resource.py import-3.13!skip
plugins/module_utils/client/resource.py import-3.14!skip
plugins/module_utils/k8sdynamicclient.py import-3.11!skip
plugins/module_utils/k8sdynamicclient.py import-3.12!skip
plugins/module_utils/k8sdynamicclient.py import-3.13!skip
plugins/module_utils/k8sdynamicclient.py import-3.14!skip
plugins/module_utils/version.py pylint!skip
plugins/modules/k8s.py validate-modules:parameter-type-not-in-doc
plugins/modules/k8s_scale.py validate-modules:parameter-type-not-in-doc
plugins/modules/k8s_service.py validate-modules:parameter-type-not-in-doc
tests/unit/module_utils/fixtures/clusteroperator.yml yamllint!skip
tests/unit/module_utils/fixtures/definitions.yml yamllint!skip
tests/unit/module_utils/fixtures/deployments.yml yamllint!skip
tests/integration/targets/k8s_delete/files/deployments.yaml yamllint!skip
tests/unit/module_utils/fixtures/pods.yml yamllint!skip
tests/integration/targets/helm/files/appversionless-chart-v2/templates/configmap.yaml yamllint!skip
tests/integration/targets/helm/files/appversionless-chart/templates/configmap.yaml yamllint!skip
tests/integration/targets/helm/files/test-chart-v2/templates/configmap.yaml yamllint!skip
tests/integration/targets/helm/files/test-chart/templates/configmap.yaml yamllint!skip
tests/integration/targets/helm_diff/files/test-chart/templates/configmap.yaml yamllint!skip
tests/integration/targets/k8s_scale/files/deployment.yaml yamllint!skip
plugins/modules/k8s.py validate-modules:return-syntax-error
plugins/modules/k8s_scale.py validate-modules:return-syntax-error
plugins/modules/k8s_service.py validate-modules:return-syntax-error
plugins/modules/k8s_taint.py validate-modules:return-syntax-error
tests/integration/targets/helm_diff/files/test-chart-reuse-values/templates/configmap.yaml yamllint!skip
tests/integration/targets/helm_registry_auth/tasks/main.yaml yamllint!skip
tests/integration/targets/helm_diff/files/test-chart-deployment-time/templates/configmap.yaml yamllint!skip
plugins/modules/helm.py validate-modules:bad-return-value-key
plugins/modules/helm_info.py validate-modules:bad-return-value-key

View File

@@ -8,8 +8,8 @@ from io import BytesIO
import ansible.module_utils.basic
import pytest
from ansible.module_utils._text import to_bytes
from ansible.module_utils.common._collections_compat import MutableMapping
from ansible.module_utils.common.text.converters import to_bytes
@pytest.fixture

View File

@@ -443,3 +443,46 @@ def test_module_get_helm_set_values_args(set_values, expected):
result = helm_module.get_helm_set_values_args(set_values)
assert " ".join(expected) == result
@pytest.mark.parametrize(
"helm_version,should_fail",
[
("3.0.0", False),
("3.5.0", False),
("3.10.3", False),
("3.15.0", False),
("3.17.0", False),
("2.9.0", True),
("2.17.0", True),
("4.0.0", True),
("4.1.0", True),
("5.0.0", True),
],
)
def test_module_validate_helm_version(_ansible_helm_module, helm_version, should_fail):
_ansible_helm_module.get_helm_version = MagicMock()
_ansible_helm_module.get_helm_version.return_value = helm_version
if should_fail:
with pytest.raises(SystemExit):
_ansible_helm_module.validate_helm_version()
_ansible_helm_module.fail_json.assert_called_once()
call_args = _ansible_helm_module.fail_json.call_args
assert "Helm version must be >=3.0.0,<4.0.0" in call_args[1]["msg"]
assert helm_version in call_args[1]["msg"]
else:
_ansible_helm_module.validate_helm_version()
_ansible_helm_module.fail_json.assert_not_called()
def test_module_validate_helm_version_none(_ansible_helm_module):
_ansible_helm_module.get_helm_version = MagicMock()
_ansible_helm_module.get_helm_version.return_value = None
with pytest.raises(SystemExit):
_ansible_helm_module.validate_helm_version()
_ansible_helm_module.fail_json.assert_called_once_with(
msg="Unable to determine Helm version"
)

View File

@@ -0,0 +1,401 @@
# Copyright [2025] [Red Hat, Inc.]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import warnings
import pytest
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
extract_sensitive_values_from_kubeconfig,
)
@pytest.fixture
def mock_kubeconfig_with_sensitive_data():
return {
"apiVersion": "v1",
"kind": "Config",
"clusters": [
{
"name": "test-cluster",
"cluster": {
"server": "https://test-cluster.example.com",
"certificate-authority-data": "LS0tLS1CRUdJTi...fake-cert-data",
"insecure-skip-tls-verify": False,
},
}
],
"contexts": [
{
"name": "test-context",
"context": {
"cluster": "test-cluster",
"user": "test-user",
"namespace": "default",
},
}
],
"current-context": "test-context",
"users": [
{
"name": "test-user",
"user": {
"client-certificate-data": "LS0tLS1CRUdJTi...fake-client-cert",
"client-key-data": "LS0tLS1CRUdJTi...fake-client-key",
"token": "eyJhbGciOiJSUzI1NiIs...fake-token",
"password": "fake-password-123",
"username": "testuser",
},
},
{
"name": "service-account-user",
"user": {
"token": "eyJhbGciOiJSUzI1NiIs...fake-service-token",
},
},
],
}
@pytest.fixture
def mock_kubeconfig_with_nested_sensitive_data():
return {
"apiVersion": "v1",
"kind": "Config",
"clusters": [
{
"name": "cluster-1",
"cluster": {
"certificate-authority-data": "fake-ca-data-1",
},
},
{
"name": "cluster-2",
"cluster": {
"certificate-authority-data": "fake-ca-data-2",
},
},
],
"users": [
{
"name": "user-1",
"user": {
"client-certificate-data": "fake-cert-1",
"client-key-data": "fake-key-1",
"token": "fake-token-1",
"secret": "fake-secret-1",
"api_key": "fake-api-key-1",
"access-token": "fake-access-token-1",
"refresh-token": "fake-refresh-token-1",
},
},
],
}
@pytest.fixture
def mock_kubeconfig_without_sensitive_data():
return {
"apiVersion": "v1",
"kind": "Config",
"clusters": [
{
"name": "test-cluster",
"cluster": {
"server": "https://test-cluster.example.com",
"insecure-skip-tls-verify": True,
},
}
],
"users": [
{
"name": "test-user",
"user": {
"username": "testuser",
},
}
],
}
@pytest.fixture
def mock_kubeconfig_v2():
"""Mock kubeconfig with API version v2 to test warning behavior."""
return {
"apiVersion": "v2",
"kind": "Config",
"clusters": [
{
"name": "test-cluster",
"cluster": {
"server": "https://test-cluster.example.com",
"certificate-authority-data": "fake-ca-data-v2",
},
}
],
"users": [
{
"name": "test-user",
"user": {
"token": "fake-token-v2",
"password": "fake-password-v2",
},
}
],
}
def test_extract_sensitive_values_basic(mock_kubeconfig_with_sensitive_data):
result = extract_sensitive_values_from_kubeconfig(
mock_kubeconfig_with_sensitive_data
)
# Should extract all sensitive string values
expected_values = {
"LS0tLS1CRUdJTi...fake-cert-data", # certificate-authority-data
"LS0tLS1CRUdJTi...fake-client-cert", # client-certificate-data
"LS0tLS1CRUdJTi...fake-client-key", # client-key-data
"eyJhbGciOiJSUzI1NiIs...fake-token", # token
"fake-password-123", # password
"eyJhbGciOiJSUzI1NiIs...fake-service-token", # second token
}
assert result == expected_values
def test_extract_sensitive_values_nested(mock_kubeconfig_with_nested_sensitive_data):
result = extract_sensitive_values_from_kubeconfig(
mock_kubeconfig_with_nested_sensitive_data
)
# Should extract all sensitive values from multiple clusters and users
expected_values = {
"fake-ca-data-1",
"fake-ca-data-2",
"fake-cert-1",
"fake-key-1",
"fake-token-1",
"fake-secret-1",
"fake-api-key-1",
"fake-access-token-1",
"fake-refresh-token-1",
}
assert result == expected_values
def test_extract_sensitive_values_no_sensitive_data(
mock_kubeconfig_without_sensitive_data,
):
result = extract_sensitive_values_from_kubeconfig(
mock_kubeconfig_without_sensitive_data
)
# Should return empty set since there is no sensitive data
assert result == set()
def test_redaction_placeholder_appears_in_output(mock_kubeconfig_with_sensitive_data):
"""Test that sensitive values are replaced with VALUE_SPECIFIED_IN_NO_LOG_PARAMETER in output."""
sensitive_values = extract_sensitive_values_from_kubeconfig(
mock_kubeconfig_with_sensitive_data
)
# Create a mock module output that would contain sensitive data
mock_output = {
"kubeconfig": mock_kubeconfig_with_sensitive_data,
"result": "success",
"sensitive_token": "eyJhbGciOiJSUzI1NiIs...fake-token",
"sensitive_password": "fake-password-123",
}
# Simulate what Ansible does when no_log_values is set
json_str = json.dumps(mock_output)
for sensitive_value in sensitive_values:
json_str = json_str.replace(
f'"{sensitive_value}"', '"VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"'
)
redacted_output = json.loads(json_str)
# Verify that sensitive values are replaced with the redaction placeholder
assert redacted_output["sensitive_token"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert (
redacted_output["sensitive_password"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
# Verify that non-sensitive data remains unchanged
assert redacted_output["result"] == "success"
assert redacted_output["kubeconfig"]["users"][0]["user"]["username"] == "testuser"
def test_redaction_placeholder_appears_in_nested_output(
mock_kubeconfig_with_nested_sensitive_data,
):
# Extract sensitive values
sensitive_values = extract_sensitive_values_from_kubeconfig(
mock_kubeconfig_with_nested_sensitive_data
)
# Create a mock module output that would contain nested sensitive data
mock_output = {
"kubeconfig": mock_kubeconfig_with_nested_sensitive_data,
"result": "success",
"cluster_ca_data": "fake-ca-data-1",
"user_cert_data": "fake-cert-1",
"user_key_data": "fake-key-1",
"user_token": "fake-token-1",
"user_secret": "fake-secret-1",
"api_key": "fake-api-key-1",
"access_token": "fake-access-token-1",
"refresh_token": "fake-refresh-token-1",
}
# Simulate what Ansible does when no_log_values is set
json_str = json.dumps(mock_output)
for sensitive_value in sensitive_values:
json_str = json_str.replace(
f'"{sensitive_value}"', '"VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"'
)
redacted_output = json.loads(json_str)
# Verify that sensitive values are replaced with the redaction placeholder
assert redacted_output["cluster_ca_data"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert redacted_output["user_cert_data"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert redacted_output["user_key_data"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert redacted_output["user_token"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert redacted_output["user_secret"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert redacted_output["api_key"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert redacted_output["access_token"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
assert redacted_output["refresh_token"] == "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
# Verify that non-sensitive data remains unchanged
assert redacted_output["result"] == "success"
assert redacted_output["kubeconfig"]["apiVersion"] == "v1"
assert redacted_output["kubeconfig"]["kind"] == "Config"
assert redacted_output["kubeconfig"]["clusters"][0]["name"] == "cluster-1"
assert redacted_output["kubeconfig"]["clusters"][1]["name"] == "cluster-2"
assert redacted_output["kubeconfig"]["users"][0]["name"] == "user-1"
# Verify that sensitive values within the nested kubeconfig structure are also redacted
assert (
redacted_output["kubeconfig"]["clusters"][0]["cluster"][
"certificate-authority-data"
]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["clusters"][1]["cluster"][
"certificate-authority-data"
]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["users"][0]["user"]["client-certificate-data"]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["users"][0]["user"]["client-key-data"]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["users"][0]["user"]["token"]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["users"][0]["user"]["secret"]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["users"][0]["user"]["api_key"]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["users"][0]["user"]["access-token"]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
assert (
redacted_output["kubeconfig"]["users"][0]["user"]["refresh-token"]
== "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
)
def test_warning_for_non_v1_api_version(mock_kubeconfig_v2):
with pytest.warns(UserWarning) as warning_list:
result = extract_sensitive_values_from_kubeconfig(mock_kubeconfig_v2)
# Verify that exactly one warning was raised
assert len(warning_list) == 1
# Verify the warning message content
warning = warning_list[0]
assert "Kubeconfig API version 'v2' is not 'v1'" in str(warning.message)
# Verify that the function still works and extracts sensitive values
expected_values = {
"fake-ca-data-v2",
"fake-token-v2",
"fake-password-v2",
}
assert result == expected_values
def test_no_warning_for_v1_api_version(mock_kubeconfig_with_sensitive_data):
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always") # Capture all warnings
result = extract_sensitive_values_from_kubeconfig(
mock_kubeconfig_with_sensitive_data
)
# Filter for UserWarning specifically (our warning type)
user_warnings = [w for w in warning_list if issubclass(w.category, UserWarning)]
assert len(user_warnings) == 0
# Verify that the function still works normally
assert len(result) > 0
def test_no_warning_for_missing_api_version():
"""Test that no warning is raised when apiVersion field is missing (defaults to v1)."""
kubeconfig_no_version = {
"kind": "Config",
"clusters": [
{
"name": "test-cluster",
"cluster": {
"server": "https://test-cluster.example.com",
"certificate-authority-data": "fake-ca-data",
},
}
],
"users": [
{
"name": "test-user",
"user": {
"token": "fake-token",
},
}
],
}
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always") # Capture all warnings
result = extract_sensitive_values_from_kubeconfig(kubeconfig_no_version)
# Filter for UserWarning specifically (our warning type)
user_warnings = [w for w in warning_list if issubclass(w.category, UserWarning)]
assert len(user_warnings) == 0
# Verify that the function still works normally
expected_values = {"fake-ca-data", "fake-token"}
assert result == expected_values

View File

@@ -43,15 +43,21 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
def test_dependency_update_option_not_defined(self):
set_module_args({"chart_ref": "/tmp/path"})
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm_template.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm template /tmp/path", environ_update={}, data=None
# Check the last call was the actual helm template command
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm template /tmp/path"
)
assert result.exception.args[0]["command"] == "/usr/bin/helm template /tmp/path"
@@ -64,17 +70,21 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
}
)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm_template.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm template test --repo=https://charts.com/test",
environ_update={},
data=None,
# Check the last call was the actual helm template command
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm template test --repo=https://charts.com/test"
)
assert (
result.exception.args[0]["command"]
@@ -86,17 +96,21 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
{"chart_ref": "https://charts/example.tgz", "dependency_update": True}
)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm_template.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm template https://charts/example.tgz --dependency-update",
environ_update={},
data=None,
# Check the last call was the actual helm template command
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm template https://charts/example.tgz --dependency-update"
)
assert (
result.exception.args[0]["command"]

View File

@@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
import unittest
from unittest.mock import MagicMock, call, patch
from unittest.mock import MagicMock, patch
from ansible.module_utils import basic
from ansible_collections.kubernetes.core.plugins.modules import helm
@@ -77,18 +77,22 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
helm.run_dep_update = MagicMock()
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
helm.run_dep_update.assert_not_called()
mock_run_command.assert_called_once_with(
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'"
)
assert (
result.exception.args[0]["command"]
@@ -108,18 +112,22 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
helm.run_dep_update = MagicMock()
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
helm.run_dep_update.assert_not_called()
mock_run_command.assert_called_once_with(
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'"
)
assert (
result.exception.args[0]["command"]
@@ -139,19 +147,23 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_with_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = 0, "configuration updated", ""
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with patch.object(basic.AnsibleModule, "warn") as mock_warn:
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_warn.assert_not_called()
mock_run_command.assert_has_calls(
[
call(
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
)
]
# Check calls include the actual helm command (after version check)
assert any(
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'" in str(call)
for call in mock_run_command.call_args_list
)
assert (
result.exception.args[0]["command"]
@@ -170,23 +182,23 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with patch.object(basic.AnsibleModule, "warn") as mock_warn:
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_warn.assert_called_once()
mock_run_command.assert_has_calls(
[
call(
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
)
]
# Check calls include the actual helm command (after version check)
assert any(
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'" in str(call)
for call in mock_run_command.call_args_list
)
assert (
result.exception.args[0]["command"]
@@ -245,17 +257,21 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'"
)
assert (
result.exception.args[0]["command"]
@@ -275,17 +291,21 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'"
)
assert (
result.exception.args[0]["command"]
@@ -305,11 +325,15 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_with_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleFailJson) as result:
helm.main()
# mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1',
@@ -334,17 +358,21 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm --repo=http://repo.example/charts install --dependency-update --replace test 'chart1'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm --repo=http://repo.example/charts install --dependency-update --replace test 'chart1'"
)
assert (
result.exception.args[0]["command"]
@@ -402,17 +430,21 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'"
)
assert (
result.exception.args[0]["command"]
@@ -431,17 +463,21 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'"
)
assert (
result.exception.args[0]["command"]
@@ -460,11 +496,15 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_with_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleFailJson) as result:
helm.main()
# mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1',
@@ -488,17 +528,21 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase):
helm.get_release_status = MagicMock(return_value=None)
helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep)
with patch.object(basic.AnsibleModule, "run_command") as mock_run_command:
mock_run_command.return_value = (
0,
"configuration updated",
"",
) # successful execution
# Mock responses: first call is helm version, second is the actual command
mock_run_command.side_effect = [
(
0,
'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}',
"",
),
(0, "configuration updated", ""),
]
with self.assertRaises(AnsibleExitJson) as result:
helm.main()
mock_run_command.assert_called_once_with(
"/usr/bin/helm install --dependency-update --replace test 'http://repo.example/charts/application.tgz'",
environ_update={"HELM_NAMESPACE": "test"},
data=None,
# Check the last call (actual helm command, after version check)
assert (
mock_run_command.call_args_list[-1][0][0]
== "/usr/bin/helm install --dependency-update --replace test 'http://repo.example/charts/application.tgz'"
)
assert (
result.exception.args[0]["command"]