mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-01 04:30:22 -07:00
jenkins_credentials: new module to manage Jenkins credentials (#10170)
Some checks are pending
EOL CI / EOL Sanity (Ⓐ2.16) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py2.7) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.11) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.6) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/3/) (push) Waiting to run
nox / Run extra sanity tests (push) Waiting to run
Some checks are pending
EOL CI / EOL Sanity (Ⓐ2.16) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py2.7) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.11) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.6) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/3/) (push) Waiting to run
nox / Run extra sanity tests (push) Waiting to run
* Added Jenkins credentials module to manage Jenkins credentials * Added Jenkins credentials module to manage Jenkins credentials * Added import error detection, adjusted indentation, and general enhancements. * Added py3 requirement and set files value to avoid errors * Added username to BOTMETA. Switched to format() instead of f strings to support py 2.7, improved delete function, and added function to read private key * Remove redundant message Co-authored-by: Felix Fontein <felix@fontein.de> * Replaced requests with ansible.module_utils.urls, merged check domain and credential functions, and made minor adjustments to documentation * Adjusted for py 2.7 compatibility * Replaced command with state. * Added managing credentials within a folder and made adjustments to documentation * Added unit and integration tests, added token managament, and adjusted documentation. * Added unit and integration tests, added token management, and adjusted documentation.(fix) * Fix BOTMETA.yml * Removed files and generate them at runtime. * moved id and token checks to required_if * Documentation changes, different test setup, and switched to Ansible testing tools * Fixed typos * Correct indentation. Co-authored-by: Felix Fontein <felix@fontein.de> --------- Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
e37cd1a015
commit
52cd104962
12 changed files with 1921 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -781,6 +781,8 @@ files:
|
||||||
maintainers: brettmilford unnecessary-username juanmcasanova
|
maintainers: brettmilford unnecessary-username juanmcasanova
|
||||||
$modules/jenkins_build_info.py:
|
$modules/jenkins_build_info.py:
|
||||||
maintainers: juanmcasanova
|
maintainers: juanmcasanova
|
||||||
|
$modules/jenkins_credential.py:
|
||||||
|
maintainers: YoussefKhalidAli
|
||||||
$modules/jenkins_job.py:
|
$modules/jenkins_job.py:
|
||||||
maintainers: sermilrod
|
maintainers: sermilrod
|
||||||
$modules/jenkins_job_info.py:
|
$modules/jenkins_job_info.py:
|
||||||
|
|
863
plugins/modules/jenkins_credential.py
Normal file
863
plugins/modules/jenkins_credential.py
Normal file
|
@ -0,0 +1,863 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
---
|
||||||
|
module: jenkins_credential
|
||||||
|
short_description: Manage Jenkins credentials and domains via API
|
||||||
|
version_added: 11.1.0
|
||||||
|
description:
|
||||||
|
- This module allows managing Jenkins credentials and domain scopes via the Jenkins HTTP API.
|
||||||
|
- Create, update, and delete different credential types such as C(username/password), C(secret text), C(SSH key), C(certificates), C(GitHub App), and domains.
|
||||||
|
- For scoped domains (O(type=scope)), it supports restrictions based on V(hostname), V(hostname:port), V(path), and V(scheme).
|
||||||
|
requirements:
|
||||||
|
- urllib3 >= 1.26.0
|
||||||
|
author:
|
||||||
|
- Youssef Ali (@YoussefKhalidAli)
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.general.attributes
|
||||||
|
attributes:
|
||||||
|
check_mode:
|
||||||
|
support: full
|
||||||
|
diff_mode:
|
||||||
|
support: none
|
||||||
|
options:
|
||||||
|
id:
|
||||||
|
description:
|
||||||
|
- The ID of the Jenkins credential or domain.
|
||||||
|
type: str
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- Type of the credential or action.
|
||||||
|
choices:
|
||||||
|
- user_and_pass
|
||||||
|
- file
|
||||||
|
- text
|
||||||
|
- github_app
|
||||||
|
- ssh_key
|
||||||
|
- certificate
|
||||||
|
- scope
|
||||||
|
- token
|
||||||
|
type: str
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- The state of the credential.
|
||||||
|
choices:
|
||||||
|
- present
|
||||||
|
- absent
|
||||||
|
default: present
|
||||||
|
type: str
|
||||||
|
scope:
|
||||||
|
description:
|
||||||
|
- Jenkins credential domain scope.
|
||||||
|
- Deleting a domain scope deletes all credentials within it.
|
||||||
|
type: str
|
||||||
|
default: '_'
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- Force update if the credential already exists, used with O(state=present).
|
||||||
|
- If set to V(true), it deletes the existing credential before creating a new one.
|
||||||
|
- Always returns RV(ignore:changed=true).
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
url:
|
||||||
|
description:
|
||||||
|
- Jenkins server URL.
|
||||||
|
type: str
|
||||||
|
default: http://localhost:8080
|
||||||
|
jenkins_user:
|
||||||
|
description:
|
||||||
|
- Jenkins user for authentication.
|
||||||
|
required: true
|
||||||
|
type: str
|
||||||
|
jenkins_password:
|
||||||
|
description:
|
||||||
|
- Jenkins password for token creation. Required if O(type=token).
|
||||||
|
type: str
|
||||||
|
token:
|
||||||
|
description:
|
||||||
|
- Jenkins API token. Required unless O(type=token).
|
||||||
|
type: str
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Description of the credential or domain.
|
||||||
|
default: ''
|
||||||
|
type: str
|
||||||
|
location:
|
||||||
|
description:
|
||||||
|
- Location of the credential. Either V(system) or V(folder).
|
||||||
|
- If O(location=folder) then O(url) must be set to V(<jenkins-server>/job/<folder_name>).
|
||||||
|
choices:
|
||||||
|
- system
|
||||||
|
- folder
|
||||||
|
default: 'system'
|
||||||
|
type: str
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the token to generate. Required if O(type=token).
|
||||||
|
- When generating a new token, do not pass O(id). It is generated automatically.
|
||||||
|
- Creating two tokens with the same name generates two distinct tokens with different RV(token_uuid) values.
|
||||||
|
- Replacing a token with another one of the same name requires deleting the original first using O(force=True).
|
||||||
|
type: str
|
||||||
|
username:
|
||||||
|
description:
|
||||||
|
- Username for credentials types that require it (for example O(type=ssh_key) or O(type=user_and_pass)).
|
||||||
|
type: str
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- Password for credentials types that require it (for example O(type=user_and_passs) or O(type=certificate)).
|
||||||
|
type: str
|
||||||
|
secret:
|
||||||
|
description:
|
||||||
|
- Secret text (used when O(type=text)).
|
||||||
|
type: str
|
||||||
|
appID:
|
||||||
|
description:
|
||||||
|
- GitHub App ID.
|
||||||
|
type: str
|
||||||
|
api_uri:
|
||||||
|
description:
|
||||||
|
- Link to Github API.
|
||||||
|
default: 'https://api.github.com'
|
||||||
|
type: str
|
||||||
|
owner:
|
||||||
|
description:
|
||||||
|
- GitHub App owner.
|
||||||
|
type: str
|
||||||
|
file_path:
|
||||||
|
description:
|
||||||
|
- File path to secret file (for example O(type=file) or O(type=certificate)).
|
||||||
|
- For O(type=certificate), this can be a V(.p12) or V(.pem) file.
|
||||||
|
type: path
|
||||||
|
private_key_path:
|
||||||
|
description:
|
||||||
|
- Path to private key file for PEM certificates or GitHub Apps.
|
||||||
|
type: path
|
||||||
|
passphrase:
|
||||||
|
description:
|
||||||
|
- SSH passphrase if needed.
|
||||||
|
type: str
|
||||||
|
inc_hostname:
|
||||||
|
description:
|
||||||
|
- List of hostnames to include in scope.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
exc_hostname:
|
||||||
|
description:
|
||||||
|
- List of hostnames to exclude from scope.
|
||||||
|
- If a hostname appears in both this list and O(inc_hostname), the hostname is excluded.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
inc_hostname_port:
|
||||||
|
description:
|
||||||
|
- List of V(host:port) to include in scope.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
exc_hostname_port:
|
||||||
|
description:
|
||||||
|
- List of host:port to exclude from scope.
|
||||||
|
- If a hostname and port appears in both this list and O(inc_hostname_port), it is excluded.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
inc_path:
|
||||||
|
description:
|
||||||
|
- List of URL paths to include when matching credentials to domains.
|
||||||
|
- "B(Matching is hierarchical): subpaths of excluded paths are also excluded, even if explicitly included."
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
exc_path:
|
||||||
|
description:
|
||||||
|
- List of URL paths to exclude.
|
||||||
|
- If a path is also matched by O(exc_path), it is excluded.
|
||||||
|
- If you exclude a subpath of a path previously included, that subpath alone is excluded.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
schemes:
|
||||||
|
description:
|
||||||
|
- List of schemes (for example V(http) or V(https)) to match.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
- name: Generate token
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "test-token"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
jenkins_password: "password"
|
||||||
|
type: "token"
|
||||||
|
register: token_result
|
||||||
|
|
||||||
|
- name: Add CUSTOM scope credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "CUSTOM"
|
||||||
|
type: "scope"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Custom scope credential"
|
||||||
|
inc_path:
|
||||||
|
- "include/path"
|
||||||
|
- "include/path2"
|
||||||
|
exc_path:
|
||||||
|
- "exclude/path"
|
||||||
|
- "exclude/path2"
|
||||||
|
inc_hostname:
|
||||||
|
- "included-hostname"
|
||||||
|
- "included-hostname2"
|
||||||
|
exc_hostname:
|
||||||
|
- "excluded-hostname"
|
||||||
|
- "excluded-hostname2"
|
||||||
|
schemes:
|
||||||
|
- "http"
|
||||||
|
- "https"
|
||||||
|
inc_hostname_port:
|
||||||
|
- "included-hostname:7000"
|
||||||
|
- "included-hostname2:7000"
|
||||||
|
exc_hostname_port:
|
||||||
|
- "excluded-hostname:7000"
|
||||||
|
- "excluded-hostname2:7000"
|
||||||
|
|
||||||
|
- name: Add user_and_pass credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "userpass-id"
|
||||||
|
type: "user_and_pass"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "User and password credential"
|
||||||
|
username: "user1"
|
||||||
|
password: "pass1"
|
||||||
|
|
||||||
|
- name: Add file credential to custom scope
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "file-id"
|
||||||
|
type: "file"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
scope: "CUSTOM"
|
||||||
|
description: "File credential"
|
||||||
|
file_path: "../vars/my-secret.pem"
|
||||||
|
|
||||||
|
- name: Add text credential to folder
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "text-id"
|
||||||
|
type: "text"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Text credential"
|
||||||
|
secret: "mysecrettext"
|
||||||
|
location: "folder"
|
||||||
|
url: "http://localhost:8080/job/test"
|
||||||
|
|
||||||
|
- name: Add githubApp credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "githubapp-id"
|
||||||
|
type: "github_app"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "GitHub app credential"
|
||||||
|
appID: "12345"
|
||||||
|
file_path: "../vars/github.pem"
|
||||||
|
owner: "github_owner"
|
||||||
|
|
||||||
|
- name: Add sshKey credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "sshkey-id"
|
||||||
|
type: "ssh_key"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "SSH key credential"
|
||||||
|
username: "sshuser"
|
||||||
|
file_path: "../vars/ssh_key"
|
||||||
|
passphrase: 1234
|
||||||
|
|
||||||
|
- name: Add certificate credential (p12)
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id"
|
||||||
|
type: "certificate"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Certificate credential"
|
||||||
|
password: "12345678901234"
|
||||||
|
file_path: "../vars/certificate.p12"
|
||||||
|
|
||||||
|
- name: Add certificate credential (pem)
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id-pem"
|
||||||
|
type: "certificate"
|
||||||
|
jenkins_user: "admin"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Certificate credential (pem)"
|
||||||
|
file_path: "../vars/cert.pem"
|
||||||
|
private_key_path: "../vars/private.key"
|
||||||
|
"""
|
||||||
|
RETURN = r"""
|
||||||
|
details:
|
||||||
|
description: Return more details in case of errors.
|
||||||
|
type: str
|
||||||
|
returned: failed
|
||||||
|
token:
|
||||||
|
description:
|
||||||
|
- The generated API token if O(type=token).
|
||||||
|
- This is needed to authenticate API calls later.
|
||||||
|
- This should be stored securely, as it is the only time it is returned.
|
||||||
|
type: str
|
||||||
|
returned: success
|
||||||
|
token_uuid:
|
||||||
|
description:
|
||||||
|
- The generated ID of the token.
|
||||||
|
- You pass this value back to the module as O(id) to edit or revoke the token later.
|
||||||
|
- This should be stored securely, as it is the only time it is returned.
|
||||||
|
type: str
|
||||||
|
returned: success
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils.urls import fetch_url, basic_auth_header
|
||||||
|
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||||
|
from ansible_collections.community.general.plugins.module_utils import deps
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
with deps.declare("urllib3", reason="urllib3 is required to embed files into requests"):
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
|
||||||
|
# Function to validate file paths exist on disk
|
||||||
|
def validate_file_exist(module, path):
|
||||||
|
|
||||||
|
if path and not os.path.exists(path):
|
||||||
|
module.fail_json(msg="File not found: {}".format(path))
|
||||||
|
|
||||||
|
|
||||||
|
# Gets the Jenkins crumb for CSRF protection which is required for API calls
|
||||||
|
def get_jenkins_crumb(module, headers):
|
||||||
|
type = module.params["type"]
|
||||||
|
url = module.params["url"]
|
||||||
|
|
||||||
|
if "/job" in url:
|
||||||
|
url = url.split("/job")[0]
|
||||||
|
|
||||||
|
crumb_url = "{}/crumbIssuer/api/json".format(url)
|
||||||
|
|
||||||
|
response, info = fetch_url(module, crumb_url, headers=headers)
|
||||||
|
|
||||||
|
if info["status"] != 200:
|
||||||
|
module.fail_json(msg="Failed to fetch Jenkins crumb. Confirm token is real.")
|
||||||
|
|
||||||
|
# Cookie is needed to generate API token
|
||||||
|
cookie = info.get("set-cookie", "")
|
||||||
|
session_cookie = cookie.split(";")[0] if cookie else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = response.read()
|
||||||
|
json_data = json.loads(data)
|
||||||
|
crumb_request_field = json_data["crumbRequestField"]
|
||||||
|
crumb = json_data["crumb"]
|
||||||
|
headers[crumb_request_field] = crumb # Set the crumb in headers
|
||||||
|
headers["Content-Type"] = (
|
||||||
|
"application/x-www-form-urlencoded" # Set Content-Type for form data
|
||||||
|
)
|
||||||
|
if type == "token":
|
||||||
|
headers["Cookie"] = (
|
||||||
|
session_cookie # Set session cookie for token operations
|
||||||
|
)
|
||||||
|
return crumb_request_field, crumb, session_cookie # Return for test purposes
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Function to clean the data sent via API by removing unwanted keys and None values
|
||||||
|
def clean_data(data):
|
||||||
|
# Keys to remove (including those with None values)
|
||||||
|
keys_to_remove = {
|
||||||
|
"url",
|
||||||
|
"token",
|
||||||
|
"jenkins_user",
|
||||||
|
"jenkins_password",
|
||||||
|
"file_path",
|
||||||
|
"private_key_path",
|
||||||
|
"type",
|
||||||
|
"state",
|
||||||
|
"force",
|
||||||
|
"name",
|
||||||
|
"scope",
|
||||||
|
"location",
|
||||||
|
"api_uri",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter out None values and unwanted keys
|
||||||
|
cleaned_data = {
|
||||||
|
key: value
|
||||||
|
for key, value in data.items()
|
||||||
|
if value is not None and key not in keys_to_remove
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
# Function to check if credentials/domain exists
|
||||||
|
def target_exists(module, check_domain=False):
|
||||||
|
url = module.params["url"]
|
||||||
|
location = module.params["location"]
|
||||||
|
scope = module.params["scope"]
|
||||||
|
name = module.params["id"]
|
||||||
|
user = module.params["jenkins_user"]
|
||||||
|
token = module.params["token"]
|
||||||
|
|
||||||
|
headers = {"Authorization": basic_auth_header(user, token)}
|
||||||
|
|
||||||
|
if module.params["type"] == "scope" or check_domain:
|
||||||
|
target_url = "{}/credentials/store/{}/domain/{}/api/json".format(
|
||||||
|
url, location, scope if check_domain else name
|
||||||
|
)
|
||||||
|
elif module.params["type"] == "token":
|
||||||
|
return False # Can't check token
|
||||||
|
else:
|
||||||
|
target_url = "{}/credentials/store/{}/domain/{}/credential/{}/api/json".format(
|
||||||
|
url, location, scope, name
|
||||||
|
)
|
||||||
|
|
||||||
|
response, info = fetch_url(module, target_url, headers=headers)
|
||||||
|
status = info.get("status", 0)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
return True
|
||||||
|
elif status == 404:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Unexpected status code {} when checking {} existence.".format(
|
||||||
|
status, name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Function to delete the scope or credential provided
|
||||||
|
def delete_target(module, headers):
|
||||||
|
user = module.params["jenkins_user"]
|
||||||
|
type = module.params["type"]
|
||||||
|
url = module.params["url"]
|
||||||
|
location = module.params["location"]
|
||||||
|
id = module.params["id"]
|
||||||
|
scope = module.params["scope"]
|
||||||
|
|
||||||
|
body = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
if type == "token":
|
||||||
|
delete_url = "{}/user/{}/descriptorByName/jenkins.security.ApiTokenProperty/revoke".format(
|
||||||
|
url, user
|
||||||
|
)
|
||||||
|
body = urlencode({"tokenUuid": id})
|
||||||
|
|
||||||
|
elif type == "scope":
|
||||||
|
delete_url = "{}/credentials/store/{}/domain/{}/doDelete".format(
|
||||||
|
url, location, id
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
delete_url = (
|
||||||
|
"{}/credentials/store/{}/domain/{}/credential/{}/doDelete".format(
|
||||||
|
url, location, scope, id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response, info = fetch_url(
|
||||||
|
module,
|
||||||
|
delete_url,
|
||||||
|
headers=headers,
|
||||||
|
data=body if body else None,
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
|
||||||
|
status = info.get("status", 0)
|
||||||
|
if not status == 200:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Failed to delete: HTTP {}, {}, {}".format(
|
||||||
|
status, response, headers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg="Exception during delete: {}".format(str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
# Function to read the private key for types texts and ssh_key
|
||||||
|
def read_privateKey(module):
|
||||||
|
try:
|
||||||
|
with open(module.params["private_key_path"], "r") as f:
|
||||||
|
private_key = f.read().strip()
|
||||||
|
return private_key
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg="Failed to read private key file: {}".format(str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
# Function to builds multipart form-data body and content-type header for file credential upload.
|
||||||
|
# Returns:
|
||||||
|
# body (bytes): Encoded multipart data
|
||||||
|
# content_type (str): Content-Type header including boundary
|
||||||
|
def embed_file_into_body(module, file_path, credentials):
|
||||||
|
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
file_bytes = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg="Failed to read file: {}".format(str(e)))
|
||||||
|
return "", "" # Return for test purposes
|
||||||
|
|
||||||
|
credentials.update(
|
||||||
|
{
|
||||||
|
"file": "file0",
|
||||||
|
"fileName": filename,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {"credentials": credentials}
|
||||||
|
|
||||||
|
fields = {"file0": (filename, file_bytes), "json": json.dumps(payload)}
|
||||||
|
|
||||||
|
body, content_type = urllib3.encode_multipart_formdata(fields)
|
||||||
|
return body, content_type
|
||||||
|
|
||||||
|
|
||||||
|
# Main function to run the Ansible module
|
||||||
|
def run_module():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
id=dict(type="str"),
|
||||||
|
type=dict(
|
||||||
|
type="str",
|
||||||
|
choices=[
|
||||||
|
"user_and_pass",
|
||||||
|
"file",
|
||||||
|
"text",
|
||||||
|
"github_app",
|
||||||
|
"ssh_key",
|
||||||
|
"certificate",
|
||||||
|
"scope",
|
||||||
|
"token",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||||
|
force=dict(type="bool", default=False),
|
||||||
|
scope=dict(type="str", default="_"),
|
||||||
|
url=dict(type="str", default="http://localhost:8080"),
|
||||||
|
jenkins_user=dict(type="str", required=True),
|
||||||
|
jenkins_password=dict(type="str", no_log=True),
|
||||||
|
token=dict(type="str", no_log=True),
|
||||||
|
description=dict(type="str", default=""),
|
||||||
|
location=dict(type="str", default="system", choices=["system", "folder"]),
|
||||||
|
name=dict(type="str"),
|
||||||
|
username=dict(type="str"),
|
||||||
|
password=dict(type="str", no_log=True),
|
||||||
|
file_path=dict(type="path", default=None),
|
||||||
|
secret=dict(type="str", no_log=True),
|
||||||
|
appID=dict(type="str"),
|
||||||
|
api_uri=dict(type="str", default="https://api.github.com"),
|
||||||
|
owner=dict(type="str"),
|
||||||
|
passphrase=dict(type="str", no_log=True),
|
||||||
|
private_key_path=dict(type="path", no_log=True),
|
||||||
|
# Scope specifications parameters
|
||||||
|
inc_hostname=dict(type="list", elements="str"),
|
||||||
|
exc_hostname=dict(type="list", elements="str"),
|
||||||
|
inc_hostname_port=dict(type="list", elements="str"),
|
||||||
|
exc_hostname_port=dict(type="list", elements="str"),
|
||||||
|
inc_path=dict(type="list", elements="str"),
|
||||||
|
exc_path=dict(type="list", elements="str"),
|
||||||
|
schemes=dict(type="list", elements="str"),
|
||||||
|
),
|
||||||
|
supports_check_mode=True,
|
||||||
|
required_if=[
|
||||||
|
("state", "present", ["type"]),
|
||||||
|
("state", "absent", ["id"]),
|
||||||
|
("type", "token", ["name", "jenkins_password"]),
|
||||||
|
("type", "user_and_pass", ["username", "password", "id", "token"]),
|
||||||
|
("type", "file", ["file_path", "id", "token"]),
|
||||||
|
("type", "text", ["secret", "id", "token"]),
|
||||||
|
("type", "github_app", ["appID", "private_key_path", "id", "token"]),
|
||||||
|
("type", "ssh_key", ["username", "private_key_path", "id", "token"]),
|
||||||
|
("type", "certificate", ["file_path", "id", "token"]),
|
||||||
|
("type", "scope", ["id", "token"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
id = module.params["id"]
|
||||||
|
type = module.params["type"]
|
||||||
|
state = module.params["state"]
|
||||||
|
force = module.params["force"]
|
||||||
|
scope = module.params["scope"]
|
||||||
|
url = module.params["url"]
|
||||||
|
jenkins_user = module.params["jenkins_user"]
|
||||||
|
jenkins_password = module.params["jenkins_password"]
|
||||||
|
name = module.params["name"]
|
||||||
|
token = module.params["token"]
|
||||||
|
description = module.params["description"]
|
||||||
|
location = module.params["location"]
|
||||||
|
filePath = module.params["file_path"]
|
||||||
|
private_key_path = module.params["private_key_path"]
|
||||||
|
api_uri = module.params["api_uri"]
|
||||||
|
inc_hostname = module.params["inc_hostname"]
|
||||||
|
exc_hostname = module.params["exc_hostname"]
|
||||||
|
inc_hostname_port = module.params["inc_hostname_port"]
|
||||||
|
exc_hostname_port = module.params["exc_hostname_port"]
|
||||||
|
inc_path = module.params["inc_path"]
|
||||||
|
exc_path = module.params["exc_path"]
|
||||||
|
schemes = module.params["schemes"]
|
||||||
|
|
||||||
|
deps.validate(module)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": basic_auth_header(jenkins_user, token or jenkins_password),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the crumb for CSRF protection
|
||||||
|
get_jenkins_crumb(module, headers)
|
||||||
|
|
||||||
|
result = dict(
|
||||||
|
changed=False,
|
||||||
|
msg="",
|
||||||
|
)
|
||||||
|
|
||||||
|
credentials = clean_data(module.params)
|
||||||
|
|
||||||
|
does_exist = target_exists(module)
|
||||||
|
|
||||||
|
# Check if the credential/domain doesn't exist and the user wants to delete
|
||||||
|
if not does_exist and state == "absent" and not type == "token":
|
||||||
|
result["changed"] = False
|
||||||
|
result["msg"] = "{} does not exist.".format(id)
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
if state == "present":
|
||||||
|
|
||||||
|
# If updating, we need to delete the existing credential/domain first based on force parameter
|
||||||
|
if force and (does_exist or type == "token"):
|
||||||
|
delete_target(module, headers)
|
||||||
|
elif does_exist and not force:
|
||||||
|
result["changed"] = False
|
||||||
|
result["msg"] = "{} already exists. Use force=True to update.".format(id)
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
if type == "token":
|
||||||
|
|
||||||
|
post_url = "{}/user/{}/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken".format(
|
||||||
|
url, jenkins_user
|
||||||
|
)
|
||||||
|
|
||||||
|
body = "newTokenName={}".format(name)
|
||||||
|
|
||||||
|
elif type == "scope":
|
||||||
|
|
||||||
|
post_url = "{}/credentials/store/{}/createDomain".format(url, location)
|
||||||
|
|
||||||
|
specifications = []
|
||||||
|
|
||||||
|
# Create a domain in Jenkins
|
||||||
|
if inc_hostname or exc_hostname:
|
||||||
|
specifications.append(
|
||||||
|
{
|
||||||
|
"stapler-class": "com.cloudbees.plugins.credentials.domains.HostnameSpecification",
|
||||||
|
"includes": ",".join(inc_hostname),
|
||||||
|
"excludes": ",".join(exc_hostname),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if inc_hostname_port or exc_hostname_port:
|
||||||
|
specifications.append(
|
||||||
|
{
|
||||||
|
"stapler-class": "com.cloudbees.plugins.credentials.domains.HostnamePortSpecification",
|
||||||
|
"includes": ",".join(inc_hostname_port),
|
||||||
|
"excludes": ",".join(exc_hostname_port),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if schemes:
|
||||||
|
specifications.append(
|
||||||
|
{
|
||||||
|
"stapler-class": "com.cloudbees.plugins.credentials.domains.SchemeSpecification",
|
||||||
|
"schemes": ",".join(schemes),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if inc_path or exc_path:
|
||||||
|
specifications.append(
|
||||||
|
{
|
||||||
|
"stapler-class": "com.cloudbees.plugins.credentials.domains.PathSpecification",
|
||||||
|
"includes": ",".join(inc_path),
|
||||||
|
"excludes": ",".join(exc_path),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"name": id,
|
||||||
|
"description": description,
|
||||||
|
"specifications": specifications,
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
if filePath:
|
||||||
|
validate_file_exist(module, filePath)
|
||||||
|
elif private_key_path:
|
||||||
|
validate_file_exist(module, private_key_path)
|
||||||
|
|
||||||
|
post_url = "{}/credentials/store/{}/domain/{}/createCredentials".format(
|
||||||
|
url, location, scope
|
||||||
|
)
|
||||||
|
|
||||||
|
cred_class = {
|
||||||
|
"user_and_pass": "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl",
|
||||||
|
"file": "org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl",
|
||||||
|
"text": "org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl",
|
||||||
|
"github_app": "org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials",
|
||||||
|
"ssh_key": "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey",
|
||||||
|
"certificate": "com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl",
|
||||||
|
}
|
||||||
|
credentials.update({"$class": cred_class[type]})
|
||||||
|
|
||||||
|
if type == "file":
|
||||||
|
|
||||||
|
# Build multipart body and content-type
|
||||||
|
body, content_type = embed_file_into_body(module, filePath, credentials)
|
||||||
|
headers["Content-Type"] = content_type
|
||||||
|
|
||||||
|
elif type == "github_app":
|
||||||
|
|
||||||
|
private_key = read_privateKey(module)
|
||||||
|
|
||||||
|
credentials.update(
|
||||||
|
{
|
||||||
|
"privateKey": private_key,
|
||||||
|
"apiUri": api_uri,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif type == "ssh_key":
|
||||||
|
|
||||||
|
private_key = read_privateKey(module)
|
||||||
|
|
||||||
|
credentials.update(
|
||||||
|
{
|
||||||
|
"privateKeySource": {
|
||||||
|
"stapler-class": "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource",
|
||||||
|
"privateKey": private_key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif type == "certificate":
|
||||||
|
|
||||||
|
name, ext = os.path.splitext(filePath)
|
||||||
|
|
||||||
|
if ext.lower() in [".p12", ".pfx"]:
|
||||||
|
try:
|
||||||
|
with open(filePath, "rb") as f:
|
||||||
|
file_content = f.read()
|
||||||
|
uploaded_keystore = base64.b64encode(file_content).decode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Failed to read or encode keystore file: {}".format(
|
||||||
|
str(e)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
credentials.update(
|
||||||
|
{
|
||||||
|
"keyStoreSource": {
|
||||||
|
"$class": "com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl$UploadedKeyStoreSource",
|
||||||
|
"uploadedKeystore": uploaded_keystore,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif ext.lower() in [".pem", ".crt"]: # PEM mode
|
||||||
|
try:
|
||||||
|
with open(filePath, "r") as f:
|
||||||
|
cert_chain = f.read()
|
||||||
|
with open(private_key_path, "r") as f:
|
||||||
|
private_key = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Failed to read PEM files: {}".format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
|
credentials.update(
|
||||||
|
{
|
||||||
|
"keyStoreSource": {
|
||||||
|
"$class": "com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl$PEMEntryKeyStoreSource",
|
||||||
|
"certChain": cert_chain,
|
||||||
|
"privateKey": private_key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Unsupported certificate file type. Only .p12, .pfx, .pem or .crt are supported."
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {"credentials": credentials}
|
||||||
|
|
||||||
|
if not type == "file" and not type == "token":
|
||||||
|
body = urlencode({"json": json.dumps(payload)})
|
||||||
|
|
||||||
|
else: # Delete
|
||||||
|
|
||||||
|
delete_target(module, headers)
|
||||||
|
|
||||||
|
module.exit_json(changed=True, msg="{} deleted successfully.".format(id))
|
||||||
|
|
||||||
|
if (
|
||||||
|
not type == "scope" and not scope == "_"
|
||||||
|
): # Check if custom scope exists if adding to a custom scope
|
||||||
|
if not target_exists(module, True):
|
||||||
|
module.fail_json(msg="Domain {} doesn't exists".format(scope))
|
||||||
|
|
||||||
|
try:
|
||||||
|
response, info = fetch_url(
|
||||||
|
module, post_url, headers=headers, data=body, method="POST"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg="Request to {} failed: {}".format(post_url, str(e)))
|
||||||
|
|
||||||
|
status = info.get("status", 0)
|
||||||
|
|
||||||
|
if not status == 200:
|
||||||
|
body = response.read() if response else b""
|
||||||
|
module.fail_json(
|
||||||
|
msg="Failed to {} credential".format(
|
||||||
|
"add/update" if state == "present" else "delete"
|
||||||
|
),
|
||||||
|
details=body.decode("utf-8", errors="ignore"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if type == "token":
|
||||||
|
response_data = json.loads(response.read())
|
||||||
|
result["token"] = response_data["data"]["tokenValue"]
|
||||||
|
result["token_uuid"] = response_data["data"]["tokenUuid"]
|
||||||
|
|
||||||
|
result["changed"] = True
|
||||||
|
result["msg"] = response.read().decode("utf-8")
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_module()
|
16
tests/integration/targets/jenkins_credential/README.md
Normal file
16
tests/integration/targets/jenkins_credential/README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!--
|
||||||
|
Copyright (c) Ansible Project
|
||||||
|
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
The integration test can be performed as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 1. Start docker-compose:
|
||||||
|
docker-compose -f tests/integration/targets/jenkins_credential/docker-compose.yml down
|
||||||
|
docker-compose -f tests/integration/targets/jenkins_credential/docker-compose.yml up -d
|
||||||
|
|
||||||
|
# 2. Run the integration tests:
|
||||||
|
ansible-test integration jenkins_credential --allow-unsupported -v
|
||||||
|
```
|
5
tests/integration/targets/jenkins_credential/aliases
Normal file
5
tests/integration/targets/jenkins_credential/aliases
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
unsupported
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
jenkins:
|
||||||
|
image: bitnami/jenkins
|
||||||
|
container_name: jenkins-test
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
JENKINS_USERNAME: "FishLegs"
|
||||||
|
JENKINS_PASSWORD: "MeatLug"
|
||||||
|
JENKINS_PLUGINS: "credentials,cloudbees-folder,plain-credentials,github-branch-source,github-api,scm-api,workflow-step-api"
|
||||||
|
healthcheck:
|
||||||
|
test: curl -s http://localhost:8080/login || exit 1
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
169
tests/integration/targets/jenkins_credential/tasks/add.yml
Normal file
169
tests/integration/targets/jenkins_credential/tasks/add.yml
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
- name: Add CUSTOM scope (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "CUSTOM"
|
||||||
|
type: "scope"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Custom scope credential"
|
||||||
|
inc_path:
|
||||||
|
- "include/path"
|
||||||
|
- "include/path2"
|
||||||
|
exc_path:
|
||||||
|
- "exclude/path"
|
||||||
|
- "exclude/path2"
|
||||||
|
inc_hostname:
|
||||||
|
- "included-hostname"
|
||||||
|
- "included-hostname2"
|
||||||
|
exc_hostname:
|
||||||
|
- "excluded-hostname"
|
||||||
|
- "excluded-hostname2"
|
||||||
|
schemes:
|
||||||
|
- "http"
|
||||||
|
- "https"
|
||||||
|
inc_hostname_port:
|
||||||
|
- "included-hostname:7000"
|
||||||
|
- "included-hostname2:7000"
|
||||||
|
exc_hostname_port:
|
||||||
|
- "excluded-hostname:7000"
|
||||||
|
- "excluded-hostname2:7000"
|
||||||
|
register: custom_scope
|
||||||
|
|
||||||
|
- name: Assert CUSTOM scope changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- custom_scope.changed == (run_number == 1)
|
||||||
|
fail_msg: "CUSTOM scope changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "CUSTOM scope behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Add user_and_pass credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "userpass-id"
|
||||||
|
type: "user_and_pass"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "User and password credential"
|
||||||
|
username: "user1"
|
||||||
|
password: "pass1"
|
||||||
|
register: userpass_cred
|
||||||
|
|
||||||
|
- name: Assert user_and_pass changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- userpass_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "user_and_pass credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "user_and_pass credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Add file credential to custom scope (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "file-id"
|
||||||
|
type: "file"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
scope: "CUSTOM"
|
||||||
|
description: "File credential"
|
||||||
|
file_path: "{{ output_dir }}/my-secret.pem"
|
||||||
|
register: file_cred
|
||||||
|
|
||||||
|
- name: Assert file credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- file_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "file credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "file credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Add text credential to folder (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "text-id"
|
||||||
|
type: "text"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Text credential"
|
||||||
|
secret: "mysecrettext"
|
||||||
|
location: "folder"
|
||||||
|
url: "http://localhost:8080/job/test"
|
||||||
|
register: text_cred
|
||||||
|
|
||||||
|
- name: Assert text credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- text_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "text credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "text credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Add githubApp credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "githubapp-id"
|
||||||
|
type: "github_app"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "GitHub app credential"
|
||||||
|
appID: "12345"
|
||||||
|
private_key_path: "{{ output_dir }}/github.pem"
|
||||||
|
owner: "github_owner"
|
||||||
|
register: githubapp_cred
|
||||||
|
|
||||||
|
- name: Assert githubApp credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- githubapp_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "githubApp credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "githubApp credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Add sshKey credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "sshkey-id"
|
||||||
|
type: "ssh_key"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "SSH key credential"
|
||||||
|
username: "sshuser"
|
||||||
|
private_key_path: "{{ output_dir }}/ssh_key"
|
||||||
|
passphrase: 1234
|
||||||
|
register: sshkey_cred
|
||||||
|
|
||||||
|
- name: Assert sshKey credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- sshkey_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "sshKey credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "sshKey credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Add certificate (p12) credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id"
|
||||||
|
type: "certificate"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Certificate credential"
|
||||||
|
password: "12345678901234"
|
||||||
|
file_path: "{{ output_dir }}/certificate.p12"
|
||||||
|
register: cert_p12_cred
|
||||||
|
|
||||||
|
- name: Assert certificate (p12) credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cert_p12_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "certificate (p12) credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "certificate (p12) credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Add certificate (pem) credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id-pem"
|
||||||
|
type: "certificate"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "Certificate credential (pem)"
|
||||||
|
file_path: "{{ output_dir }}/cert.pem"
|
||||||
|
private_key_path: "{{ output_dir }}/private.key"
|
||||||
|
register: cert_pem_cred
|
||||||
|
|
||||||
|
- name: Assert certificate (pem) credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cert_pem_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "certificate (pem) credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "certificate (pem) credential behaved correctly on run {{ run_number }}"
|
128
tests/integration/targets/jenkins_credential/tasks/del.yml
Normal file
128
tests/integration/targets/jenkins_credential/tasks/del.yml
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
- name: Delete user_and_pass credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "userpass-id"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
state: "absent"
|
||||||
|
register: userpass_cred
|
||||||
|
|
||||||
|
- name: Assert user_and_pass changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- userpass_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "user_and_pass credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "user_and_pass credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Delete file credential to custom scope (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "file-id"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
scope: "CUSTOM"
|
||||||
|
state: "absent"
|
||||||
|
register: file_cred
|
||||||
|
|
||||||
|
- name: Assert file credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- file_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "file credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "file credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Delete CUSTOM scope credential (run {{ run_number}})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "CUSTOM"
|
||||||
|
type: "scope"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
state: "absent"
|
||||||
|
register: custom_scope
|
||||||
|
|
||||||
|
- name: Assert CUSTOM scope changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- custom_scope.changed == (run_number == 1)
|
||||||
|
fail_msg: "CUSTOM scope changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "CUSTOM scope behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Delete text credential to folder (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "text-id"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
state: "absent"
|
||||||
|
location: "folder"
|
||||||
|
url: "http://localhost:8080/job/test"
|
||||||
|
register: text_cred
|
||||||
|
|
||||||
|
- name: Assert text credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- text_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "text credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "text credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Delete githubApp credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "githubapp-id"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
state: "absent"
|
||||||
|
register: githubapp_cred
|
||||||
|
|
||||||
|
- name: Assert githubApp credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- githubapp_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "githubApp credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "githubApp credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Delete sshKey credential (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "sshkey-id"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "SSH key credential"
|
||||||
|
state: "absent"
|
||||||
|
register: sshkey_cred
|
||||||
|
|
||||||
|
- name: Assert sshKey credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- sshkey_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "sshKey credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "sshKey credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Delete certificate credential (p12) (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
state: "absent"
|
||||||
|
register: cert_p12_cred
|
||||||
|
|
||||||
|
- name: Assert certificate (p12) credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cert_p12_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "certificate (p12) credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "certificate (p12) credential behaved correctly on run {{ run_number }}"
|
||||||
|
|
||||||
|
- name: Delete certificate credential (pem) (run {{ run_number }})
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id-pem"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
state: "absent"
|
||||||
|
register: cert_pem_cred
|
||||||
|
|
||||||
|
- name: Assert certificate (pem) credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cert_pem_cred.changed == (run_number == 1)
|
||||||
|
fail_msg: "certificate (pem) credential changed status incorrect on run {{ run_number }}"
|
||||||
|
success_msg: "certificate (pem) credential behaved correctly on run {{ run_number }}"
|
192
tests/integration/targets/jenkins_credential/tasks/edit.yml
Normal file
192
tests/integration/targets/jenkins_credential/tasks/edit.yml
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
- name: Generate token
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "{{ tokenUuid}}"
|
||||||
|
name: "test-token-2"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
jenkins_password: "{{ jenkins_password }}"
|
||||||
|
type: "token"
|
||||||
|
force: yes
|
||||||
|
register: token_result
|
||||||
|
|
||||||
|
- name: Set token in vars
|
||||||
|
set_fact:
|
||||||
|
token: "{{ token_result.token }}"
|
||||||
|
tokenUuid: "{{ token_result.token_uuid }}"
|
||||||
|
|
||||||
|
- name: Edit CUSTOM scope credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "CUSTOM"
|
||||||
|
type: "scope"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "New custom scope credential"
|
||||||
|
inc_path:
|
||||||
|
- "new_include/path"
|
||||||
|
- "new_include/path2"
|
||||||
|
exc_path:
|
||||||
|
- "new_exclude/path"
|
||||||
|
- "new_exclude/path2"
|
||||||
|
inc_hostname:
|
||||||
|
- "new_included-hostname"
|
||||||
|
- "new_included-hostname2"
|
||||||
|
exc_hostname:
|
||||||
|
- "new_excluded-hostname"
|
||||||
|
- "new_excluded-hostname2"
|
||||||
|
schemes:
|
||||||
|
- "new_http"
|
||||||
|
- "new_https"
|
||||||
|
inc_hostname_port:
|
||||||
|
- "new_included-hostname:7000"
|
||||||
|
- "new_included-hostname2:7000"
|
||||||
|
exc_hostname_port:
|
||||||
|
- "new_excluded-hostname:7000"
|
||||||
|
- "new_excluded-hostname2:7000"
|
||||||
|
force: yes
|
||||||
|
register: custom_scope
|
||||||
|
|
||||||
|
- name: Assert CUSTOM scope changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- custom_scope.changed == true
|
||||||
|
fail_msg: "CUSTOM scope changed status when it shouldn't"
|
||||||
|
success_msg: "CUSTOM scope behaved correctly"
|
||||||
|
|
||||||
|
- name: Edit user_and_pass credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "userpass-id"
|
||||||
|
type: "user_and_pass"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "new user and password credential"
|
||||||
|
username: "user2"
|
||||||
|
password: "pass2"
|
||||||
|
force: yes
|
||||||
|
register: userpass_cred
|
||||||
|
|
||||||
|
- name: Assert user_and_pass changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- userpass_cred.changed == true
|
||||||
|
fail_msg: "user_and_pass credential changed status incorrect"
|
||||||
|
success_msg: "user_and_pass credential behaved correctly"
|
||||||
|
|
||||||
|
- name: Edit file credential to custom scope
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "file-id"
|
||||||
|
type: "file"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
scope: "CUSTOM"
|
||||||
|
description: "New file credential"
|
||||||
|
file_path: "{{ output_dir }}/my-secret.pem"
|
||||||
|
force: yes
|
||||||
|
register: file_cred
|
||||||
|
|
||||||
|
- name: Assert file credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- file_cred.changed == true
|
||||||
|
fail_msg: "file credential changed status incorrect"
|
||||||
|
success_msg: "file credential behaved correctly"
|
||||||
|
|
||||||
|
- name: Edit text credential to folder
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "text-id"
|
||||||
|
type: "text"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "New text credential"
|
||||||
|
secret: "mynewsecrettext"
|
||||||
|
location: "folder"
|
||||||
|
url: "http://localhost:8080/job/test"
|
||||||
|
force: yes
|
||||||
|
register: text_cred
|
||||||
|
|
||||||
|
- name: Assert text credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- text_cred.changed == true
|
||||||
|
fail_msg: "text credential changed status incorrect"
|
||||||
|
success_msg: "text credential behaved correctly"
|
||||||
|
|
||||||
|
- name: Edit githubApp credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "githubapp-id"
|
||||||
|
type: "github_app"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "New GitHub app credential"
|
||||||
|
appID: "12345678"
|
||||||
|
private_key_path: "{{ output_dir }}/github.pem"
|
||||||
|
owner: "new_github_owner"
|
||||||
|
force: yes
|
||||||
|
register: githubapp_cred
|
||||||
|
|
||||||
|
- name: Assert githubApp credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- githubapp_cred.changed == true
|
||||||
|
fail_msg: "githubApp credential changed status incorrect"
|
||||||
|
success_msg: "githubApp credential behaved correctly"
|
||||||
|
|
||||||
|
- name: Edit sshKey credential
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "sshkey-id"
|
||||||
|
type: "ssh_key"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "New SSH key credential"
|
||||||
|
username: "new_sshuser"
|
||||||
|
private_key_path: "{{ output_dir }}/ssh_key"
|
||||||
|
passphrase: 1234
|
||||||
|
force: yes
|
||||||
|
register: sshkey_cred
|
||||||
|
|
||||||
|
- name: Assert sshKey credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- sshkey_cred.changed == true
|
||||||
|
fail_msg: "sshKey credential changed status incorrect"
|
||||||
|
success_msg: "sshKey credential behaved correctly"
|
||||||
|
|
||||||
|
- name: Edit certificate credential (p12)
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id"
|
||||||
|
type: "certificate"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "New certificate credential"
|
||||||
|
password: "12345678901234"
|
||||||
|
file_path: "{{ output_dir }}/certificate.p12"
|
||||||
|
force: yes
|
||||||
|
register: cert_p12_cred
|
||||||
|
|
||||||
|
- name: Assert certificate (p12) credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cert_p12_cred.changed == true
|
||||||
|
fail_msg: "certificate (p12) credential changed status incorrect"
|
||||||
|
success_msg: "certificate (p12) credential behaved correctly"
|
||||||
|
|
||||||
|
- name: Edit certificate credential (pem)
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "certificate-id-pem"
|
||||||
|
type: "certificate"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
token: "{{ token }}"
|
||||||
|
description: "New certificate credential (pem)"
|
||||||
|
file_path: "{{ output_dir }}/cert.pem"
|
||||||
|
private_key_path: "{{ output_dir }}/private.key"
|
||||||
|
force: yes
|
||||||
|
register: cert_pem_cred
|
||||||
|
|
||||||
|
- name: Assert certificate (pem) credential changed value
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cert_pem_cred.changed == true
|
||||||
|
fail_msg: "certificate (pem) credential changed status incorrect"
|
||||||
|
success_msg: "certificate (pem) credential behaved correctly"
|
79
tests/integration/targets/jenkins_credential/tasks/main.yml
Normal file
79
tests/integration/targets/jenkins_credential/tasks/main.yml
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
- name: Prepare the test environment
|
||||||
|
include_tasks: pre.yml
|
||||||
|
vars:
|
||||||
|
output_dir: "{{ playbook_dir }}/generated"
|
||||||
|
|
||||||
|
- name: Generate token
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
name: "test-token"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
jenkins_password: "{{ jenkins_password }}"
|
||||||
|
type: "token"
|
||||||
|
no_log: yes
|
||||||
|
register: token_result
|
||||||
|
|
||||||
|
- name: Assert token and tokenUuid are returned
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- token_result.token is defined
|
||||||
|
- token_result.token_uuid is defined
|
||||||
|
fail_msg: "Token generation failed"
|
||||||
|
success_msg: "Token and tokenUuid successfully returned"
|
||||||
|
|
||||||
|
- name: Set token facts
|
||||||
|
set_fact:
|
||||||
|
token: "{{ token_result.token }}"
|
||||||
|
tokenUuid: "{{ token_result.token_uuid }}"
|
||||||
|
|
||||||
|
- name: Test adding new credentials and scopes
|
||||||
|
include_tasks: add.yml
|
||||||
|
vars:
|
||||||
|
run_number: 1
|
||||||
|
output_dir: "{{ playbook_dir }}/generated"
|
||||||
|
|
||||||
|
- name: Test adding credentials and scopes when they already exist
|
||||||
|
include_tasks: add.yml
|
||||||
|
vars:
|
||||||
|
run_number: 2
|
||||||
|
output_dir: "{{ playbook_dir }}/generated"
|
||||||
|
|
||||||
|
- name: Test editing credentials
|
||||||
|
include_tasks: edit.yml
|
||||||
|
vars:
|
||||||
|
output_dir: "{{ playbook_dir }}/generated"
|
||||||
|
|
||||||
|
- name: Test deleting credentials and scopes
|
||||||
|
include_tasks: del.yml
|
||||||
|
vars:
|
||||||
|
run_number: 1
|
||||||
|
|
||||||
|
- name: Test deleting credentials and scopes when they don't exist
|
||||||
|
include_tasks: del.yml
|
||||||
|
vars:
|
||||||
|
run_number: 2
|
||||||
|
|
||||||
|
- name: Delete token
|
||||||
|
community.general.jenkins_credential:
|
||||||
|
id: "{{ tokenUuid }}"
|
||||||
|
name: "test-token-2"
|
||||||
|
jenkins_user: "{{ jenkins_username }}"
|
||||||
|
jenkins_password: "{{ jenkins_password }}"
|
||||||
|
state: "absent"
|
||||||
|
type: "token"
|
||||||
|
register: delete_token_result
|
||||||
|
|
||||||
|
- name: Assert token deletion
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- delete_token_result.changed is true
|
||||||
|
fail_msg: "Token deletion failed"
|
||||||
|
success_msg: "Token successfully deleted"
|
||||||
|
|
||||||
|
- name: Remove generated test files
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ playbook_dir }}/generated"
|
||||||
|
state: absent
|
92
tests/integration/targets/jenkins_credential/tasks/pre.yml
Normal file
92
tests/integration/targets/jenkins_credential/tasks/pre.yml
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
- name: Include Jenkins user variables
|
||||||
|
include_vars: "{{ role_path }}/vars/credentials.yml"
|
||||||
|
|
||||||
|
- name: Make sure Jenkins is ready
|
||||||
|
uri:
|
||||||
|
url: http://localhost:8080/login
|
||||||
|
status_code: 200
|
||||||
|
return_content: no
|
||||||
|
timeout: 30
|
||||||
|
register: result
|
||||||
|
retries: 10
|
||||||
|
delay: 5
|
||||||
|
until: result.status == 200
|
||||||
|
|
||||||
|
- name: Get Jenkins crumb and save cookie
|
||||||
|
shell: |
|
||||||
|
curl -s -c cookies.txt -u FishLegs:MeatLug http://localhost:8080/crumbIssuer/api/json > crumb.json
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
|
||||||
|
- name: Read crumb value
|
||||||
|
set_fact:
|
||||||
|
crumb_data: "{{ lookup('file', 'crumb.json') | from_json }}"
|
||||||
|
|
||||||
|
- name: Create Jenkins folder 'test'
|
||||||
|
shell: |
|
||||||
|
curl -b cookies.txt -u {{ jenkins_username }}:{{ jenkins_password }} \
|
||||||
|
-H "{{ crumb_data.crumbRequestField }}: {{ crumb_data.crumb }}" \
|
||||||
|
-H "Content-Type: application/xml" \
|
||||||
|
--data-binary @- http://localhost:8080/createItem?name=test <<EOF
|
||||||
|
<com.cloudbees.hudson.plugins.folder.Folder plugin="cloudbees-folder@6.15">
|
||||||
|
<description>Test Folder</description>
|
||||||
|
<properties/>
|
||||||
|
</com.cloudbees.hudson.plugins.folder.Folder>
|
||||||
|
EOF
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
|
||||||
|
- name: Create output directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ output_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Generate private key
|
||||||
|
community.crypto.openssl_privatekey:
|
||||||
|
path: "{{ output_dir }}/private.key"
|
||||||
|
size: 2048
|
||||||
|
type: RSA
|
||||||
|
|
||||||
|
- name: Generate CSR (certificate signing request)
|
||||||
|
community.crypto.openssl_csr:
|
||||||
|
path: "{{ output_dir }}/request.csr"
|
||||||
|
privatekey_path: "{{ output_dir }}/private.key"
|
||||||
|
common_name: "dummy.local"
|
||||||
|
|
||||||
|
- name: Generate self-signed certificate
|
||||||
|
community.crypto.x509_certificate:
|
||||||
|
path: "{{ output_dir }}/cert.pem"
|
||||||
|
privatekey_path: "{{ output_dir }}/private.key"
|
||||||
|
csr_path: "{{ output_dir }}/request.csr"
|
||||||
|
provider: selfsigned
|
||||||
|
|
||||||
|
- name: Create PKCS#12 (.p12) file
|
||||||
|
community.crypto.openssl_pkcs12:
|
||||||
|
path: "{{ output_dir }}/certificate.p12"
|
||||||
|
privatekey_path: "{{ output_dir }}/private.key"
|
||||||
|
certificate_path: "{{ output_dir }}/cert.pem"
|
||||||
|
friendly_name: "dummy-cert"
|
||||||
|
passphrase: "12345678901234"
|
||||||
|
|
||||||
|
- name: Copy cert.pem to github.pem
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ output_dir }}/cert.pem"
|
||||||
|
dest: "{{ output_dir }}/github.pem"
|
||||||
|
remote_src: true
|
||||||
|
|
||||||
|
- name: Copy private.key to my-secret.pem
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ output_dir }}/private.key"
|
||||||
|
dest: "{{ output_dir }}/my-secret.pem"
|
||||||
|
remote_src: true
|
||||||
|
|
||||||
|
- name: Generate dummy SSH key
|
||||||
|
community.crypto.openssh_keypair:
|
||||||
|
path: "{{ output_dir }}/ssh_key"
|
||||||
|
type: rsa
|
||||||
|
size: 2048
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
jenkins_username: FishLegs
|
||||||
|
jenkins_password: MeatLug
|
348
tests/unit/plugins/modules/test_jenkins_credential.py
Normal file
348
tests/unit/plugins/modules/test_jenkins_credential.py
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
# Copyright (c) Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
from ansible_collections.community.general.plugins.modules import jenkins_credential
|
||||||
|
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import (
|
||||||
|
MagicMock,
|
||||||
|
patch,
|
||||||
|
mock_open,
|
||||||
|
)
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
import builtins
|
||||||
|
open_path = "builtins.open"
|
||||||
|
else:
|
||||||
|
import __builtin__ as builtins
|
||||||
|
open_path = "__builtin__.open"
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_file_exist_passes_when_file_exists():
|
||||||
|
module = MagicMock()
|
||||||
|
with patch("os.path.exists", return_value=True):
|
||||||
|
jenkins_credential.validate_file_exist(module, "/some/file/path")
|
||||||
|
module.fail_json.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_file_exist_fails_when_file_missing():
|
||||||
|
module = MagicMock()
|
||||||
|
with patch("os.path.exists", return_value=False):
|
||||||
|
jenkins_credential.validate_file_exist(module, "/missing/file/path")
|
||||||
|
module.fail_json.assert_called_once_with(
|
||||||
|
msg="File not found: /missing/file/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_get_jenkins_crumb_sets_crumb_header(fetch_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {"type": "file", "url": "http://localhost:8080"}
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
fake_response = MagicMock()
|
||||||
|
fake_response.read.return_value = json.dumps(
|
||||||
|
{"crumbRequestField": "crumb_field", "crumb": "abc123"}
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
fetch_mock.return_value = (
|
||||||
|
fake_response,
|
||||||
|
{"status": 200, "set-cookie": "JSESSIONID=something; Path=/"},
|
||||||
|
)
|
||||||
|
|
||||||
|
crumb_request_field, crumb, session_coockie = jenkins_credential.get_jenkins_crumb(
|
||||||
|
module, headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Cookie" not in headers
|
||||||
|
assert "crumb_field" in headers
|
||||||
|
assert crumb == "abc123"
|
||||||
|
assert headers[crumb_request_field] == crumb
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_get_jenkins_crumb_sets_cookie_if_type_token(fetch_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {"type": "token", "url": "http://localhost:8080"}
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
fake_response = MagicMock()
|
||||||
|
fake_response.read.return_value = json.dumps(
|
||||||
|
{"crumbRequestField": "crumb_field", "crumb": "secure"}
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
fetch_mock.return_value = (
|
||||||
|
fake_response,
|
||||||
|
{"status": 200, "set-cookie": "JSESSIONID=token-cookie; Path=/"},
|
||||||
|
)
|
||||||
|
|
||||||
|
crumb_request_field, crumb, session_cookie = jenkins_credential.get_jenkins_crumb(
|
||||||
|
module, headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "crumb_field" in headers
|
||||||
|
assert crumb == "secure"
|
||||||
|
assert headers[crumb_request_field] == crumb
|
||||||
|
assert headers["Cookie"] == session_cookie
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_get_jenkins_crumb_fails_on_non_200_status(fetch_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {"type": "file", "url": "http://localhost:8080"}
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
fetch_mock.return_value = (MagicMock(), {"status": 403})
|
||||||
|
|
||||||
|
jenkins_credential.get_jenkins_crumb(module, headers)
|
||||||
|
|
||||||
|
module.fail_json.assert_called_once()
|
||||||
|
assert "Failed to fetch Jenkins crumb" in module.fail_json.call_args[1]["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_get_jenkins_crumb_removes_job_from_url(fetch_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {"type": "file", "url": "http://localhost:8080/job/test"}
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
fake_response = MagicMock()
|
||||||
|
fake_response.read.return_value = json.dumps(
|
||||||
|
{"crumbRequestField": "Jenkins-Crumb", "crumb": "xyz"}
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
fetch_mock.return_value = (fake_response, {"status": 200, "set-cookie": ""})
|
||||||
|
|
||||||
|
jenkins_credential.get_jenkins_crumb(module, headers)
|
||||||
|
|
||||||
|
url_called = fetch_mock.call_args[0][1]
|
||||||
|
assert url_called == "http://localhost:8080/crumbIssuer/api/json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_data_removes_extraneous_fields():
|
||||||
|
data = {
|
||||||
|
"id": "cred1",
|
||||||
|
"description": "test",
|
||||||
|
"jenkins_user": "admin",
|
||||||
|
"token": "secret",
|
||||||
|
"url": "http://localhost:8080",
|
||||||
|
"file_path": None,
|
||||||
|
}
|
||||||
|
expected = {"id": "cred1", "description": "test"}
|
||||||
|
result = jenkins_credential.clean_data(data)
|
||||||
|
assert result == expected, "Expected {}, got {}".format(expected, result)
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_target_exists_returns_true_on_200(fetch_url_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {
|
||||||
|
"url": "http://localhost:8080",
|
||||||
|
"location": "system",
|
||||||
|
"scope": "_",
|
||||||
|
"id": "my-id",
|
||||||
|
"jenkins_user": "admin",
|
||||||
|
"token": "secret",
|
||||||
|
"type": "file",
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_url_mock.return_value = (MagicMock(), {"status": 200})
|
||||||
|
assert jenkins_credential.target_exists(module) is True
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_target_exists_returns_false_on_404(fetch_url_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {
|
||||||
|
"url": "http://localhost:8080",
|
||||||
|
"location": "system",
|
||||||
|
"scope": "_",
|
||||||
|
"id": "my-id",
|
||||||
|
"jenkins_user": "admin",
|
||||||
|
"token": "secret",
|
||||||
|
"type": "file",
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_url_mock.return_value = (MagicMock(), {"status": 404})
|
||||||
|
assert jenkins_credential.target_exists(module) is False
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_target_exists_calls_fail_json_on_unexpected_status(fetch_url_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {
|
||||||
|
"url": "http://localhost:8080",
|
||||||
|
"location": "system",
|
||||||
|
"scope": "_",
|
||||||
|
"id": "my-id",
|
||||||
|
"jenkins_user": "admin",
|
||||||
|
"token": "secret",
|
||||||
|
"type": "file",
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_url_mock.return_value = (MagicMock(), {"status": 500})
|
||||||
|
jenkins_credential.target_exists(module)
|
||||||
|
module.fail_json.assert_called_once()
|
||||||
|
assert "Unexpected status code" in module.fail_json.call_args[1]["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_target_exists_skips_check_for_token_type(fetch_url_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {
|
||||||
|
"type": "token",
|
||||||
|
"url": "ignored",
|
||||||
|
"location": "ignored",
|
||||||
|
"scope": "ignored",
|
||||||
|
"id": "ignored",
|
||||||
|
"jenkins_user": "ignored",
|
||||||
|
"token": "ignored",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert jenkins_credential.target_exists(module) is False
|
||||||
|
fetch_url_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
||||||
|
)
|
||||||
|
def test_delete_target_fails_deleting(fetch_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {
|
||||||
|
"type": "token",
|
||||||
|
"jenkins_user": "admin",
|
||||||
|
"url": "http://localhost:8080",
|
||||||
|
"id": "token-id",
|
||||||
|
"location": "system",
|
||||||
|
"scope": "_",
|
||||||
|
}
|
||||||
|
headers = {"Authorization": "Basic abc", "Content-Type": "whatever"}
|
||||||
|
|
||||||
|
fetch_mock.return_value = (MagicMock(), {"status": 500})
|
||||||
|
|
||||||
|
jenkins_credential.delete_target(module, headers)
|
||||||
|
|
||||||
|
module.fail_json.assert_called_once()
|
||||||
|
assert "Failed to delete" in module.fail_json.call_args[1]["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url",
|
||||||
|
side_effect=Exception("network error"),
|
||||||
|
)
|
||||||
|
def test_delete_target_raises_exception(fetch_mock):
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {
|
||||||
|
"type": "scope",
|
||||||
|
"jenkins_user": "admin",
|
||||||
|
"location": "system",
|
||||||
|
"url": "http://localhost:8080",
|
||||||
|
"id": "domain-id",
|
||||||
|
"scope": "_",
|
||||||
|
}
|
||||||
|
headers = {"Authorization": "Basic auth"}
|
||||||
|
|
||||||
|
jenkins_credential.delete_target(module, headers)
|
||||||
|
|
||||||
|
module.fail_json.assert_called_once()
|
||||||
|
assert "Exception during delete" in module.fail_json.call_args[1]["msg"]
|
||||||
|
assert "network error" in module.fail_json.call_args[1]["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_privateKey_returns_trimmed_contents():
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {"private_key_path": "/fake/path/key.pem"}
|
||||||
|
|
||||||
|
mocked_file = mock_open(
|
||||||
|
read_data="\n \t -----BEGIN PRIVATE KEY-----\nKEYDATA\n-----END PRIVATE KEY----- \n\n"
|
||||||
|
)
|
||||||
|
with patch(open_path, mocked_file):
|
||||||
|
result = jenkins_credential.read_privateKey(module)
|
||||||
|
|
||||||
|
expected = "-----BEGIN PRIVATE KEY-----\nKEYDATA\n-----END PRIVATE KEY-----"
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
mocked_file.assert_called_once_with("/fake/path/key.pem", "r")
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_privateKey_handles_file_read_error():
|
||||||
|
module = MagicMock()
|
||||||
|
module.params = {"private_key_path": "/invalid/path.pem"}
|
||||||
|
|
||||||
|
with patch(open_path, side_effect=IOError("cannot read file")):
|
||||||
|
jenkins_credential.read_privateKey(module)
|
||||||
|
|
||||||
|
module.fail_json.assert_called_once()
|
||||||
|
assert "Failed to read private key file" in module.fail_json.call_args[1]["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_embed_file_into_body_returns_multipart_fields():
|
||||||
|
module = MagicMock()
|
||||||
|
file_path = "/fake/path/secret.pem"
|
||||||
|
credentials = {"id": "my-id"}
|
||||||
|
fake_file_content = b"MY SECRET DATA"
|
||||||
|
|
||||||
|
mock = mock_open()
|
||||||
|
mock.return_value.read.return_value = fake_file_content
|
||||||
|
|
||||||
|
with patch("os.path.basename", return_value="secret.pem"), patch.object(
|
||||||
|
builtins, "open", mock
|
||||||
|
):
|
||||||
|
body, content_type = jenkins_credential.embed_file_into_body(
|
||||||
|
module, file_path, credentials.copy()
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "multipart/form-data; boundary=" in content_type
|
||||||
|
|
||||||
|
# Check if file content is embedded in body
|
||||||
|
assert b"MY SECRET DATA" in body
|
||||||
|
assert b'filename="secret.pem"' in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_embed_file_into_body_fails_when_file_unreadable():
|
||||||
|
module = MagicMock()
|
||||||
|
file_path = "/fake/path/missing.pem"
|
||||||
|
credentials = {"id": "something"}
|
||||||
|
|
||||||
|
with patch(open_path, side_effect=IOError("can't read file")):
|
||||||
|
jenkins_credential.embed_file_into_body(module, file_path, credentials)
|
||||||
|
|
||||||
|
module.fail_json.assert_called_once()
|
||||||
|
assert "Failed to read file" in module.fail_json.call_args[1]["msg"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_embed_file_into_body_injects_file_keys_into_credentials():
|
||||||
|
module = MagicMock()
|
||||||
|
file_path = "/fake/path/file.txt"
|
||||||
|
credentials = {"id": "test"}
|
||||||
|
|
||||||
|
with patch(open_path, mock_open(read_data=b"1234")), patch(
|
||||||
|
"os.path.basename", return_value="file.txt"
|
||||||
|
):
|
||||||
|
|
||||||
|
jenkins_credential.embed_file_into_body(module, file_path, credentials)
|
||||||
|
|
||||||
|
assert credentials["file"] == "file0"
|
||||||
|
assert credentials["fileName"] == "file.txt"
|
Loading…
Add table
Add a link
Reference in a new issue