mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-26 21:33:02 +00:00
helm_registry_auth module to authenticate in OCI registry (#800)
* new module helm_registry_auth
* Initial integration tests
* final update copyright and integration test before pr
* update link to pr in changelog fragment
* reformat plugins/module_utils/helm.py with black
to fix linters in actions
* attempt to fix unit test
unit test was missing initially
* fix https://pycqa.github.io/isort/ linter
* next attemp to fix unit-test
* remove unused and unsupported helm_args_common
* remove unused imports and fix other linters errors
* another fix for unit test
* fix issue introducied by commit ff02893a12a31f9c44b5c48f9a8bf85057295961
* add binary_path to arg_spec
* return helm_cmd in the output of check mode
remove changlog fragment
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* description suggestion from reviewer/maintainer
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* remove changed from module return
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* remove redundant code
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* Update plugins/modules/helm_registry_auth.py
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* consider support of logout when user is not logged in
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* consider support helm < 3.0.0
* Revert "consider support helm < 3.0.0"
This reverts commit f20004d196.
* reintroduce support of helm version less than 3.8.0
reference: https://helm.sh/docs/topics/registries/#enabling-oci-support-prior-to-v380
* revert reintroducing support of helm < 3.8.0
reason: didn't find a quick way to deal with tests
* update documentation with the recent module updates
* Update plugins/modules/helm_registry_auth.py
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* add test of logout impendency
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
* fix linters
* fix intendations in the integration tests
* create tests/integration/targets/helm_registry_auth/aliases
* fix integration test (typo)
* fix integration tests (test wrong cred)
* add stderr when module fail
* another attempt to fix integration test
* fix assertion in integration test to be not affceted by the #830
---------
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6609abdd5a
commit
aee847431a
13
README.md
13
README.md
@@ -21,7 +21,7 @@ For more information about communication, see the [Ansible communication guide](
|
||||
## Requirements
|
||||
|
||||
<!--start requires_ansible-->
|
||||
### Ansible version compatibility
|
||||
## Ansible version compatibility
|
||||
|
||||
This collection has been tested against following Ansible versions: **>=2.15.0**.
|
||||
|
||||
@@ -47,28 +47,28 @@ This collection supports Kubernetes versions >= 1.24.
|
||||
Click on the name of a plugin or module to view that content's documentation:
|
||||
|
||||
<!--start collection content-->
|
||||
#### Connection plugins
|
||||
### Connection plugins
|
||||
Name | Description
|
||||
--- | ---
|
||||
[kubernetes.core.kubectl](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.kubectl_connection.rst)|Execute tasks in pods running on Kubernetes.
|
||||
|
||||
#### K8s filter plugins
|
||||
### K8s filter plugins
|
||||
Name | Description
|
||||
--- | ---
|
||||
kubernetes.core.k8s_config_resource_name|Generate resource name for the given resource of type ConfigMap, Secret
|
||||
|
||||
#### Inventory plugins
|
||||
### Inventory plugins
|
||||
Name | Description
|
||||
--- | ---
|
||||
[kubernetes.core.k8s](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.k8s_inventory.rst)|Kubernetes (K8s) inventory source
|
||||
|
||||
#### Lookup plugins
|
||||
### Lookup plugins
|
||||
Name | Description
|
||||
--- | ---
|
||||
[kubernetes.core.k8s](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.k8s_lookup.rst)|Query the K8s API
|
||||
[kubernetes.core.kustomize](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.kustomize_lookup.rst)|Build a set of kubernetes resources using a 'kustomization.yaml' file.
|
||||
|
||||
#### Modules
|
||||
### Modules
|
||||
Name | Description
|
||||
--- | ---
|
||||
[kubernetes.core.helm](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_module.rst)|Manages Kubernetes packages with the Helm package manager
|
||||
@@ -76,6 +76,7 @@ Name | Description
|
||||
[kubernetes.core.helm_plugin](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_plugin_module.rst)|Manage Helm plugins
|
||||
[kubernetes.core.helm_plugin_info](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_plugin_info_module.rst)|Gather information about Helm plugins
|
||||
[kubernetes.core.helm_pull](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_pull_module.rst)|download a chart from a repository and (optionally) unpack it in local directory.
|
||||
[kubernetes.core.helm_registry_auth](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_registry_auth_module.rst)|Helm registry authentication module
|
||||
[kubernetes.core.helm_repository](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_repository_module.rst)|Manage Helm repositories.
|
||||
[kubernetes.core.helm_template](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_template_module.rst)|Render chart templates
|
||||
[kubernetes.core.k8s](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.k8s_module.rst)|Manage Kubernetes (K8s) objects
|
||||
|
||||
332
docs/kubernetes.core.helm_registry_auth_module.rst
Normal file
332
docs/kubernetes.core.helm_registry_auth_module.rst
Normal file
@@ -0,0 +1,332 @@
|
||||
.. _kubernetes.core.helm_registry_auth_module:
|
||||
|
||||
|
||||
**********************************
|
||||
kubernetes.core.helm_registry_auth
|
||||
**********************************
|
||||
|
||||
**Helm registry authentication module**
|
||||
|
||||
|
||||
Version added: 5.1.0
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 1
|
||||
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
- Helm registry authentication module allows you to login ``helm registry login`` and logout ``helm registry logout`` from a Helm registry.
|
||||
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
The below requirements are needed on the host that executes this module.
|
||||
|
||||
- helm (https://github.com/helm/helm/releases) => 3.8.0
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<table border=0 cellpadding=0 class="documentation-table">
|
||||
<tr>
|
||||
<th colspan="1">Parameter</th>
|
||||
<th>Choices/<font color="blue">Defaults</font></th>
|
||||
<th width="100%">Comments</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>binary_path</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">path</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>The path of a helm binary to use.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>ca_file</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">path</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Path to the CA certificate SSL file for verify registry server certificate.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>cert_file</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">path</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Path to the client certificate SSL file for identify registry client using this certificate file.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>host</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
/ <span style="color: red">required</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Provide a URL for accessing the registry.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: registry_url</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>insecure</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">boolean</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ul style="margin: 0; padding: 0"><b>Choices:</b>
|
||||
<li><div style="color: blue"><b>no</b> ←</div></li>
|
||||
<li>yes</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>Allow connections to SSL sites without certs.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>key_file</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">path</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Path to the client key SSL file for identify registry client using this key file.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>password</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Password for the registry.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: repo_password</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>state</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ul style="margin: 0; padding: 0"><b>Choices:</b>
|
||||
<li><div style="color: blue"><b>present</b> ←</div></li>
|
||||
<li>absent</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>Desired state of the registry.</div>
|
||||
<div>If set to V(present) attempt to log in to the remote registry server using the URL specified in O(host).</div>
|
||||
<div>If set to V(absent) attempt to log out from the remote registry server using the URL specified in O(host).</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>username</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username for the registry.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: repo_username</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- name: Login to remote registry
|
||||
kubernetes.core.helm_registry_auth:
|
||||
username: admin
|
||||
password: "sample_password"
|
||||
host: localhost:5000
|
||||
|
||||
- name: Logout from remote registry
|
||||
kubernetes.core.helm_registry_auth:
|
||||
state: absent
|
||||
host: localhost:5000
|
||||
|
||||
|
||||
|
||||
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:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<table border=0 cellpadding=0 class="documentation-table">
|
||||
<tr>
|
||||
<th colspan="1">Key</th>
|
||||
<th>Returned</th>
|
||||
<th width="100%">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>command</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>always</td>
|
||||
<td>
|
||||
<div>Full <code>helm</code> command executed</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;">/usr/local/bin/helm registry login oci-registry.domain.example --username=admin --password-stdin --insecure</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>failed</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">boolean</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>always</td>
|
||||
<td>
|
||||
<div>Indicate if the <code>helm</code> command failed</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>stderr</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>always</td>
|
||||
<td>
|
||||
<div>Full <code>helm</code> command stderr, in case you want to display it or examine the event log. Please be note that helm binnary may print messages to stderr even if the command is successful.</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;">Login Succeeded\n</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>stderr_lines</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>always</td>
|
||||
<td>
|
||||
<div>Full <code>helm</code> command stderr, in case you want to display it or examine the event log</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>stdout</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>always</td>
|
||||
<td>
|
||||
<div>Full <code>helm</code> command stdout, in case you want to display it or examine the event log</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>stout_lines</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>always</td>
|
||||
<td>
|
||||
<div>Full <code>helm</code> command stdout, in case you want to display it or examine the event log</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/><br/>
|
||||
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
- Yuriy Novostavskiy (@yurnov)
|
||||
@@ -159,11 +159,13 @@ class AnsibleHelmModule(object):
|
||||
self.helm_env = self._prepare_helm_environment()
|
||||
return self.helm_env
|
||||
|
||||
def run_helm_command(self, command, fails_on_error=True):
|
||||
def run_helm_command(self, command, fails_on_error=True, data=None):
|
||||
if not HAS_YAML:
|
||||
self.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
||||
|
||||
rc, out, err = self.run_command(command, environ_update=self.env_update)
|
||||
rc, out, err = self.run_command(
|
||||
command, environ_update=self.env_update, data=data
|
||||
)
|
||||
if fails_on_error and rc != 0:
|
||||
self.fail_json(
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
|
||||
|
||||
238
plugins/modules/helm_registry_auth.py
Normal file
238
plugins/modules/helm_registry_auth.py
Normal file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: © Ericsson AB 2024
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: helm_registry_auth
|
||||
|
||||
short_description: Helm registry authentication module
|
||||
|
||||
version_added: 5.1.0
|
||||
|
||||
author:
|
||||
- Yuriy Novostavskiy (@yurnov)
|
||||
|
||||
requirements:
|
||||
- "helm (https://github.com/helm/helm/releases) => 3.8.0"
|
||||
|
||||
description:
|
||||
- Helm registry authentication module allows you to login C(helm registry login) and logout C(helm registry logout) from a Helm registry.
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Desired state of the registry.
|
||||
- If set to V(present) attempt to log in to the remote registry server using the URL specified in O(host).
|
||||
- If set to V(absent) attempt to log out from the remote registry server using the URL specified in O(host).
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
type: str
|
||||
host:
|
||||
description:
|
||||
- Provide a URL for accessing the registry.
|
||||
required: true
|
||||
aliases: [ registry_url ]
|
||||
type: str
|
||||
insecure:
|
||||
description:
|
||||
- Allow connections to SSL sites without certs.
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
username:
|
||||
description:
|
||||
- Username for the registry.
|
||||
required: false
|
||||
type: str
|
||||
aliases: [ repo_username ]
|
||||
password:
|
||||
description:
|
||||
- Password for the registry.
|
||||
required: false
|
||||
type: str
|
||||
aliases: [ repo_password ]
|
||||
key_file:
|
||||
description:
|
||||
- Path to the client key SSL file for identify registry client using this key file.
|
||||
required: false
|
||||
type: path
|
||||
cert_file:
|
||||
description:
|
||||
- Path to the client certificate SSL file for identify registry client using this certificate file.
|
||||
required: false
|
||||
type: path
|
||||
ca_file:
|
||||
description:
|
||||
- Path to the CA certificate SSL file for verify registry server certificate.
|
||||
required: false
|
||||
type: path
|
||||
binary_path:
|
||||
description:
|
||||
- The path of a helm binary to use.
|
||||
required: false
|
||||
type: path
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Login to remote registry
|
||||
kubernetes.core.helm_registry_auth:
|
||||
username: admin
|
||||
password: "sample_password"
|
||||
host: localhost:5000
|
||||
|
||||
- name: Logout from remote registry
|
||||
kubernetes.core.helm_registry_auth:
|
||||
state: absent
|
||||
host: localhost:5000
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
stdout:
|
||||
type: str
|
||||
description: Full C(helm) command stdout, in case you want to display it or examine the event log
|
||||
returned: always
|
||||
stout_lines:
|
||||
type: list
|
||||
description: Full C(helm) command stdout, in case you want to display it or examine the event log
|
||||
returned: always
|
||||
stderr:
|
||||
type: str
|
||||
description: >-
|
||||
Full C(helm) command stderr, in case you want to display it or examine the event log.
|
||||
Please be note that helm binnary may print messages to stderr even if the command is successful.
|
||||
returned: always
|
||||
sample: 'Login Succeeded\n'
|
||||
stderr_lines:
|
||||
type: list
|
||||
description: Full C(helm) command stderr, in case you want to display it or examine the event log
|
||||
returned: always
|
||||
command:
|
||||
type: str
|
||||
description: Full C(helm) command executed
|
||||
returned: always
|
||||
sample: '/usr/local/bin/helm registry login oci-registry.domain.example --username=admin --password-stdin --insecure'
|
||||
failed:
|
||||
type: bool
|
||||
description: Indicate if the C(helm) command failed
|
||||
returned: always
|
||||
sample: false
|
||||
"""
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
AnsibleHelmModule,
|
||||
)
|
||||
|
||||
|
||||
def arg_spec():
|
||||
return dict(
|
||||
binary_path=dict(type="path", required=False),
|
||||
host=dict(type="str", aliases=["registry_url"], required=True),
|
||||
state=dict(default="present", choices=["present", "absent"], required=False),
|
||||
insecure=dict(type="bool", default=False, required=False),
|
||||
username=dict(type="str", aliases=["repo_username"], required=False),
|
||||
password=dict(
|
||||
type="str", aliases=["repo_password"], no_log=True, required=False
|
||||
),
|
||||
key_file=dict(type="path", required=False),
|
||||
cert_file=dict(type="path", required=False),
|
||||
ca_file=dict(type="path", required=False),
|
||||
)
|
||||
|
||||
|
||||
def login(
|
||||
command,
|
||||
host,
|
||||
insecure,
|
||||
username,
|
||||
password,
|
||||
key_file,
|
||||
cert_file,
|
||||
ca_file,
|
||||
):
|
||||
login_command = command + " registry login " + host
|
||||
|
||||
if username is not None and password is not None:
|
||||
login_command += " --username=" + username + " --password-stdin"
|
||||
|
||||
if insecure:
|
||||
login_command += " --insecure"
|
||||
|
||||
if key_file is not None:
|
||||
login_command += " --key-file=" + key_file
|
||||
|
||||
if cert_file is not None:
|
||||
login_command += " --cert-file=" + cert_file
|
||||
|
||||
if ca_file is not None:
|
||||
login_command += " --ca-file=" + ca_file
|
||||
|
||||
return login_command
|
||||
|
||||
|
||||
def logout(command, host):
|
||||
return command + " registry logout " + host
|
||||
|
||||
|
||||
def main():
|
||||
global module
|
||||
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=arg_spec(),
|
||||
required_together=[["username", "password"]],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
changed = False
|
||||
|
||||
host = module.params.get("host")
|
||||
state = module.params.get("state")
|
||||
insecure = module.params.get("insecure")
|
||||
username = module.params.get("username")
|
||||
password = module.params.get("password")
|
||||
key_file = module.params.get("key_file")
|
||||
cert_file = module.params.get("cert_file")
|
||||
ca_file = module.params.get("ca_file")
|
||||
|
||||
helm_cmd = module.get_helm_binary()
|
||||
|
||||
if state == "absent":
|
||||
helm_cmd = logout(helm_cmd, host)
|
||||
changed = True
|
||||
elif state == "present":
|
||||
helm_cmd = login(
|
||||
helm_cmd, host, insecure, username, password, key_file, cert_file, ca_file
|
||||
)
|
||||
changed = True
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=changed, command=helm_cmd)
|
||||
|
||||
rc, out, err = module.run_helm_command(
|
||||
helm_cmd, data=password, fails_on_error=False
|
||||
)
|
||||
|
||||
if rc != 0:
|
||||
if state == "absent" and "Error: not logged in" in err:
|
||||
changed = False
|
||||
else:
|
||||
module.fail_json(
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
|
||||
rc, out, err
|
||||
),
|
||||
stderr=err,
|
||||
command=helm_cmd,
|
||||
)
|
||||
|
||||
module.exit_json(changed=changed, stdout=out, stderr=err, command=helm_cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
tests/integration/targets/helm_registry_auth/aliases
Normal file
2
tests/integration/targets/helm_registry_auth/aliases
Normal file
@@ -0,0 +1,2 @@
|
||||
time=16
|
||||
helm_registry_auth
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
# Username and password for the registry
|
||||
# ../files/registry.password contains username and hashed password
|
||||
username: testuser
|
||||
password: testpassword
|
||||
wrong_password: 'WrongPassword'
|
||||
registry_name: oci_registry
|
||||
registry_port: 5000
|
||||
test_chart: https://github.com/grafana/helm-charts/releases/download/k8s-monitoring-1.6.8/k8s-monitoring-1.6.8.tgz
|
||||
@@ -0,0 +1 @@
|
||||
testuser:$2y$05$PmdUjSCJYdRUZlsYy8QGWuJDiwuHtWXa28YrELlN5haeHkZ1seZZG
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
dependencies:
|
||||
- install_helm
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Test helm_registry_auth module
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: true
|
||||
roles:
|
||||
- helm_registry_auth
|
||||
5
tests/integration/targets/helm_registry_auth/runme.sh
Executable file
5
tests/integration/targets/helm_registry_auth/runme.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eux
|
||||
export ANSIBLE_CALLBACKS_ENABLED=profile_tasks
|
||||
export ANSIBLE_ROLES_PATH=../
|
||||
ansible-playbook playbook.yaml "$@"
|
||||
178
tests/integration/targets/helm_registry_auth/tasks/main.yaml
Normal file
178
tests/integration/targets/helm_registry_auth/tasks/main.yaml
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
- name: Run module test
|
||||
# using a shell and command module to run the test as test can be non-idempotent
|
||||
# and it allow to not install any additional dependencies
|
||||
block:
|
||||
- name: Ensure that helm is installed
|
||||
ansible.builtin.shell: helm version --client --short | grep v3
|
||||
register: _helm_version
|
||||
failed_when: _helm_version.rc != 0
|
||||
|
||||
- name: Ensure that Docker demon is running
|
||||
ansible.builtin.command: "docker info"
|
||||
register: _docker_info
|
||||
failed_when: _docker_info.rc != 0
|
||||
|
||||
- name: Create a tmpfile htpasswd directory
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: .httppasswd
|
||||
register: _tmpfile
|
||||
|
||||
- name: Copy htpasswd to the tmpfile directory
|
||||
ansible.builtin.copy:
|
||||
src: registry.password
|
||||
dest: "{{ _tmpfile.path }}/registry.password"
|
||||
|
||||
- name: Setup the registry
|
||||
ansible.builtin.command: >-
|
||||
docker run -d --rm
|
||||
-p {{ registry_port }}:5000
|
||||
--name "{{ registry_name }}"
|
||||
-v "{{ _tmpfile.path }}:/auth"
|
||||
-e "REGISTRY_AUTH=htpasswd"
|
||||
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm"
|
||||
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/registry.password
|
||||
registry:2
|
||||
register: _setup_registry
|
||||
failed_when: _setup_registry.rc != 0
|
||||
|
||||
- name: Ensure that the registry is running and rechable
|
||||
ansible.builtin.wait_for:
|
||||
host: localhost
|
||||
port: "{{ registry_port }}"
|
||||
|
||||
- name: Test the registry with correct credentials to ensure that the registry is running
|
||||
ansible.builtin.shell: >-
|
||||
echo {{ password | quote }} | helm registry login localhost:{{ registry_port }}
|
||||
-u {{ username }} --password-stdin
|
||||
register: _login_correct
|
||||
failed_when: _login_correct.rc != 0
|
||||
|
||||
- name: Clean up credentials to run test on clean environment
|
||||
ansible.builtin.shell: >-
|
||||
helm registry logout localhost:{{ registry_port }}
|
||||
register: _logout
|
||||
failed_when: _logout.rc != 0
|
||||
|
||||
- name: Create directory for helm chart
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: ".helm"
|
||||
register: _destination
|
||||
|
||||
- name: Pull test helm chart
|
||||
ansible.builtin.uri:
|
||||
url: "{{ test_chart }}"
|
||||
dest: "{{ _destination.path }}/k8s-monitoring-1.6.8.tgz"
|
||||
return_content: no
|
||||
status_code: 200
|
||||
|
||||
- name: Test module helm_registry_auth with correct credentials
|
||||
helm_registry_auth:
|
||||
username: "{{ username }}"
|
||||
password: "{{ password }}"
|
||||
host: localhost:{{ registry_port }}
|
||||
state: present
|
||||
register: _helm_registry_auth_correct
|
||||
|
||||
- name: Assert that the registry is logged in
|
||||
# Helm binary prints the message to stderr, refence: https://github.com/helm/helm/issues/13464
|
||||
assert:
|
||||
that:
|
||||
- "'Login Succeeded' in _helm_registry_auth_correct.stderr"
|
||||
- "'{{ password }}' not in _helm_registry_auth_correct.command"
|
||||
- "'{{ password }}' not in _helm_registry_auth_correct.stdout"
|
||||
- "'{{ password }}' not in _helm_registry_auth_correct.stderr"
|
||||
|
||||
- name: Ensure that push to the registry is working
|
||||
ansible.builtin.shell: >-
|
||||
helm push "{{ _destination.path }}/k8s-monitoring-1.6.8.tgz" oci://localhost:{{ registry_port }}/test/
|
||||
register: _save_chart
|
||||
failed_when: _save_chart.rc != 0
|
||||
|
||||
- name: Assert that the chart is saved
|
||||
# Helm binary prints the message to stderr, refence: https://github.com/helm/helm/issues/13464
|
||||
assert:
|
||||
that: "'Pushed: localhost:{{ registry_port }}/test/k8s-monitoring' in _save_chart.stderr"
|
||||
|
||||
|
||||
- name: Test logout
|
||||
helm_registry_auth:
|
||||
host: localhost:{{ registry_port }}
|
||||
state: absent
|
||||
register: _helm_registry_auth_logout
|
||||
|
||||
- name: Assert logout
|
||||
# Helm binary prints the message to stderr
|
||||
assert:
|
||||
that: "'Removing login credentials' in _helm_registry_auth_logout.stderr"
|
||||
|
||||
- name: Test logout idempotency
|
||||
helm_registry_auth:
|
||||
host: localhost:{{ registry_port }}
|
||||
state: absent
|
||||
register: _helm_registry_auth_logout_idempotency
|
||||
|
||||
- name: Assert logout operation did not report change
|
||||
ansible.builtin.assert:
|
||||
that: _helm_registry_auth_logout_idempotency is not changed
|
||||
|
||||
- name: Ensure that not able to push to the registry
|
||||
ansible.builtin.shell: >-
|
||||
helm push "{{ _destination.path }}/k8s-monitoring-1.6.8.tgz" oci://localhost:{{ registry_port }}/test/
|
||||
register: _save_chart
|
||||
failed_when: _save_chart.rc == 0
|
||||
|
||||
- name: Read content of ~/.config/helm/registry/config.json
|
||||
ansible.builtin.slurp:
|
||||
src: ~/.config/helm/registry/config.json
|
||||
register: _config_json
|
||||
|
||||
- name: Assert that auth data is remove and the chart is not saved
|
||||
# Helm binary prints the message to stderr
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'push access denied' in _save_chart.stderr"
|
||||
- "'authorization failed' in _save_chart.stderr"
|
||||
- "_save_chart.rc != 0"
|
||||
- "'localhost:{{ registry_port }}' not in _config_json.content | b64decode"
|
||||
|
||||
- name: Test module helm_registry_auth with wrong credentials
|
||||
helm_registry_auth:
|
||||
username: "{{ username }}"
|
||||
password: "{{ wrong_password }}"
|
||||
host: localhost:{{ registry_port }}
|
||||
state: present
|
||||
register: _helm_registry_auth_wrong
|
||||
ignore_errors: true
|
||||
|
||||
- name: Read content of ~/.config/helm/registry/config.json
|
||||
ansible.builtin.slurp:
|
||||
src: ~/.config/helm/registry/config.json
|
||||
register: _config_json
|
||||
|
||||
- name: Assert that the registry is not logged in and auth data is not saved
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'401 Unauthorized' in _helm_registry_auth_wrong.stderr"
|
||||
- "'{{ wrong_password }}' not in _helm_registry_auth_correct.command"
|
||||
- "'{{ wrong_password }}' not in _helm_registry_auth_correct.stdout"
|
||||
- "'{{ wrong_password }}' not in _helm_registry_auth_correct.stderr"
|
||||
- "'localhost:{{ registry_port }}' not in _config_json.content | b64decode"
|
||||
|
||||
# Clean up
|
||||
always:
|
||||
- name: Stop and remove the registry
|
||||
ansible.builtin.command: docker stop {{ registry_name }}
|
||||
ignore_errors: true
|
||||
|
||||
- name: Remove the tmpfile
|
||||
ansible.builtin.file:
|
||||
state: absent
|
||||
path: "{{ item }}"
|
||||
force: true
|
||||
loop:
|
||||
- "{{ _tmpfile.path }}"
|
||||
- "{{ _destination.path }}"
|
||||
ignore_errors: true
|
||||
@@ -25,4 +25,6 @@ plugins/modules/k8s_service.py validate-modules:return-syntax-error
|
||||
plugins/modules/k8s_taint.py validate-modules:return-syntax-error
|
||||
tests/integration/targets/k8s_delete/files/deployments.yaml yamllint!skip
|
||||
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
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@ 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
|
||||
|
||||
@@ -29,4 +29,5 @@ 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
|
||||
|
||||
@@ -29,4 +29,5 @@ 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
|
||||
|
||||
@@ -26,4 +26,5 @@ 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
|
||||
|
||||
@@ -26,4 +26,5 @@ 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
|
||||
|
||||
@@ -242,7 +242,7 @@ def test_module_run_helm_command(_ansible_helm_module):
|
||||
assert (rc, out, err) == (0, output, error)
|
||||
|
||||
_ansible_helm_module.run_command.assert_called_once_with(
|
||||
command, environ_update=env_update
|
||||
command, environ_update=env_update, data=None
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
|
||||
with self.assertRaises(AnsibleExitJson) as result:
|
||||
helm_template.main()
|
||||
mock_run_command.assert_called_once_with(
|
||||
"/usr/bin/helm template /tmp/path", environ_update={}
|
||||
"/usr/bin/helm template /tmp/path", environ_update={}, data=None
|
||||
)
|
||||
assert result.exception.args[0]["command"] == "/usr/bin/helm template /tmp/path"
|
||||
|
||||
@@ -74,6 +74,7 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
|
||||
mock_run_command.assert_called_once_with(
|
||||
"/usr/bin/helm template test --repo=https://charts.com/test",
|
||||
environ_update={},
|
||||
data=None,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -95,6 +96,7 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
|
||||
mock_run_command.assert_called_once_with(
|
||||
"/usr/bin/helm template https://charts/example.tgz --dependency-update",
|
||||
environ_update={},
|
||||
data=None,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
|
||||
@@ -88,6 +88,7 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
|
||||
mock_run_command.assert_called_once_with(
|
||||
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
|
||||
environ_update={"HELM_NAMESPACE": "test"},
|
||||
data=None,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -118,6 +119,7 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
|
||||
mock_run_command.assert_called_once_with(
|
||||
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
|
||||
environ_update={"HELM_NAMESPACE": "test"},
|
||||
data=None,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -147,6 +149,7 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
|
||||
call(
|
||||
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
|
||||
environ_update={"HELM_NAMESPACE": "test"},
|
||||
data=None,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -181,6 +184,7 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase):
|
||||
call(
|
||||
"/usr/bin/helm upgrade -i --reset-values test '/tmp/path'",
|
||||
environ_update={"HELM_NAMESPACE": "test"},
|
||||
data=None,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -251,6 +255,7 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase):
|
||||
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,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -280,6 +285,7 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase):
|
||||
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,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -338,6 +344,7 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase):
|
||||
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,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -405,6 +412,7 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase):
|
||||
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,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -433,6 +441,7 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase):
|
||||
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,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
@@ -489,6 +498,7 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase):
|
||||
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,
|
||||
)
|
||||
assert (
|
||||
result.exception.args[0]["command"]
|
||||
|
||||
Reference in New Issue
Block a user