mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-05-11 12:02:09 +00:00
Compare commits
143 Commits
2.2.2
...
stable-2.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ec30da62f | ||
|
|
7103da38ff | ||
|
|
ed3969fc54 | ||
|
|
7e6c2cdd18 | ||
|
|
818f06f215 | ||
|
|
e9ae1fee21 | ||
|
|
c255d11c6e | ||
|
|
b01f78b0b7 | ||
|
|
d40f478552 | ||
|
|
bb45bb676f | ||
|
|
ea54179b23 | ||
|
|
9e41291489 | ||
|
|
15da427a34 | ||
|
|
0a6d3b604b | ||
|
|
1527469cb5 | ||
|
|
e6ac874098 | ||
|
|
8ed4d4b6ed | ||
|
|
af7c24cba7 | ||
|
|
804b9ab57c | ||
|
|
26cd550bc0 | ||
|
|
0b4fda7585 | ||
|
|
42ee210ecf | ||
|
|
c073eea5b3 | ||
|
|
979b492233 | ||
|
|
646eb18806 | ||
|
|
b967b55a16 | ||
|
|
1b66dbbd8b | ||
|
|
2a3862b67a | ||
|
|
2092d921cd | ||
|
|
29c75fa1c6 | ||
|
|
0e86fe0b7b | ||
|
|
43ad31d936 | ||
|
|
093d06ab55 | ||
|
|
454d0efe0a | ||
|
|
08596fd05b | ||
|
|
5e48c6973c | ||
|
|
a3a5f3cf4b | ||
|
|
5ff3566f30 | ||
|
|
c4c12ca2c3 | ||
|
|
7f7008fecc | ||
|
|
09d54919e3 | ||
|
|
58cbbf6364 | ||
|
|
7d0f0449ae | ||
|
|
9f51fc0ef0 | ||
|
|
14fe6f1c55 | ||
|
|
4f1623fe9c | ||
|
|
adf3503d4e | ||
|
|
beb53652db | ||
|
|
92785f58da | ||
|
|
25644ac192 | ||
|
|
3bf147580f | ||
|
|
193a0cb68c | ||
|
|
58a0fb1605 | ||
|
|
b62ea00ebf | ||
|
|
349e9f473a | ||
|
|
61faa1079e | ||
|
|
08a3d951d0 | ||
|
|
8171c994df | ||
|
|
f5a0dd5946 | ||
|
|
9aa20f0fbe | ||
|
|
afa6a74178 | ||
|
|
e2e3f71ecf | ||
|
|
346e303084 | ||
|
|
d68da5bbdd | ||
|
|
e2f54d3431 | ||
|
|
f168a3f67f | ||
|
|
7fb89a7b6f | ||
|
|
42644ee26e | ||
|
|
2a9d894c90 | ||
|
|
3729b8bb5b | ||
|
|
531a9fe3ac | ||
|
|
77dd2496d0 | ||
|
|
95e2add65b | ||
|
|
b5cfc854cb | ||
|
|
5662fa777c | ||
|
|
4fa1fb966b | ||
|
|
f2f4b66d77 | ||
|
|
67c808d934 | ||
|
|
882e672bc5 | ||
|
|
1d05cf54f0 | ||
|
|
764e4499b5 | ||
|
|
2c96b70702 | ||
|
|
c475117bee | ||
|
|
d311ac718e | ||
|
|
7c71436f3b | ||
|
|
0299aa8807 | ||
|
|
f418353e44 | ||
|
|
074f0a6555 | ||
|
|
d68dec3b90 | ||
|
|
30e84faa24 | ||
|
|
fd61f8b15d | ||
|
|
db78d3a505 | ||
|
|
73499d9a09 | ||
|
|
7031829897 | ||
|
|
44c8cff78b | ||
|
|
aae5960dce | ||
|
|
691f0cb235 | ||
|
|
951be74dc0 | ||
|
|
1f79a03edf | ||
|
|
bf3fe91a5d | ||
|
|
791175daef | ||
|
|
e62a271faf | ||
|
|
583de3217c | ||
|
|
82565dad78 | ||
|
|
dde6eb3c06 | ||
|
|
a122bad685 | ||
|
|
b54e9ef4ef | ||
|
|
acb015c788 | ||
|
|
ed33d0b56e | ||
|
|
10cffc5032 | ||
|
|
9a0b3fe30c | ||
|
|
c3ecb64b72 | ||
|
|
50a1bd9db0 | ||
|
|
04e14c1f95 | ||
|
|
b19ff9d70a | ||
|
|
526f0454ab | ||
|
|
e77c8f1449 | ||
|
|
79699ba429 | ||
|
|
fa65698362 | ||
|
|
4ae1856b5c | ||
|
|
ef46c352d0 | ||
|
|
a62c42782f | ||
|
|
ba5cb30305 | ||
|
|
39b6c43ab7 | ||
|
|
b0f1501cd4 | ||
|
|
1116056eeb | ||
|
|
60933457e8 | ||
|
|
9e2d78404f | ||
|
|
bf26f5a3be | ||
|
|
91b80b1d1d | ||
|
|
4010987d1f | ||
|
|
281ff563ed | ||
|
|
ff43353de6 | ||
|
|
d6c06a2078 | ||
|
|
8436ad1341 | ||
|
|
c65512357d | ||
|
|
8e46f92703 | ||
|
|
ab0e38753b | ||
|
|
6061586289 | ||
|
|
45ba8b1a0d | ||
|
|
d01e4a6e4d | ||
|
|
938f7e12e8 | ||
|
|
24ac45741d |
7
.ansible-lint
Normal file
7
.ansible-lint
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
profile: production
|
||||
skip_list:
|
||||
- meta-runtime[unsupported-version]
|
||||
exclude_paths:
|
||||
- tests/integration
|
||||
- tests/sanity
|
||||
4
.github/patchback.yml
vendored
Normal file
4
.github/patchback.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
backport_branch_prefix: patchback/backports/
|
||||
backport_label_prefix: backport-
|
||||
target_branch_prefix: stable-
|
||||
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -12,7 +12,6 @@ daysUntilClose: 30
|
||||
# Only issues or pull requests with all of these labels are check if stale.
|
||||
# Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set
|
||||
# to `[]` to disable
|
||||
exemptLabels:
|
||||
|
||||
21
.github/workflows/changelog.yaml
vendored
Normal file
21
.github/workflows/changelog.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Changelog
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
branches:
|
||||
- main
|
||||
- stable-*
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
uses: ansible-network/github_actions/.github/workflows/changelog.yml@main
|
||||
14
.github/workflows/galaxy-import.yaml
vendored
Normal file
14
.github/workflows/galaxy-import.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: galaxy-import
|
||||
concurrency:
|
||||
group: ${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- stable-*
|
||||
|
||||
jobs:
|
||||
galaxy_importer:
|
||||
uses: ansible-network/github_actions/.github/workflows/galaxy_importer.yml@main
|
||||
146
.github/workflows/integration-tests.yaml
vendored
Normal file
146
.github/workflows/integration-tests.yaml
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
name: Integration tests
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
branches:
|
||||
- main
|
||||
- stable-*
|
||||
|
||||
jobs:
|
||||
splitter:
|
||||
env:
|
||||
source_dir: "./source"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
test_targets: ${{ steps.splitter.outputs.test_targets }}
|
||||
test_targets_json: ${{ steps.splitter.outputs.test_targets_json }}
|
||||
test_jobs: ${{ steps.splitter.outputs.test_jobs }}
|
||||
steps:
|
||||
- name: Checkout the collection repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ${{ env.source_dir }}
|
||||
fetch-depth: "0"
|
||||
|
||||
- name: list changes for pull request
|
||||
id: splitter
|
||||
uses: ansible-network/github_actions/.github/actions/ansible_test_splitter@main
|
||||
with:
|
||||
collections_to_test: ${{ env.source_dir }}
|
||||
total_jobs: 8
|
||||
|
||||
- name: Display splitter output
|
||||
run: |
|
||||
echo "test_targets=${{ steps.splitter.outputs.test_targets }}"
|
||||
echo "test_targets_json=${{ steps.splitter.outputs.test_targets_json }}"
|
||||
echo "test_jobs=${{ steps.splitter.outputs.test_jobs }}"
|
||||
shell: bash
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
needs:
|
||||
- splitter
|
||||
if: ${{ needs.splitter.outputs.test_targets != '' }}
|
||||
env:
|
||||
source: "./source"
|
||||
cloud_common: "./cloudcommon"
|
||||
ansible_posix: "./ansible_posix"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ansible-version:
|
||||
- milestone
|
||||
python-version:
|
||||
- "3.12"
|
||||
enable-turbo-mode:
|
||||
- true
|
||||
- false
|
||||
workflow-id: ${{ fromJson(needs.splitter.outputs.test_jobs) }}
|
||||
name: "integration-py${{ matrix.python-version }}-${{ matrix.ansible-version }}-${{ matrix.workflow-id }}"
|
||||
steps:
|
||||
- name: Read target
|
||||
id: read-targets
|
||||
run: |
|
||||
import json, os
|
||||
with open(os.environ.get('GITHUB_OUTPUT'), "a", encoding="utf-8") as fh:
|
||||
fh.write(f'ansible_test_targets={json.loads(os.environ.get("ALL_TEST_TARGETS")).get(os.environ.get("WORKFLOW_ID"))}\n')
|
||||
shell: python
|
||||
env:
|
||||
ALL_TEST_TARGETS: ${{ needs.splitter.outputs.test_targets_json }}
|
||||
WORKFLOW_ID: ${{ matrix.workflow-id }}
|
||||
|
||||
- name: Display ansible test targets
|
||||
run: |
|
||||
echo "ansible_test_targets -> ${{ steps.read-targets.outputs.ansible_test_targets }}"
|
||||
|
||||
- name: Checkout kubernetes.core repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ${{ env.source }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
# install ansible
|
||||
- name: Install ansible-core (${{ matrix.ansible-version }})
|
||||
run: >-
|
||||
python3 -m pip install
|
||||
https://github.com/ansible/ansible/archive/${{ matrix.ansible-version }}.tar.gz
|
||||
--disable-pip-version-check
|
||||
shell: bash
|
||||
|
||||
- name: Build and install collection
|
||||
id: install-src
|
||||
uses: ansible-network/github_actions/.github/actions/build_install_collection@main
|
||||
with:
|
||||
install_python_dependencies: true
|
||||
source_path: ${{ env.source }}
|
||||
|
||||
- name: checkout ansible-collections/cloud.common
|
||||
uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
|
||||
with:
|
||||
repository: ansible-collections/cloud.common
|
||||
path: ${{ env.cloud_common }}
|
||||
ref: main
|
||||
|
||||
- name: checkout ansible-collections/ansible.posix
|
||||
uses: ansible-network/github_actions/.github/actions/checkout_dependency@main
|
||||
with:
|
||||
repository: ansible-collections/ansible.posix
|
||||
path: ${{ env.ansible_posix }}
|
||||
ref: main
|
||||
|
||||
- name: install cloud.common collection
|
||||
uses: ansible-network/github_actions/.github/actions/build_install_collection@main
|
||||
with:
|
||||
install_python_dependencies: true
|
||||
source_path: ${{ env.cloud_common }}
|
||||
|
||||
- name: install ansible.posix collection
|
||||
uses: ansible-network/github_actions/.github/actions/build_install_collection@main
|
||||
with:
|
||||
install_python_dependencies: true
|
||||
source_path: ${{ env.ansible_posix }}
|
||||
|
||||
- name: create kubernetes cluster
|
||||
uses: helm/kind-action@v1.8.0
|
||||
with:
|
||||
node_image: "kindest/node:v1.29.2"
|
||||
|
||||
- name: Run integration tests
|
||||
uses: ansible-network/github_actions/.github/actions/ansible_test_integration@main
|
||||
with:
|
||||
collection_path: ${{ steps.install-src.outputs.collection_path }}
|
||||
python_version: ${{ matrix.python-version }}
|
||||
ansible_version: ${{ matrix.ansible-version }}
|
||||
ansible_test_targets: ${{ steps.read-targets.outputs.ansible_test_targets }}
|
||||
ansible_test_environment: |
|
||||
ENABLE_TURBO_MODE=${{ matrix.enable-turbo-mode }}
|
||||
24
.github/workflows/linters.yaml
vendored
Normal file
24
.github/workflows/linters.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Linters
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- stable-*
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
linters:
|
||||
uses: ansible-network/github_actions/.github/workflows/tox-linters.yml@main
|
||||
ansible-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run ansible-lint
|
||||
uses: ansible/ansible-lint@v6.21.0
|
||||
15
.github/workflows/sanity-tests.yaml
vendored
Normal file
15
.github/workflows/sanity-tests.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Sanity tests
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- stable-*
|
||||
|
||||
jobs:
|
||||
sanity:
|
||||
uses: ansible-network/github_actions/.github/workflows/sanity.yml@main
|
||||
15
.github/workflows/unit-tests.yaml
vendored
Normal file
15
.github/workflows/unit-tests.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Unit tests
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- stable-*
|
||||
|
||||
jobs:
|
||||
unit-source:
|
||||
uses: ansible-network/github_actions/.github/workflows/unit_source.yml@main
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -15,4 +15,8 @@ tests/integration/cloud-config-*
|
||||
.cache
|
||||
|
||||
# Helm charts
|
||||
molecule/default/*-chart-*.tgz
|
||||
tests/integration/*-chart-*.tgz
|
||||
|
||||
# ansible-test generated file
|
||||
tests/integration/inventory
|
||||
tests/integration/*-*.yml
|
||||
|
||||
165
CHANGELOG.rst
165
CHANGELOG.rst
@@ -4,6 +4,171 @@ Kubernetes Collection Release Notes
|
||||
|
||||
.. contents:: Topics
|
||||
|
||||
v2.4.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
fix resource discovery when complex subresources present
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Resolve Collections util resource discovery fails when complex subresources present (https://github.com/ansible-collections/kubernetes.core/pull/676).
|
||||
|
||||
v2.4.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
The kubernetes.core 2.4.1 release includes several trivial bug fixes related to code sanity.
|
||||
|
||||
v2.4.0
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- refactor K8sAnsibleMixin into module_utils/k8s/ (https://github.com/ansible-collections/kubernetes.core/pull/481).
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Adjust k8s_user_impersonation tests to be compatible with Kubernetes 1.24 (https://github.com/ansible-collections/kubernetes.core/pull/520).
|
||||
- add support for dry run with kubernetes client version >=18.20 (https://github.com/ansible-collections/kubernetes.core/pull/245).
|
||||
- added ignore.txt for Ansible 2.14 devel branch.
|
||||
- fixed module_defaults by removing routing hacks from runtime.yml (https://github.com/ansible-collections/kubernetes.core/pull/347).
|
||||
- helm - add support for -set-file, -set-json, -set and -set-string options when running helm install (https://github.com/ansible-collections/kubernetes.core/issues/533).
|
||||
- helm - add support for helm dependency update (https://github.com/ansible-collections/kubernetes.core/pull/208).
|
||||
- helm - add support for post-renderer flag (https://github.com/ansible-collections/kubernetes.core/issues/30).
|
||||
- helm - add support for timeout cli parameter to allow setting Helm timeout independent of wait (https://github.com/ansible-collections/kubernetes.core/issues/67).
|
||||
- helm - add support for wait parameter for helm uninstall command. (https://github.com/ansible-collections/kubernetes/core/issues/33).
|
||||
- helm - support repo location for helm diff (https://github.com/ansible-collections/kubernetes.core/issues/174).
|
||||
- helm - when ansible is executed in check mode, return the diff between what's deployed and what will be deployed.
|
||||
- helm, helm_plugin, helm_info, helm_plugin_info, kubectl - add support for in-memory kubeconfig. (https://github.com/ansible-collections/kubernetes.core/issues/492).
|
||||
- helm_info - add hooks, notes and manifest as part of returned information (https://github.com/ansible-collections/kubernetes.core/pull/546).
|
||||
- helm_info - add release state as a module argument (https://github.com/ansible-collections/kubernetes.core/issues/377).
|
||||
- helm_info - added possibility to get all values by adding get_all_values parameter (https://github.com/ansible-collections/kubernetes.core/pull/531).
|
||||
- helm_plugin - Add plugin_version parameter to the helm_plugin module (https://github.com/ansible-collections/kubernetes.core/issues/157).
|
||||
- helm_plugin - Add support for helm plugin update using state=update.
|
||||
- helm_repository - Ability to replace (overwrite) the repo if it already exists by forcing (https://github.com/ansible-collections/kubernetes.core/issues/491).
|
||||
- helm_repository - add support for pass-credentials cli parameter (https://github.com/ansible-collections/kubernetes.core/pull/282).
|
||||
- helm_repository - added support for ``host``, ``api_key``, ``validate_certs``, and ``ca_cert``.
|
||||
- helm_repository - mark `pass_credentials` as no_log=True to silence false warning (https://github.com/ansible-collections/kubernetes.core/issues/412).
|
||||
- helm_template - add name (NAME of release) and disable_hook as optional module arguments (https://github.com/ansible-collections/kubernetes.core/issues/313).
|
||||
- helm_template - add show_only and release_namespace as module arguments (https://github.com/ansible-collections/kubernetes.core/issues/313).
|
||||
- helm_template - add support for -set-file, -set-json, -set and -set-string options when running helm template (https://github.com/ansible-collections/kubernetes.core/pull/546).
|
||||
- k8s - add no_proxy support to k8s* (https://github.com/ansible-collections/kubernetes.core/pull/272).
|
||||
- k8s - add support for server_side_apply. (https://github.com/ansible-collections/kubernetes.core/issues/87).
|
||||
- k8s - add support for user impersonation. (https://github.com/ansible-collections/kubernetes/core/issues/40).
|
||||
- k8s - allow resource definition using metadata.generateName (https://github.com/ansible-collections/kubernetes.core/issues/35).
|
||||
- k8s lookup plugin - Enable turbo mode via environment variable (https://github.com/ansible-collections/kubernetes.core/issues/291).
|
||||
- k8s, k8s_scale, k8s_service - add support for resource definition as manifest via. (https://github.com/ansible-collections/kubernetes.core/issues/451).
|
||||
- k8s_cp - remove dependency with 'find' executable on remote pod when state=from_pod (https://github.com/ansible-collections/kubernetes.core/issues/486).
|
||||
- k8s_drain - Adds ``delete_emptydir_data`` option to ``k8s_drain.delete_options`` to evict pods with an ``emptyDir`` volume attached (https://github.com/ansible-collections/kubernetes.core/pull/322).
|
||||
- k8s_exec - select first container from the pod if none specified (https://github.com/ansible-collections/kubernetes.core/issues/358).
|
||||
- k8s_exec - update deprecation warning for `return_code` (https://github.com/ansible-collections/kubernetes.core/issues/417).
|
||||
- k8s_json_patch - minor typo fix in the example section (https://github.com/ansible-collections/kubernetes.core/issues/411).
|
||||
- k8s_log - add the ``all_containers`` for retrieving all containers' logs in the pod(s).
|
||||
- k8s_log - added the `previous` parameter for retrieving the previously terminated pod logs (https://github.com/ansible-collections/kubernetes.core/issues/437).
|
||||
- k8s_log - added the `tail_lines` parameter to limit the number of lines to be retrieved from the end of the logs (https://github.com/ansible-collections/kubernetes.core/issues/488).
|
||||
- k8s_rollback - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/243).
|
||||
- k8s_scale - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/244).
|
||||
- kubectl - wait for dd command to complete before proceeding (https://github.com/ansible-collections/kubernetes.core/pull/321).
|
||||
- kubectl.py - replace distutils.spawn.find_executable with shutil.which in the kubectl connection plugin (https://github.com/ansible-collections/kubernetes.core/pull/456).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix dry_run logic - Pass the value dry_run=All instead of dry_run=True to the client, add conditional check on kubernetes client version as this feature is supported only for kubernetes >= 18.20.0 (https://github.com/ansible-collections/kubernetes.core/pull/561).
|
||||
- Fix kubeconfig parameter when multiple config files are provided (https://github.com/ansible-collections/kubernetes.core/issues/435).
|
||||
- Helm - Fix issue with alternative kubeconfig provided with validate_certs=False (https://github.com/ansible-collections/kubernetes.core/issues/538).
|
||||
- Various modules and plugins - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/kubernetes.core/pull/314).
|
||||
- add missing documentation for filter plugin kubernetes.core.k8s_config_resource_name (https://github.com/ansible-collections/kubernetes.core/issues/558).
|
||||
- common - Ensure the label_selectors parameter of _wait_for method is optional.
|
||||
- common - handle ``aliases`` passed from inventory and lookup plugins.
|
||||
- helm_template - evaluate release_values after values_files, insuring highest precedence (now same behavior as in helm module). (https://github.com/ansible-collections/kubernetes.core/pull/348)
|
||||
- import exception from ``kubernetes.client.rest``.
|
||||
- k8s - Fix issue with check_mode when using server side apply (https://github.com/ansible-collections/kubernetes.core/issues/547).
|
||||
- k8s - Fix issue with server side apply with kubernetes release '25.3.0' (https://github.com/ansible-collections/kubernetes.core/issues/548).
|
||||
- k8s_cp - add support for check_mode (https://github.com/ansible-collections/kubernetes.core/issues/380).
|
||||
- k8s_drain - fix error caused by accessing an undefined variable when pods have local storage (https://github.com/ansible-collections/kubernetes.core/issues/292).
|
||||
- k8s_info - don't wait on empty List resources (https://github.com/ansible-collections/kubernetes.core/pull/253).
|
||||
- k8s_info - fix issue when module returns successful true after the resource cache has been established during periods where communication to the api-server is not possible (https://github.com/ansible-collections/kubernetes.core/issues/508).
|
||||
- k8s_log - Fix module traceback when no resource found (https://github.com/ansible-collections/kubernetes.core/issues/479).
|
||||
- k8s_log - fix exception raised when the name is not provided for resources requiring. (https://github.com/ansible-collections/kubernetes.core/issues/514)
|
||||
- k8s_scale - fix waiting on statefulset when scaled down to 0 replicas (https://github.com/ansible-collections/kubernetes.core/issues/203).
|
||||
- module_utils.common - change default opening mode to read-bytes to avoid bad interpretation of non ascii characters and strings, often present in 3rd party manifests.
|
||||
- module_utils/k8s/client.py - fix issue when trying to authenticate with host, client_cert and client_key parameters only.
|
||||
- remove binary file from k8s_cp test suite (https://github.com/ansible-collections/kubernetes.core/pull/298).
|
||||
- use resource prefix when finding resource and apiVersion is v1 (https://github.com/ansible-collections/kubernetes.core/issues/351).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
- helm_pull - download a chart from a repository and (optionally) unpack it in local directory.
|
||||
|
||||
v2.3.1
|
||||
======
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Catch exception raised when the process is waiting for resources (https://github.com/ansible-collections/kubernetes.core/issues/407).
|
||||
- Remove `omit` placeholder when defining resource using template parameter (https://github.com/ansible-collections/kubernetes.core/issues/431).
|
||||
- k8s - fix the issue when trying to delete resources using label_selectors options (https://github.com/ansible-collections/kubernetes.core/issues/433).
|
||||
- k8s_cp - fix issue when using parameter local_path with file on managed node. (https://github.com/ansible-collections/kubernetes.core/issues/421).
|
||||
- k8s_drain - fix error occurring when trying to drain node with disable_eviction set to yes (https://github.com/ansible-collections/kubernetes.core/issues/416).
|
||||
|
||||
v2.3.0
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- add support for dry run with kubernetes client version >=18.20 (https://github.com/ansible-collections/kubernetes.core/pull/245).
|
||||
- fixed module_defaults by removing routing hacks from runtime.yml (https://github.com/ansible-collections/kubernetes.core/pull/347).
|
||||
- helm - add support for timeout cli parameter to allow setting Helm timeout independent of wait (https://github.com/ansible-collections/kubernetes.core/issues/67).
|
||||
- helm - add support for wait parameter for helm uninstall command. (https://github.com/ansible-collections/kubernetes/core/issues/33).
|
||||
- helm - support repo location for helm diff (https://github.com/ansible-collections/kubernetes.core/issues/174).
|
||||
- helm - when ansible is executed in check mode, return the diff between what's deployed and what will be deployed.
|
||||
- helm_info - add release state as a module argument (https://github.com/ansible-collections/kubernetes.core/issues/377).
|
||||
- helm_plugin - Add plugin_version parameter to the helm_plugin module (https://github.com/ansible-collections/kubernetes.core/issues/157).
|
||||
- helm_plugin - Add support for helm plugin update using state=update.
|
||||
- helm_repository - add support for pass-credentials cli parameter (https://github.com/ansible-collections/kubernetes.core/pull/282).
|
||||
- helm_repository - added support for ``host``, ``api_key``, ``validate_certs``, and ``ca_cert``.
|
||||
- helm_template - add show_only and release_namespace as module arguments (https://github.com/ansible-collections/kubernetes.core/issues/313).
|
||||
- k8s - add no_proxy support to k8s* (https://github.com/ansible-collections/kubernetes.core/pull/272).
|
||||
- k8s - add support for server_side_apply. (https://github.com/ansible-collections/kubernetes.core/issues/87).
|
||||
- k8s - add support for user impersonation. (https://github.com/ansible-collections/kubernetes/core/issues/40).
|
||||
- k8s - allow resource definition using metadata.generateName (https://github.com/ansible-collections/kubernetes.core/issues/35).
|
||||
- k8s lookup plugin - Enable turbo mode via environment variable (https://github.com/ansible-collections/kubernetes.core/issues/291).
|
||||
- k8s_drain - Adds ``delete_emptydir_data`` option to ``k8s_drain.delete_options`` to evict pods with an ``emptyDir`` volume attached (https://github.com/ansible-collections/kubernetes.core/pull/322).
|
||||
- k8s_exec - select first container from the pod if none specified (https://github.com/ansible-collections/kubernetes.core/issues/358).
|
||||
- k8s_rollback - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/243).
|
||||
- k8s_scale - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/244).
|
||||
- kubectl - wait for dd command to complete before proceeding (https://github.com/ansible-collections/kubernetes.core/pull/321).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Various modules and plugins - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/kubernetes.core/pull/314).
|
||||
- common - Ensure the label_selectors parameter of _wait_for method is optional.
|
||||
- helm_template - evaluate release_values after values_files, insuring highest precedence (now same behavior as in helm module). (https://github.com/ansible-collections/kubernetes.core/pull/348)
|
||||
- import exception from ``kubernetes.client.rest``.
|
||||
- k8s_drain - fix error caused by accessing an undefined variable when pods have local storage (https://github.com/ansible-collections/kubernetes.core/issues/292).
|
||||
- k8s_info - don't wait on empty List resources (https://github.com/ansible-collections/kubernetes.core/pull/253).
|
||||
- k8s_scale - fix waiting on statefulset when scaled down to 0 replicas (https://github.com/ansible-collections/kubernetes.core/issues/203).
|
||||
- module_utils.common - change default opening mode to read-bytes to avoid bad interpretation of non ascii characters and strings, often present in 3rd party manifests.
|
||||
- remove binary file from k8s_cp test suite (https://github.com/ansible-collections/kubernetes.core/pull/298).
|
||||
- use resource prefix when finding resource and apiVersion is v1 (https://github.com/ansible-collections/kubernetes.core/issues/351).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
- k8s_taint - Taint a node in a Kubernetes/OpenShift cluster
|
||||
|
||||
v2.2.0
|
||||
======
|
||||
|
||||
7
Makefile
7
Makefile
@@ -1,5 +1,5 @@
|
||||
# Also needs to be updated in galaxy.yml
|
||||
VERSION = 2.2.0
|
||||
VERSION = 2.4.1
|
||||
|
||||
TEST_ARGS ?= ""
|
||||
PYTHON_VERSION ?= `python -c 'import platform; print(".".join(platform.python_version_tuple()[0:2]))'`
|
||||
@@ -22,10 +22,7 @@ test-sanity:
|
||||
ansible-test sanity --docker -v --color --python $(PYTHON_VERSION) $(?TEST_ARGS)
|
||||
|
||||
test-integration:
|
||||
ansible-test integration --docker -v --color --retry-on-error --python $(PYTHON_VERSION) --continue-on-error --diff --coverage $(?TEST_ARGS)
|
||||
|
||||
test-molecule:
|
||||
molecule test
|
||||
ansible-test integration --diff --no-temp-workdir --color --skip-tags False --retry-on-error --continue-on-error --python $(PYTHON_VERSION) -v --coverage $(?TEST_ARGS)
|
||||
|
||||
test-unit:
|
||||
ansible-test units --docker -v --color --python $(PYTHON_VERSION) $(?TEST_ARGS)
|
||||
|
||||
48
PSF-license.txt
Normal file
48
PSF-license.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation;
|
||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||
prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
15
README.md
15
README.md
@@ -11,6 +11,8 @@ The collection includes a variety of Ansible content to help automate the manage
|
||||
|
||||
This collection has been tested against following Ansible versions: **>=2.9.17**.
|
||||
|
||||
For collections that support Ansible 2.9, please ensure you update your `network_os` to use the
|
||||
fully qualified collection name (for example, `cisco.ios.ios`).
|
||||
Plugins and modules within a collection may be tested with only specific Ansible versions.
|
||||
A collection may contain metadata that identifies these versions.
|
||||
PEP440 is the schema used to describe the versions of Ansible.
|
||||
@@ -22,6 +24,10 @@ PEP440 is the schema used to describe the versions of Ansible.
|
||||
|
||||
Note: Python2 is deprecated from [1st January 2020](https://www.python.org/doc/sunset-python-2/). Please switch to Python3.
|
||||
|
||||
## Kubernetes Version Support
|
||||
|
||||
This collection supports Kubernetes versions >=1.19.
|
||||
|
||||
## Included content
|
||||
|
||||
Click on the name of a plugin or module to view that content's documentation:
|
||||
@@ -55,6 +61,7 @@ Name | Description
|
||||
[kubernetes.core.helm_info](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.helm_info_module.rst)|Get information from Helm package deployed inside the cluster
|
||||
[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_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
|
||||
@@ -68,6 +75,7 @@ Name | Description
|
||||
[kubernetes.core.k8s_rollback](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.k8s_rollback_module.rst)|Rollback Kubernetes (K8S) Deployments and DaemonSets
|
||||
[kubernetes.core.k8s_scale](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.k8s_scale_module.rst)|Set a new size for a Deployment, ReplicaSet, Replication Controller, or Job.
|
||||
[kubernetes.core.k8s_service](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.k8s_service_module.rst)|Manage Services on Kubernetes
|
||||
[kubernetes.core.k8s_taint](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/kubernetes.core.k8s_taint_module.rst)|Taint a node in a Kubernetes/OpenShift cluster
|
||||
|
||||
<!--end collection content-->
|
||||
|
||||
@@ -85,7 +93,7 @@ You can also include it in a `requirements.yml` file and install it via `ansible
|
||||
---
|
||||
collections:
|
||||
- name: kubernetes.core
|
||||
version: 2.2.0
|
||||
version: 2.4.1
|
||||
```
|
||||
|
||||
### Installing the Kubernetes Python Library
|
||||
@@ -165,7 +173,7 @@ For documentation on how to use individual modules and other content included in
|
||||
## Ansible Turbo mode Tech Preview
|
||||
|
||||
|
||||
The ``kubernetes.core`` collection supports Ansible Turbo mode as a tech preview via the ``cloud.common`` collection. By default, this feature is disabled. To enable Turbo mode, set the environment variable `ENABLE_TURBO_MODE=1` on the managed node. For example:
|
||||
The ``kubernetes.core`` collection supports Ansible Turbo mode as a tech preview via the ``cloud.common`` collection. By default, this feature is disabled. To enable Turbo mode for modules, set the environment variable `ENABLE_TURBO_MODE=1` on the managed node. For example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -176,6 +184,9 @@ The ``kubernetes.core`` collection supports Ansible Turbo mode as a tech preview
|
||||
...
|
||||
```
|
||||
|
||||
To enable Turbo mode for k8s lookup plugin, set the environment variable `ENABLE_TURBO_MODE=1` on the managed node. This is not working when
|
||||
defined in the playbook using `environment` keyword as above, you must set it using `export ENABLE_TURBO_MODE=1`.
|
||||
|
||||
Please read more about Ansible Turbo mode - [here](https://github.com/ansible-collections/kubernetes.core/blob/main/docs/ansible_turbo_mode.rst).
|
||||
|
||||
## Testing and Development
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
kubernetes-client [platform:fedora]
|
||||
openshift-clients [platform:rhel-8]
|
||||
openshift-clients [platform:rhel-9]
|
||||
|
||||
@@ -486,3 +486,296 @@ releases:
|
||||
name: kustomize
|
||||
namespace: null
|
||||
release_date: '2021-09-15'
|
||||
2.3.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Various modules and plugins - use vendored version of ``distutils.version``
|
||||
instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/kubernetes.core/pull/314).
|
||||
- common - Ensure the label_selectors parameter of _wait_for method is optional.
|
||||
- helm_template - evaluate release_values after values_files, insuring highest
|
||||
precedence (now same behavior as in helm module). (https://github.com/ansible-collections/kubernetes.core/pull/348)
|
||||
- import exception from ``kubernetes.client.rest``.
|
||||
- k8s_drain - fix error caused by accessing an undefined variable when pods
|
||||
have local storage (https://github.com/ansible-collections/kubernetes.core/issues/292).
|
||||
- k8s_info - don't wait on empty List resources (https://github.com/ansible-collections/kubernetes.core/pull/253).
|
||||
- k8s_scale - fix waiting on statefulset when scaled down to 0 replicas (https://github.com/ansible-collections/kubernetes.core/issues/203).
|
||||
- module_utils.common - change default opening mode to read-bytes to avoid bad
|
||||
interpretation of non ascii characters and strings, often present in 3rd party
|
||||
manifests.
|
||||
- remove binary file from k8s_cp test suite (https://github.com/ansible-collections/kubernetes.core/pull/298).
|
||||
- use resource prefix when finding resource and apiVersion is v1 (https://github.com/ansible-collections/kubernetes.core/issues/351).
|
||||
minor_changes:
|
||||
- add support for dry run with kubernetes client version >=18.20 (https://github.com/ansible-collections/kubernetes.core/pull/245).
|
||||
- fixed module_defaults by removing routing hacks from runtime.yml (https://github.com/ansible-collections/kubernetes.core/pull/347).
|
||||
- helm - add support for timeout cli parameter to allow setting Helm timeout
|
||||
independent of wait (https://github.com/ansible-collections/kubernetes.core/issues/67).
|
||||
- helm - add support for wait parameter for helm uninstall command. (https://github.com/ansible-collections/kubernetes/core/issues/33).
|
||||
- helm - support repo location for helm diff (https://github.com/ansible-collections/kubernetes.core/issues/174).
|
||||
- helm - when ansible is executed in check mode, return the diff between what's
|
||||
deployed and what will be deployed.
|
||||
- helm_info - add release state as a module argument (https://github.com/ansible-collections/kubernetes.core/issues/377).
|
||||
- helm_plugin - Add plugin_version parameter to the helm_plugin module (https://github.com/ansible-collections/kubernetes.core/issues/157).
|
||||
- helm_plugin - Add support for helm plugin update using state=update.
|
||||
- helm_repository - add support for pass-credentials cli parameter (https://github.com/ansible-collections/kubernetes.core/pull/282).
|
||||
- helm_repository - added support for ``host``, ``api_key``, ``validate_certs``,
|
||||
and ``ca_cert``.
|
||||
- helm_template - add show_only and release_namespace as module arguments (https://github.com/ansible-collections/kubernetes.core/issues/313).
|
||||
- k8s - add no_proxy support to k8s* (https://github.com/ansible-collections/kubernetes.core/pull/272).
|
||||
- k8s - add support for server_side_apply. (https://github.com/ansible-collections/kubernetes.core/issues/87).
|
||||
- k8s - add support for user impersonation. (https://github.com/ansible-collections/kubernetes/core/issues/40).
|
||||
- k8s - allow resource definition using metadata.generateName (https://github.com/ansible-collections/kubernetes.core/issues/35).
|
||||
- k8s lookup plugin - Enable turbo mode via environment variable (https://github.com/ansible-collections/kubernetes.core/issues/291).
|
||||
- k8s_drain - Adds ``delete_emptydir_data`` option to ``k8s_drain.delete_options``
|
||||
to evict pods with an ``emptyDir`` volume attached (https://github.com/ansible-collections/kubernetes.core/pull/322).
|
||||
- k8s_exec - select first container from the pod if none specified (https://github.com/ansible-collections/kubernetes.core/issues/358).
|
||||
- k8s_rollback - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/243).
|
||||
- k8s_scale - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/244).
|
||||
- kubectl - wait for dd command to complete before proceeding (https://github.com/ansible-collections/kubernetes.core/pull/321).
|
||||
fragments:
|
||||
- 0-copy_ignore_txt.yml
|
||||
- 226-add-version-parameter-to-helm_plugin.yml
|
||||
- 231-helm-add-timeout-parameter.yaml
|
||||
- 238-helm-add-support-for-helm-uninstall-wait.yaml
|
||||
- 238-k8s-add-support-for-generate_name.yml
|
||||
- 245-add-dry-run.yaml
|
||||
- 250-k8s-add-support-for-impersonation.yaml
|
||||
- 253-dont-wait-on-list-resources.yaml
|
||||
- 255-k8s_scale-k8s_rollback-add-support-for-check_mode.yml
|
||||
- 260-k8s-add-support-for-server_side_apply.yml
|
||||
- 272-k8s-add-support-no_proxy.yaml
|
||||
- 282-helm-repository-add-pass-credentials.yaml
|
||||
- 290-returns-diff-in-check-mode.yaml
|
||||
- 295-fix-k8s-drain-variable-declaration.yaml
|
||||
- 298-remove-binary-file.yaml
|
||||
- 308-fix-for-common-non-ascii-characters-in-resources.yaml
|
||||
- 313-helm-template-add-support-for-show-only-and-release-namespace.yml
|
||||
- 321-kubectl_sleep.yml
|
||||
- 322-Add-delete_emptydir_data-to-drain-delete_options.yaml
|
||||
- 335-k8s-lookup-add-support-for-turbo-mode.yml
|
||||
- 347-routing.yml
|
||||
- 348-helm_template-fix-precedence-of-release-values-over-values-files.yaml
|
||||
- 358-k8s_exec.yml
|
||||
- 364-use-resource-prefix.yaml
|
||||
- 377-helm-info-state.yml
|
||||
- 389-helm-add-support-chart_repo_url-on-helm_diff.yml
|
||||
- 391-fix-statefulset-wait.yaml
|
||||
- _wait_for_label_selector_optional.yaml
|
||||
- disutils.version.yml
|
||||
- exception.yml
|
||||
- helm_repository.yml
|
||||
modules:
|
||||
- description: Taint a node in a Kubernetes/OpenShift cluster
|
||||
name: k8s_taint
|
||||
namespace: ''
|
||||
release_date: '2022-03-11'
|
||||
2.3.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Catch expectation raised when the process is waiting for resources (https://github.com/ansible-collections/kubernetes.core/issues/407).
|
||||
- Remove `omit` placeholder when defining resource using template parameter
|
||||
(https://github.com/ansible-collections/kubernetes.core/issues/431).
|
||||
- k8s - fix the issue when trying to delete resources using label_selectors
|
||||
options (https://github.com/ansible-collections/kubernetes.core/issues/433).
|
||||
- k8s_cp - fix issue when using parameter local_path with file on managed node.
|
||||
(https://github.com/ansible-collections/kubernetes.core/issues/421).
|
||||
- k8s_drain - fix error occurring when trying to drain node with disable_eviction
|
||||
set to yes (https://github.com/ansible-collections/kubernetes.core/issues/416).
|
||||
fragments:
|
||||
- 408-fix-wait-on-exception.yml
|
||||
- 417-fix-k8s-drain-delete-options.yaml
|
||||
- 422-k8s_cp-fix-issue-when-issue-local_path.yaml
|
||||
- 432-fix-issue-when-using-template-parameter.yaml
|
||||
- 434-fix-k8s-delete-using-label_selector.yaml
|
||||
release_date: '2022-05-02'
|
||||
2.4.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Fix dry_run logic - Pass the value dry_run=All instead of dry_run=True to
|
||||
the client, add conditional check on kubernetes client version as this feature
|
||||
is supported only for kubernetes >= 18.20.0 (https://github.com/ansible-collections/kubernetes.core/pull/561).
|
||||
- Fix kubeconfig parameter when multiple config files are provided (https://github.com/ansible-collections/kubernetes.core/issues/435).
|
||||
- Helm - Fix issue with alternative kubeconfig provided with validate_certs=False
|
||||
(https://github.com/ansible-collections/kubernetes.core/issues/538).
|
||||
- Various modules and plugins - use vendored version of ``distutils.version``
|
||||
instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/kubernetes.core/pull/314).
|
||||
- add missing documentation for filter plugin kubernetes.core.k8s_config_resource_name
|
||||
(https://github.com/ansible-collections/kubernetes.core/issues/558).
|
||||
- common - Ensure the label_selectors parameter of _wait_for method is optional.
|
||||
- common - handle ``aliases`` passed from inventory and lookup plugins.
|
||||
- helm_template - evaluate release_values after values_files, insuring highest
|
||||
precedence (now same behavior as in helm module). (https://github.com/ansible-collections/kubernetes.core/pull/348)
|
||||
- import exception from ``kubernetes.client.rest``.
|
||||
- k8s - Fix issue with check_mode when using server side apply (https://github.com/ansible-collections/kubernetes.core/issues/547).
|
||||
- k8s - Fix issue with server side apply with kubernetes release '25.3.0' (https://github.com/ansible-collections/kubernetes.core/issues/548).
|
||||
- k8s_cp - add support for check_mode (https://github.com/ansible-collections/kubernetes.core/issues/380).
|
||||
- k8s_drain - fix error caused by accessing an undefined variable when pods
|
||||
have local storage (https://github.com/ansible-collections/kubernetes.core/issues/292).
|
||||
- k8s_info - don't wait on empty List resources (https://github.com/ansible-collections/kubernetes.core/pull/253).
|
||||
- k8s_info - fix issue when module returns successful true after the resource
|
||||
cache has been established during periods where communication to the api-server
|
||||
is not possible (https://github.com/ansible-collections/kubernetes.core/issues/508).
|
||||
- k8s_log - Fix module traceback when no resource found (https://github.com/ansible-collections/kubernetes.core/issues/479).
|
||||
- k8s_log - fix exception raised when the name is not provided for resources
|
||||
requiring. (https://github.com/ansible-collections/kubernetes.core/issues/514)
|
||||
- k8s_scale - fix waiting on statefulset when scaled down to 0 replicas (https://github.com/ansible-collections/kubernetes.core/issues/203).
|
||||
- module_utils.common - change default opening mode to read-bytes to avoid bad
|
||||
interpretation of non ascii characters and strings, often present in 3rd party
|
||||
manifests.
|
||||
- module_utils/k8s/client.py - fix issue when trying to authenticate with host,
|
||||
client_cert and client_key parameters only.
|
||||
- remove binary file from k8s_cp test suite (https://github.com/ansible-collections/kubernetes.core/pull/298).
|
||||
- use resource prefix when finding resource and apiVersion is v1 (https://github.com/ansible-collections/kubernetes.core/issues/351).
|
||||
major_changes:
|
||||
- refactor K8sAnsibleMixin into module_utils/k8s/ (https://github.com/ansible-collections/kubernetes.core/pull/481).
|
||||
minor_changes:
|
||||
- Adjust k8s_user_impersonation tests to be compatible with Kubernetes 1.24
|
||||
(https://github.com/ansible-collections/kubernetes.core/pull/520).
|
||||
- add support for dry run with kubernetes client version >=18.20 (https://github.com/ansible-collections/kubernetes.core/pull/245).
|
||||
- added ignore.txt for Ansible 2.14 devel branch.
|
||||
- fixed module_defaults by removing routing hacks from runtime.yml (https://github.com/ansible-collections/kubernetes.core/pull/347).
|
||||
- helm - add support for -set-file, -set-json, -set and -set-string options
|
||||
when running helm install (https://github.com/ansible-collections/kubernetes.core/issues/533).
|
||||
- helm - add support for helm dependency update (https://github.com/ansible-collections/kubernetes.core/pull/208).
|
||||
- helm - add support for post-renderer flag (https://github.com/ansible-collections/kubernetes.core/issues/30).
|
||||
- helm - add support for timeout cli parameter to allow setting Helm timeout
|
||||
independent of wait (https://github.com/ansible-collections/kubernetes.core/issues/67).
|
||||
- helm - add support for wait parameter for helm uninstall command. (https://github.com/ansible-collections/kubernetes/core/issues/33).
|
||||
- helm - support repo location for helm diff (https://github.com/ansible-collections/kubernetes.core/issues/174).
|
||||
- helm - when ansible is executed in check mode, return the diff between what's
|
||||
deployed and what will be deployed.
|
||||
- helm, helm_plugin, helm_info, helm_plugin_info, kubectl - add support for
|
||||
in-memory kubeconfig. (https://github.com/ansible-collections/kubernetes.core/issues/492).
|
||||
- helm_info - add hooks, notes and manifest as part of returned information
|
||||
(https://github.com/ansible-collections/kubernetes.core/pull/546).
|
||||
- helm_info - add release state as a module argument (https://github.com/ansible-collections/kubernetes.core/issues/377).
|
||||
- helm_info - added possibility to get all values by adding get_all_values parameter
|
||||
(https://github.com/ansible-collections/kubernetes.core/pull/531).
|
||||
- helm_plugin - Add plugin_version parameter to the helm_plugin module (https://github.com/ansible-collections/kubernetes.core/issues/157).
|
||||
- helm_plugin - Add support for helm plugin update using state=update.
|
||||
- helm_repository - Ability to replace (overwrite) the repo if it already exists
|
||||
by forcing (https://github.com/ansible-collections/kubernetes.core/issues/491).
|
||||
- helm_repository - add support for pass-credentials cli parameter (https://github.com/ansible-collections/kubernetes.core/pull/282).
|
||||
- helm_repository - added support for ``host``, ``api_key``, ``validate_certs``,
|
||||
and ``ca_cert``.
|
||||
- helm_repository - mark `pass_credentials` as no_log=True to silence false
|
||||
warning (https://github.com/ansible-collections/kubernetes.core/issues/412).
|
||||
- helm_template - add name (NAME of release) and disable_hook as optional module
|
||||
arguments (https://github.com/ansible-collections/kubernetes.core/issues/313).
|
||||
- helm_template - add show_only and release_namespace as module arguments (https://github.com/ansible-collections/kubernetes.core/issues/313).
|
||||
- helm_template - add support for -set-file, -set-json, -set and -set-string
|
||||
options when running helm template (https://github.com/ansible-collections/kubernetes.core/pull/546).
|
||||
- k8s - add no_proxy support to k8s* (https://github.com/ansible-collections/kubernetes.core/pull/272).
|
||||
- k8s - add support for server_side_apply. (https://github.com/ansible-collections/kubernetes.core/issues/87).
|
||||
- k8s - add support for user impersonation. (https://github.com/ansible-collections/kubernetes/core/issues/40).
|
||||
- k8s - allow resource definition using metadata.generateName (https://github.com/ansible-collections/kubernetes.core/issues/35).
|
||||
- k8s lookup plugin - Enable turbo mode via environment variable (https://github.com/ansible-collections/kubernetes.core/issues/291).
|
||||
- k8s, k8s_scale, k8s_service - add support for resource definition as manifest
|
||||
via. (https://github.com/ansible-collections/kubernetes.core/issues/451).
|
||||
- k8s_cp - remove dependency with 'find' executable on remote pod when state=from_pod
|
||||
(https://github.com/ansible-collections/kubernetes.core/issues/486).
|
||||
- k8s_drain - Adds ``delete_emptydir_data`` option to ``k8s_drain.delete_options``
|
||||
to evict pods with an ``emptyDir`` volume attached (https://github.com/ansible-collections/kubernetes.core/pull/322).
|
||||
- k8s_exec - select first container from the pod if none specified (https://github.com/ansible-collections/kubernetes.core/issues/358).
|
||||
- k8s_exec - update deprecation warning for `return_code` (https://github.com/ansible-collections/kubernetes.core/issues/417).
|
||||
- k8s_json_patch - minor typo fix in the example section (https://github.com/ansible-collections/kubernetes.core/issues/411).
|
||||
- k8s_log - add the ``all_containers`` for retrieving all containers' logs in
|
||||
the pod(s).
|
||||
- k8s_log - added the `previous` parameter for retrieving the previously terminated
|
||||
pod logs (https://github.com/ansible-collections/kubernetes.core/issues/437).
|
||||
- k8s_log - added the `tail_lines` parameter to limit the number of lines to
|
||||
be retrieved from the end of the logs (https://github.com/ansible-collections/kubernetes.core/issues/488).
|
||||
- k8s_rollback - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/243).
|
||||
- k8s_scale - add support for check_mode. (https://github.com/ansible-collections/kubernetes/core/issues/244).
|
||||
- kubectl - wait for dd command to complete before proceeding (https://github.com/ansible-collections/kubernetes.core/pull/321).
|
||||
- kubectl.py - replace distutils.spawn.find_executable with shutil.which in
|
||||
the kubectl connection plugin (https://github.com/ansible-collections/kubernetes.core/pull/456).
|
||||
fragments:
|
||||
- 0-copy_ignore_txt.yml
|
||||
- 208-add-dependency-update.yaml
|
||||
- 226-add-version-parameter-to-helm_plugin.yml
|
||||
- 231-helm-add-timeout-parameter.yaml
|
||||
- 238-helm-add-support-for-helm-uninstall-wait.yaml
|
||||
- 238-k8s-add-support-for-generate_name.yml
|
||||
- 245-add-dry-run.yaml
|
||||
- 250-k8s-add-support-for-impersonation.yaml
|
||||
- 253-dont-wait-on-list-resources.yaml
|
||||
- 255-k8s_scale-k8s_rollback-add-support-for-check_mode.yml
|
||||
- 260-k8s-add-support-for-server_side_apply.yml
|
||||
- 272-k8s-add-support-no_proxy.yaml
|
||||
- 282-helm-repository-add-pass-credentials.yaml
|
||||
- 290-returns-diff-in-check-mode.yaml
|
||||
- 295-fix-k8s-drain-variable-declaration.yaml
|
||||
- 298-remove-binary-file.yaml
|
||||
- 30-helm-add-post-renderer-support.yml
|
||||
- 308-fix-for-common-non-ascii-characters-in-resources.yaml
|
||||
- 313-helm-template-add-support-for-name-and-disablehook.yml
|
||||
- 313-helm-template-add-support-for-show-only-and-release-namespace.yml
|
||||
- 321-kubectl_sleep.yml
|
||||
- 322-Add-delete_emptydir_data-to-drain-delete_options.yaml
|
||||
- 335-k8s-lookup-add-support-for-turbo-mode.yml
|
||||
- 347-routing.yml
|
||||
- 348-helm_template-fix-precedence-of-release-values-over-values-files.yaml
|
||||
- 358-k8s_exec.yml
|
||||
- 364-use-resource-prefix.yaml
|
||||
- 377-helm-info-state.yml
|
||||
- 389-helm-add-support-chart_repo_url-on-helm_diff.yml
|
||||
- 391-fix-statefulset-wait.yaml
|
||||
- 411_k8s_json_patch.yml
|
||||
- 412_pass_creds.yml
|
||||
- 417_deprecation.yml
|
||||
- 428-fix-kubeconfig-parameter-with-multiple-config-files.yaml
|
||||
- 437-k8s-add-support-for-previous-logs.yaml
|
||||
- 456-replace-distutils.yml
|
||||
- 478-add-support-for-manifest-url.yaml
|
||||
- 481-refactor-common.yml
|
||||
- 488-add-support-for-tail-logs.yaml
|
||||
- 493-k8s_log-fix-module-when-pod-does-exist.yaml
|
||||
- 497-helm-add-support-for-in-memory-kubeconfig.yml
|
||||
- 498-k8s-honor-aliases.yaml
|
||||
- 505-add-from-yaml-all-example.yml
|
||||
- 509-helm-repo-add-force_update-argument.yaml
|
||||
- 512-k8s_cp-add-support-for-check_mode-update-command-for-listing-files-into-pod.yaml
|
||||
- 515-update-sanity-for-2-15.yml
|
||||
- 522-fix-helm-tests.yml
|
||||
- 523-helm_info-get-all-values.yaml
|
||||
- 528-k8s_log-support-all_containers-options.yml
|
||||
- 532-k8s_crd-fix-integration-test.yml
|
||||
- 546-helm-install-add-support-for-set-options.yaml
|
||||
- 549-fix-server-side-apply.yaml
|
||||
- 552-k8s_cp-fix-issue-when-copying-item-with-space-in-its-name.yml
|
||||
- 561-fix-dry-run.yml
|
||||
- 562-helm-fix-issue-when-alternative-kubeconfig-is-provided.yaml
|
||||
- 571-k8s_info-fix-issue-with-api-server.yaml
|
||||
- _wait_for_label_selector_optional.yaml
|
||||
- disutils.version.yml
|
||||
- exception.yml
|
||||
- fix-ci-unit-tests.yaml
|
||||
- helm_repository.yml
|
||||
- ignore_2.14.yml
|
||||
- k8s_config_resource_name-add-missing-documentation.yml
|
||||
- k8s_rollback_reduce_tmeouts.yaml
|
||||
- k8s_user_impersonation_k8s_1_24.yaml
|
||||
- minor-tests-duration.yaml
|
||||
modules:
|
||||
- description: download a chart from a repository and (optionally) unpack it in
|
||||
local directory.
|
||||
name: helm_pull
|
||||
namespace: ''
|
||||
release_date: '2023-01-24'
|
||||
2.4.1:
|
||||
changes:
|
||||
release_summary: The kubernetes.core 2.4.1 release includes several trivial
|
||||
bug fixes related to sanity.
|
||||
fragments:
|
||||
- fix_sanity_errors.yml
|
||||
release_date: '2024-02-06'
|
||||
2.4.2:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Resolve Collections util resource discovery fails when complex subresources
|
||||
present (https://github.com/ansible-collections/kubernetes.core/pull/676).
|
||||
release_summary: fix resource discovery when complex subresources present
|
||||
fragments:
|
||||
- 2.4.2.yml
|
||||
- 20240222-Collections-util-resource-discovery-fails-when-complex-subresources-present.yml
|
||||
release_date: '2024-03-01'
|
||||
|
||||
@@ -10,21 +10,21 @@ notesdir: fragments
|
||||
prelude_section_name: release_summary
|
||||
prelude_section_title: Release Summary
|
||||
sections:
|
||||
- - major_changes
|
||||
- Major Changes
|
||||
- - minor_changes
|
||||
- Minor Changes
|
||||
- - breaking_changes
|
||||
- Breaking Changes / Porting Guide
|
||||
- - deprecated_features
|
||||
- Deprecated Features
|
||||
- - removed_features
|
||||
- Removed Features (previously deprecated)
|
||||
- - security_fixes
|
||||
- Security Fixes
|
||||
- - bugfixes
|
||||
- Bugfixes
|
||||
- - known_issues
|
||||
- Known Issues
|
||||
- - major_changes
|
||||
- Major Changes
|
||||
- - minor_changes
|
||||
- Minor Changes
|
||||
- - breaking_changes
|
||||
- Breaking Changes / Porting Guide
|
||||
- - deprecated_features
|
||||
- Deprecated Features
|
||||
- - removed_features
|
||||
- Removed Features (previously deprecated)
|
||||
- - security_fixes
|
||||
- Security Fixes
|
||||
- - bugfixes
|
||||
- Bugfixes
|
||||
- - known_issues
|
||||
- Known Issues
|
||||
title: Kubernetes Collection
|
||||
trivial_section_name: trivial
|
||||
|
||||
3
changelogs/fragments/654-helm-expand-user.yml
Normal file
3
changelogs/fragments/654-helm-expand-user.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
bugfixes:
|
||||
- helm - expand kubeconfig path with user's home directory for consistency with k8s
|
||||
@@ -54,20 +54,20 @@ In this use case / example, we will create a Pod in the given Kubernetes Cluster
|
||||
- kubernetes.core
|
||||
tasks:
|
||||
- name: Create a pod
|
||||
k8s:
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "utilitypod-1"
|
||||
namespace: default
|
||||
labels:
|
||||
app: galaxy
|
||||
spec:
|
||||
app: galaxy
|
||||
spec:
|
||||
containers:
|
||||
- name: utilitypod
|
||||
image: busybox
|
||||
- name: utilitypod
|
||||
image: busybox
|
||||
|
||||
Since Ansible utilizes the Kubernetes API to perform actions, in this use case we will be connecting directly to the Kubernetes cluster.
|
||||
|
||||
|
||||
@@ -193,6 +193,30 @@ Parameters
|
||||
<div>Create the release namespace if not present.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>dependency_update</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>Run standelone <code>helm dependency update CHART</code> before the operation.</div>
|
||||
<div>Run inline <code>--dependency-update</code> with <code>helm install</code> command. This feature is not supported yet with the <code>helm upgrade</code> command.</div>
|
||||
<div>So we should consider to use <em>dependency_update</em> options with <em>replace</em> option enabled when specifying <em>chart_repo_url</em>.</div>
|
||||
<div>The <em>dependency_update</em> option require the add of <code>dependencies</code> block in <code>Chart.yaml/requirements.yaml</code> file.</div>
|
||||
<div>For more information please visit <a href='https://helm.sh/docs/helm/helm_dependency/'>https://helm.sh/docs/helm/helm_dependency/</a></div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: dep_up</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
@@ -413,6 +437,24 @@ Parameters
|
||||
<div>Skip custom resource definitions when installing or upgrading.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>timeout</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>A Go duration (described here <em>https://pkg.go.dev/time#ParseDuration</em>) value to wait for Kubernetes commands to complete. This defaults to 5m0s.</div>
|
||||
<div>similar to <code>wait_timeout</code> but does not required <code>wait</code> to be activated.</div>
|
||||
<div>Mutually exclusive with <code>wait_timeout</code>.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
@@ -429,7 +471,7 @@ Parameters
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>Run <code>helm repo update</code> before the operation. Can be run as part of the package installation or as a separate step.</div>
|
||||
<div>Run <code>helm repo update</code> before the operation. Can be run as part of the package installation or as a separate step (see Examples).</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -490,7 +532,8 @@ Parameters
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>Wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful.</div>
|
||||
<div>When <em>release_state</em> is set to <code>present</code>, wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful.</div>
|
||||
<div>When <em>release_state</em> is set to <code>absent</code>, will wait until all the resources are deleted before returning. It will wait for as long as <em>wait_timeout</em>. This feature requires helm>=3.7.0. Added in version 2.3.0.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -506,6 +549,7 @@ Parameters
|
||||
</td>
|
||||
<td>
|
||||
<div>Timeout when wait option is enabled (helm2 is a number of seconds, helm3 is a duration).</div>
|
||||
<div>The use of <em>wait_timeout</em> to wait for kubernetes commands to complete has been deprecated and will be removed after 2022-12-01.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -561,6 +605,13 @@ Examples
|
||||
state: absent
|
||||
wait: true
|
||||
|
||||
- name: Separately update the repository cache
|
||||
kubernetes.core.helm:
|
||||
name: dummy
|
||||
namespace: kube-system
|
||||
state: absent
|
||||
update_repo_cache: true
|
||||
|
||||
# From git
|
||||
- name: Git clone stable repo on HEAD
|
||||
ansible.builtin.git:
|
||||
|
||||
467
docs/kubernetes.core.helm_pull_module.rst
Normal file
467
docs/kubernetes.core.helm_pull_module.rst
Normal file
@@ -0,0 +1,467 @@
|
||||
.. _kubernetes.core.helm_pull_module:
|
||||
|
||||
|
||||
*************************
|
||||
kubernetes.core.helm_pull
|
||||
*************************
|
||||
|
||||
**download a chart from a repository and (optionally) unpack it in local directory.**
|
||||
|
||||
|
||||
Version added: 2.4.0
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 1
|
||||
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
- Retrieve a package from a package repository, and download it locally.
|
||||
- It can also be used to perform cryptographic verification of a chart without installing the chart.
|
||||
- There are options for unpacking the chart after download.
|
||||
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
The below requirements are needed on the host that executes this module.
|
||||
|
||||
- helm >= 3.0 (https://github.com/helm/helm/releases)
|
||||
|
||||
|
||||
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>chart_ca_cert</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>Verify certificates of HTTPS-enabled servers using this CA bundle.</div>
|
||||
<div>Requires helm >= 3.1.0.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>chart_devel</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>no</li>
|
||||
<li>yes</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>Use development versions, too. Equivalent to version '>0.0.0-0'.</div>
|
||||
<div>Mutually exclusive with <code>chart_version</code>.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>chart_ref</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>chart name on chart repository.</div>
|
||||
<div>absolute URL.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>chart_ssl_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>Identify HTTPS client using this SSL certificate file.</div>
|
||||
<div>Requires helm >= 3.1.0.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>chart_ssl_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>Identify HTTPS client using this SSL key file</div>
|
||||
<div>Requires helm >= 3.1.0.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>chart_version</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>Specify a version constraint for the chart version to use.</div>
|
||||
<div>This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0).</div>
|
||||
<div>Mutually exclusive with <code>chart_devel</code>.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>destination</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">path</span>
|
||||
/ <span style="color: red">required</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>location to write the chart.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>pass_credentials</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>Pass credentials to all domains.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>provenance</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>Fetch the provenance file, but don't perform verification.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>repo_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>Chart repository password where to locate the requested chart.</div>
|
||||
<div>Required if <code>repo_username</code> is specified.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: password, chart_repo_password</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>repo_url</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>chart repository url where to locate the requested chart.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: url, chart_repo_url</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>repo_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>Chart repository username where to locate the requested chart.</div>
|
||||
<div>Required if <code>repo_password</code> is specified.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: username, chart_repo_username</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>skip_tls_certs_check</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>Whether or not to check tls certificate for the chart download.</div>
|
||||
<div>Requires helm >= 3.3.0.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>untar_chart</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>if set to true, will untar the chart after downloading it.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>verify_chart</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>Verify the package before using it.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>verify_chart_keyring</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>location of public keys used for verification.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- name: Download chart using chart url
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz
|
||||
destination: /path/to/chart
|
||||
|
||||
- name: Download Chart using chart_name and repo_url
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: redis
|
||||
repo_url: https://charts.bitnami.com/bitnami
|
||||
untar_chart: yes
|
||||
destination: /path/to/chart
|
||||
|
||||
- name: Download Chart (skip tls certificate check)
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: redis
|
||||
repo_url: https://charts.bitnami.com/bitnami
|
||||
untar_chart: yes
|
||||
destination: /path/to/chart
|
||||
skip_tls_certs_check: yes
|
||||
|
||||
- name: Download Chart using chart registry credentials
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: redis
|
||||
repo_url: https://charts.bitnami.com/bitnami
|
||||
untar_chart: yes
|
||||
destination: /path/to/chart
|
||||
username: myuser
|
||||
password: mypassword123
|
||||
|
||||
|
||||
|
||||
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 `helm pull` command built by this module, in case you want to re-run the command outside the module or debug a problem.</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;">helm pull --repo test ...</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>rc</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">integer</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>always</td>
|
||||
<td>
|
||||
<div>Helm pull command return code</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;">1</div>
|
||||
</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 `helm pull` 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 `helm pull` command stdout, in case you want to display it or examine the event log</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/><br/>
|
||||
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
- Aubin Bikouo (@abikouo)
|
||||
@@ -96,6 +96,28 @@ Parameters
|
||||
<div>Chart version to use. If this is not specified, the latest version is installed.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>dependency_update</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>Run helm dependency update before the operation.</div>
|
||||
<div>The <em>dependency_update</em> option require the add of <code>dependencies</code> block in <code>Chart.yaml/requirements.yaml</code> file.</div>
|
||||
<div>For more information please visit <a href='https://helm.sh/docs/helm/helm_dependency/'>https://helm.sh/docs/helm/helm_dependency/</a></div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: dep_up</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -136,6 +136,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -167,6 +167,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -140,6 +140,26 @@ Parameters
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>delete_emptydir_data</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>Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained)</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>disable_eviction</b>
|
||||
@@ -266,6 +286,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -166,6 +166,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -173,6 +173,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -155,6 +155,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -174,6 +174,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -351,6 +351,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -172,6 +172,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -189,6 +189,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
@@ -172,6 +172,41 @@ Parameters
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
|
||||
660
docs/kubernetes.core.k8s_taint_module.rst
Normal file
660
docs/kubernetes.core.k8s_taint_module.rst
Normal file
@@ -0,0 +1,660 @@
|
||||
.. _kubernetes.core.k8s_taint_module:
|
||||
|
||||
|
||||
*************************
|
||||
kubernetes.core.k8s_taint
|
||||
*************************
|
||||
|
||||
**Taint a node in a Kubernetes/OpenShift cluster**
|
||||
|
||||
|
||||
Version added: 2.3.0
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 1
|
||||
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
- Taint allows a node to refuse Pod to be scheduled unless that Pod has a matching toleration.
|
||||
- Untaint will remove taints from nodes as needed.
|
||||
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
The below requirements are needed on the host that executes this module.
|
||||
|
||||
- python >= 3.6
|
||||
- kubernetes >= 12.0.0
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<table border=0 cellpadding=0 class="documentation-table">
|
||||
<tr>
|
||||
<th colspan="2">Parameter</th>
|
||||
<th>Choices/<font color="blue">Defaults</font></th>
|
||||
<th width="100%">Comments</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>api_key</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>Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>ca_cert</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 a CA certificate used to authenticate with the API. The full certificate chain must be provided to avoid certificate validation errors. Can also be specified via K8S_AUTH_SSL_CA_CERT environment variable.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: ssl_ca_cert</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>client_cert</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 a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE environment variable.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: cert_file</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>client_key</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 a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE environment variable.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: key_file</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>context</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>The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_groups</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Group(s) to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>impersonate_user</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Username to impersonate for the operation.</div>
|
||||
<div>Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>kubeconfig</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">raw</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>Path to an existing Kubernetes config file. If not provided, and no other connection options are provided, the Kubernetes client will attempt to load the default configuration file from <em>~/.kube/config</em>. Can also be specified via K8S_AUTH_KUBECONFIG environment variable.</div>
|
||||
<div>The kubernetes configuration can be provided as dictionary. This feature requires a python kubernetes client version >= 17.17.0. Added in version 2.2.0.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>name</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>The name of the node.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>no_proxy</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>The comma separated list of hosts/domains/IP/CIDR that shouldn't go through proxy. Can also be specified via K8S_AUTH_NO_PROXY environment variable.</div>
|
||||
<div>Please note that this module does not pick up typical proxy settings from the environment (e.g. NO_PROXY).</div>
|
||||
<div>This feature requires kubernetes>=19.15.0. When kubernetes library is less than 19.15.0, it fails even no_proxy set in correct.</div>
|
||||
<div>example value is "localhost,.local,.example.com,127.0.0.1,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<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>Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD environment variable.</div>
|
||||
<div>Please read the description of the <code>username</code> option for a discussion of when this option is applicable.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>persist_config</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>no</li>
|
||||
<li>yes</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>Whether or not to save the kube config refresh tokens. Can also be specified via K8S_AUTH_PERSIST_CONFIG environment variable.</div>
|
||||
<div>When the k8s context is using a user credentials with refresh tokens (like oidc or gke/gcloud auth), the token is refreshed by the k8s python client library but not saved by default. So the old refresh token can expire and the next auth might fail. Setting this flag to true will tell the k8s python client to save the new refresh token to the kube config file.</div>
|
||||
<div>Default to false.</div>
|
||||
<div>Please note that the current version of the k8s python client library does not support setting this flag to True yet.</div>
|
||||
<div>The fix for this k8s python library is here: https://github.com/kubernetes-client/python-base/pull/169</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>proxy</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>The URL of an HTTP proxy to use for the connection. Can also be specified via K8S_AUTH_PROXY environment variable.</div>
|
||||
<div>Please note that this module does not pick up typical proxy settings from the environment (e.g. HTTP_PROXY).</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>proxy_headers</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">dictionary</span>
|
||||
</div>
|
||||
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.0.0</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>The Header used for the HTTP proxy.</div>
|
||||
<div>Documentation can be found here <a href='https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html?highlight=proxy_headers#urllib3.util.make_headers'>https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html?highlight=proxy_headers#urllib3.util.make_headers</a>.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>basic_auth</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>Colon-separated username:password for basic authentication header.</div>
|
||||
<div>Can also be specified via K8S_AUTH_PROXY_HEADERS_BASIC_AUTH environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>proxy_basic_auth</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>Colon-separated username:password for proxy basic authentication header.</div>
|
||||
<div>Can also be specified via K8S_AUTH_PROXY_HEADERS_PROXY_BASIC_AUTH environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>user_agent</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>String representing the user-agent you want, such as foo/1.0.</div>
|
||||
<div>Can also be specified via K8S_AUTH_PROXY_HEADERS_USER_AGENT environment.</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>replace</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>If <code>true</code>, allow taints to be replaced.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<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>Determines whether to add or remove taints.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>taints</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: purple">elements=dictionary</span>
|
||||
/ <span style="color: red">required</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>List containing the taints.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>effect</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>NoSchedule</li>
|
||||
<li>NoExecute</li>
|
||||
<li>PreferNoSchedule</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>The effect of the taint on Pods that do not tolerate the taint.</div>
|
||||
<div>Required when <em>state=present</em>.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>key</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>The taint key to be applied to a node.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"></td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>value</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>The taint value corresponding to the taint key.</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<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>Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME environment variable.</div>
|
||||
<div>Please note that this only works with clusters configured to use HTTP Basic Auth. If your cluster has a different form of authentication (e.g. OAuth2 in OpenShift), this option will not work as expected and you should look into the <span class='module'>community.okd.k8s_auth</span> module, as that might do what you need.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>validate_certs</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>no</li>
|
||||
<li>yes</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<div>Whether or not to verify the API server's SSL certificates. Can also be specified via K8S_AUTH_VERIFY_SSL environment variable.</div>
|
||||
<div style="font-size: small; color: darkgreen"><br/>aliases: verify_ssl</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
.. note::
|
||||
- To avoid SSL certificate validation errors when ``validate_certs`` is *True*, the full certificate chain for the API server must be provided via ``ca_cert`` or in the kubeconfig file.
|
||||
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- name: Taint node "foo"
|
||||
kubernetes.core.k8s_taint:
|
||||
state: present
|
||||
name: foo
|
||||
taints:
|
||||
- effect: NoExecute
|
||||
key: "key1"
|
||||
|
||||
- name: Taint node "foo"
|
||||
kubernetes.core.k8s_taint:
|
||||
state: present
|
||||
name: foo
|
||||
taints:
|
||||
- effect: NoExecute
|
||||
key: "key1"
|
||||
value: "value1"
|
||||
- effect: NoSchedule
|
||||
key: "key1"
|
||||
value: "value1"
|
||||
|
||||
- name: Remove taint from "foo".
|
||||
kubernetes.core.k8s_taint:
|
||||
state: absent
|
||||
name: foo
|
||||
taints:
|
||||
- effect: NoExecute
|
||||
key: "key1"
|
||||
value: "value1"
|
||||
|
||||
|
||||
|
||||
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="2">Key</th>
|
||||
<th>Returned</th>
|
||||
<th width="100%">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>result</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">complex</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>success</td>
|
||||
<td>
|
||||
<div>The tainted Node object. Will be empty in the case of a deletion.</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"> </td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>api_version</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>success</td>
|
||||
<td>
|
||||
<div>The versioned schema of this representation of an object.</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"> </td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>kind</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>success</td>
|
||||
<td>
|
||||
<div>Represents the REST resource this object represents.</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"> </td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>metadata</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">complex</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>success</td>
|
||||
<td>
|
||||
<div>Standard object metadata. Includes name, namespace, annotations, labels, etc.</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"> </td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>spec</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">complex</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>success</td>
|
||||
<td>
|
||||
<div>Specific attributes of the object. Will vary based on the <em>api_version</em> and <em>kind</em>.</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="elbow-placeholder"> </td>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>status</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">complex</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>success</td>
|
||||
<td>
|
||||
<div>Current status details for the object.</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<br/><br/>
|
||||
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
- Alina Buzachis (@alinabuzachis)
|
||||
@@ -9,8 +9,8 @@ authors:
|
||||
- mmazur (https://github.com/mmazur)
|
||||
- jamescassell (https://github.com/jamescassell)
|
||||
description: Kubernetes Collection for Ansible.
|
||||
documentation: ''
|
||||
homepage: ''
|
||||
documentation: ""
|
||||
homepage: ""
|
||||
issues: https://github.com/ansible-collections/kubernetes.core/issues
|
||||
license_file: LICENSE
|
||||
namespace: kubernetes
|
||||
@@ -25,7 +25,7 @@ tags:
|
||||
- openshift
|
||||
- okd
|
||||
- cluster
|
||||
version: 2.2.0
|
||||
version: 2.4.1
|
||||
build_ignore:
|
||||
- .DS_Store
|
||||
- '*.tar.gz'
|
||||
- "*.tar.gz"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
requires_ansible: '>=2.9.17'
|
||||
requires_ansible: ">=2.9.17"
|
||||
|
||||
action_groups:
|
||||
helm:
|
||||
@@ -9,7 +9,6 @@ action_groups:
|
||||
k8s:
|
||||
- k8s
|
||||
- k8s_exec
|
||||
- k8s_facts
|
||||
- k8s_info
|
||||
- k8s_log
|
||||
- k8s_scale
|
||||
@@ -18,37 +17,6 @@ action_groups:
|
||||
- k8s_drain
|
||||
|
||||
plugin_routing:
|
||||
action:
|
||||
helm:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
helm_info:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
helm_plugin:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
helm_plugin_info:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
helm_repository:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_cluster_info:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_cp:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_drain:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_event_info:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_exec:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_log:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_rollback:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_scale:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
k8s_service:
|
||||
redirect: kubernetes.core.k8s_info
|
||||
inventory:
|
||||
openshift:
|
||||
redirect: community.okd.openshift
|
||||
@@ -61,18 +29,18 @@ plugin_routing:
|
||||
warning_text: Use kubernetes.core.k8s_info instead.
|
||||
k8s_raw:
|
||||
tombstone:
|
||||
removal_version: 0.1.0
|
||||
removal_version: "0.1.0"
|
||||
warning_text: The k8s_raw module was slated for deprecation in Ansible 2.10 and has been removed. Use kubernetes.core.k8s instead.
|
||||
openshift_raw:
|
||||
tombstone:
|
||||
removal_version: 0.1.0
|
||||
removal_version: "0.1.0"
|
||||
warning_text: The openshift_raw module was slated for deprecation in Ansible 2.10 and has been removed. Use kubernetes.core.k8s instead.
|
||||
openshift_scale:
|
||||
tombstone:
|
||||
removal_version: 0.1.0
|
||||
removal_version: "0.1.0"
|
||||
warning_text: The openshift_scale module was slated for deprecation in Ansible 2.10 and has been removed. Use kubernetes.core.k8s_scale instead.
|
||||
lookup:
|
||||
openshift:
|
||||
tombstone:
|
||||
removal_version: 0.1.0
|
||||
removal_version: "0.1.0"
|
||||
warning_text: The openshift lookup plugin was slated for deprecation in Ansible 2.10 and has been removed. Use kubernetes.core.k8s instead.
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
---
|
||||
- name: Converge
|
||||
hosts: localhost
|
||||
connection: local
|
||||
|
||||
collections:
|
||||
- kubernetes.core
|
||||
|
||||
vars_files:
|
||||
- vars/main.yml
|
||||
|
||||
tasks:
|
||||
- name: Verify cluster is working.
|
||||
k8s_info:
|
||||
namespace: kube-system
|
||||
kind: Pod
|
||||
register: pod_list
|
||||
|
||||
- name: Verify cluster has more than 5 pods running.
|
||||
assert:
|
||||
that: (pod_list.resources | count) > 5
|
||||
|
||||
- name: Include access_review.yml
|
||||
include_tasks:
|
||||
file: tasks/access_review.yml
|
||||
apply:
|
||||
tags: [ access_review, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include append_hash.yml
|
||||
include_tasks:
|
||||
file: tasks/append_hash.yml
|
||||
apply:
|
||||
tags: [ append_hash, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include apply.yml
|
||||
include_tasks:
|
||||
file: tasks/apply.yml
|
||||
apply:
|
||||
tags: [ apply, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include cluster_info.yml
|
||||
include_tasks:
|
||||
file: tasks/cluster_info.yml
|
||||
apply:
|
||||
tags: [ cluster_info, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include crd.yml
|
||||
include_tasks:
|
||||
file: tasks/crd.yml
|
||||
apply:
|
||||
tags: [ crd, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include delete.yml
|
||||
include_tasks:
|
||||
file: tasks/delete.yml
|
||||
apply:
|
||||
tags: [ delete, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include exec.yml
|
||||
include_tasks:
|
||||
file: tasks/exec.yml
|
||||
apply:
|
||||
tags: [ exec, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include full.yml
|
||||
include_tasks:
|
||||
file: tasks/full.yml
|
||||
apply:
|
||||
tags: [ full, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include gc.yml
|
||||
include_tasks:
|
||||
file: tasks/gc.yml
|
||||
apply:
|
||||
tags: [ gc, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include info.yml
|
||||
include_tasks:
|
||||
file: tasks/info.yml
|
||||
apply:
|
||||
tags: [ info, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include json_patch.yml
|
||||
include_tasks:
|
||||
file: tasks/json_patch.yml
|
||||
apply:
|
||||
tags: [ json_patch, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include lists.yml
|
||||
include_tasks:
|
||||
file: tasks/lists.yml
|
||||
apply:
|
||||
tags: [ lists, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include log.yml
|
||||
include_tasks:
|
||||
file: tasks/log.yml
|
||||
apply:
|
||||
tags: [ log, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include rollback.yml
|
||||
include_tasks:
|
||||
file: tasks/rollback.yml
|
||||
apply:
|
||||
tags: [ rollback, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include scale.yml
|
||||
include_tasks:
|
||||
file: tasks/scale.yml
|
||||
apply:
|
||||
tags: [ scale, k8s ]
|
||||
tags:
|
||||
- always
|
||||
- name: Include template.yml
|
||||
include_tasks:
|
||||
file: tasks/template.yml
|
||||
apply:
|
||||
tags: [ template, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include validate.yml
|
||||
include_tasks:
|
||||
file: tasks/validate.yml
|
||||
apply:
|
||||
tags: [ validate, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include waiter.yml
|
||||
include_tasks:
|
||||
file: tasks/waiter.yml
|
||||
apply:
|
||||
tags: [ waiter, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include merge_type.yml
|
||||
include_tasks:
|
||||
file: tasks/merge_type.yml
|
||||
apply:
|
||||
tags: [ merge_type, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include patched.yml
|
||||
include_tasks:
|
||||
file: tasks/patched.yml
|
||||
apply:
|
||||
tags: [ patched, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include lookup_k8s.yml
|
||||
include_tasks:
|
||||
file: tasks/lookup_k8s.yml
|
||||
apply:
|
||||
tags: [ lookup_k8s, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include label_selectors.yml
|
||||
include_tasks:
|
||||
file: tasks/label_selectors.yml
|
||||
apply:
|
||||
tags: [ label_selectors, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include diff.yml
|
||||
include_tasks:
|
||||
file: tasks/diff.yml
|
||||
apply:
|
||||
tags: [ diff, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Include lookup_kustomize.yml
|
||||
include_tasks:
|
||||
file: tasks/lookup_kustomize.yml
|
||||
apply:
|
||||
tags: [ lookup_kustomize, k8s ]
|
||||
tags:
|
||||
- always
|
||||
|
||||
roles:
|
||||
- role: helm
|
||||
tags:
|
||||
- helm
|
||||
|
||||
- role: k8scopy
|
||||
tags:
|
||||
- copy
|
||||
- k8s
|
||||
|
||||
post_tasks:
|
||||
- name: Ensure namespace exists
|
||||
k8s:
|
||||
api_version: v1
|
||||
kind: Namespace
|
||||
name: inventory
|
||||
|
||||
- name: Add a deployment
|
||||
k8s:
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: inventory
|
||||
namespace: inventory
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: "{{ k8s_pod_name }}"
|
||||
template: "{{ k8s_pod_template }}"
|
||||
wait: yes
|
||||
wait_timeout: 120
|
||||
vars:
|
||||
k8s_pod_name: inventory
|
||||
k8s_pod_image: python
|
||||
k8s_pod_command:
|
||||
- python
|
||||
- '-m'
|
||||
- http.server
|
||||
k8s_pod_env:
|
||||
- name: TEST
|
||||
value: test
|
||||
|
||||
- meta: refresh_inventory
|
||||
|
||||
- name: Verify inventory and connection plugins
|
||||
hosts: namespace_inventory_pods
|
||||
gather_facts: no
|
||||
|
||||
vars:
|
||||
file_content: |
|
||||
Hello world
|
||||
|
||||
tasks:
|
||||
- name: End play if host not running (TODO should we not add these to the inventory?)
|
||||
meta: end_host
|
||||
when: pod_phase != "Running"
|
||||
|
||||
- debug: var=hostvars
|
||||
- setup:
|
||||
|
||||
- debug: var=ansible_facts
|
||||
|
||||
- name: Assert the TEST environment variable was retrieved
|
||||
assert:
|
||||
that: ansible_facts.env.TEST == 'test'
|
||||
|
||||
- name: Copy a file into the host
|
||||
copy:
|
||||
content: '{{ file_content }}'
|
||||
dest: /tmp/test_file
|
||||
|
||||
- name: Retrieve the file from the host
|
||||
slurp:
|
||||
src: /tmp/test_file
|
||||
register: slurped_file
|
||||
|
||||
- name: Assert the file content matches expectations
|
||||
assert:
|
||||
that: (slurped_file.content|b64decode) == file_content
|
||||
|
||||
- name: Delete inventory namespace
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Remove inventory namespace
|
||||
k8s:
|
||||
api_version: v1
|
||||
kind: Namespace
|
||||
name: inventory
|
||||
state: absent
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
driver:
|
||||
name: delegated
|
||||
options:
|
||||
managed: false
|
||||
login_cmd_template: 'docker exec -ti {instance} bash'
|
||||
ansible_connection_options:
|
||||
ansible_connection: docker
|
||||
platforms:
|
||||
- name: instance-kind
|
||||
provisioner:
|
||||
name: ansible
|
||||
log: true
|
||||
config_options:
|
||||
inventory:
|
||||
enable_plugins: kubernetes.core.k8s,yaml
|
||||
lint: {}
|
||||
inventory:
|
||||
hosts:
|
||||
plugin: kubernetes.core.k8s
|
||||
host_vars:
|
||||
localhost:
|
||||
ansible_python_interpreter: '{{ ansible_playbook_python }}'
|
||||
env:
|
||||
ANSIBLE_FORCE_COLOR: 'true'
|
||||
options:
|
||||
vvv: True
|
||||
scenario:
|
||||
name: default
|
||||
test_sequence:
|
||||
- dependency
|
||||
- syntax
|
||||
- prepare
|
||||
- converge
|
||||
- verify
|
||||
dependency:
|
||||
name: galaxy
|
||||
options:
|
||||
requirements-file: requirements.yml
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
- name: Prepare
|
||||
hosts: localhost
|
||||
connection: local
|
||||
|
||||
collections:
|
||||
- kubernetes.core
|
||||
|
||||
tasks:
|
||||
- name: Include drain.yml
|
||||
include_tasks:
|
||||
file: tasks/drain.yml
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
- name: Init Helm folders
|
||||
file:
|
||||
path: /tmp/helm/
|
||||
state: directory
|
||||
|
||||
- name: Unarchive Helm binary
|
||||
unarchive:
|
||||
src: 'https://get.helm.sh/{{ helm_archive_name }}'
|
||||
dest: /tmp/helm/
|
||||
remote_src: yes
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
- name: Install Chart from URL
|
||||
include_tasks: "../tests_chart.yml"
|
||||
vars:
|
||||
source: url
|
||||
chart_source: "https://github.com/kubernetes/ingress-nginx/releases/download/{{ chart_test }}-{{ chart_test_version }}/{{ chart_test }}-{{ chart_test_version }}.tgz"
|
||||
chart_source_upgrade: "https://github.com/kubernetes/ingress-nginx/releases/download/{{ chart_test }}-{{ chart_test_version_upgrade }}/{{ chart_test }}-{{ chart_test_version_upgrade }}.tgz"
|
||||
Binary file not shown.
@@ -1,217 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2021, Aubin Bikouo <@abikouo>
|
||||
# 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: kubectl_file_compare
|
||||
|
||||
short_description: Compare file and directory using kubectl
|
||||
|
||||
author:
|
||||
- Aubin Bikouo (@abikouo)
|
||||
|
||||
description:
|
||||
- This module is used to validate k8s_cp module.
|
||||
- Compare the local file/directory with the remote pod version
|
||||
|
||||
notes:
|
||||
- This module authenticates on kubernetes cluster using default kubeconfig only.
|
||||
|
||||
options:
|
||||
namespace:
|
||||
description:
|
||||
- The pod namespace name
|
||||
type: str
|
||||
required: yes
|
||||
pod:
|
||||
description:
|
||||
- The pod name
|
||||
type: str
|
||||
required: yes
|
||||
container:
|
||||
description:
|
||||
- The container to retrieve files from.
|
||||
type: str
|
||||
remote_path:
|
||||
description:
|
||||
- Path of the file or directory on Pod.
|
||||
type: path
|
||||
required: yes
|
||||
local_path:
|
||||
description:
|
||||
- Path of the local file or directory.
|
||||
type: path
|
||||
content:
|
||||
description:
|
||||
- local content to compare with remote file from pod.
|
||||
- mutually exclusive with option I(local_path).
|
||||
type: path
|
||||
required: yes
|
||||
args:
|
||||
description:
|
||||
- The file is considered to be an executable.
|
||||
- The tool will be run locally and on pod and compare result from output and stderr.
|
||||
type: list
|
||||
kubectl_path:
|
||||
description:
|
||||
- Path to the kubectl executable, if not specified it will be download.
|
||||
type: path
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: compare local /tmp/foo with /tmp/bar in a remote pod
|
||||
kubectl_file_compare:
|
||||
namespace: some-namespace
|
||||
pod: some-pod
|
||||
remote_path: /tmp/bar
|
||||
local_path: /tmp/foo
|
||||
kubectl_path: /tmp/test/kubectl
|
||||
|
||||
- name: Compare executable running help command
|
||||
kubectl_file_compare:
|
||||
namespace: some-namespace
|
||||
pod: some-pod
|
||||
remote_path: /tmp/test/kubectl
|
||||
local_path: kubectl
|
||||
kubectl_path: /tmp/test/kubectl
|
||||
args:
|
||||
- "--help"
|
||||
'''
|
||||
|
||||
|
||||
RETURN = r'''
|
||||
'''
|
||||
|
||||
import os
|
||||
import filecmp
|
||||
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def kubectl_get_content(module, dest_dir):
|
||||
kubectl_path = module.params.get('kubectl_path')
|
||||
if kubectl_path is None:
|
||||
kubectl_path = module.get_bin_path('kubectl', required=True)
|
||||
|
||||
namespace = module.params.get('namespace')
|
||||
pod = module.params.get('pod')
|
||||
file = module.params.get('remote_path')
|
||||
|
||||
cmd = [
|
||||
kubectl_path,
|
||||
'cp',
|
||||
"{0}/{1}:{2}".format(namespace, pod, file)
|
||||
]
|
||||
container = module.params.get('container')
|
||||
if container:
|
||||
cmd += ['-c', container]
|
||||
local_file = os.path.join(dest_dir, os.path.basename(module.params.get('remote_path')))
|
||||
cmd.append(local_file)
|
||||
rc, out, err = module.run_command(cmd)
|
||||
return local_file, err, rc, out
|
||||
|
||||
|
||||
def kubectl_run_from_pod(module):
|
||||
kubectl_path = module.params.get('kubectl_path')
|
||||
if kubectl_path is None:
|
||||
kubectl_path = module.get_bin_path('kubectl', required=True)
|
||||
|
||||
cmd = [
|
||||
kubectl_path,
|
||||
'exec',
|
||||
module.params.get('pod'),
|
||||
'-n',
|
||||
module.params.get('namespace')
|
||||
]
|
||||
container = module.params.get('container')
|
||||
if container:
|
||||
cmd += ['-c', container]
|
||||
cmd += ['--', module.params.get('remote_path')]
|
||||
cmd += module.params.get('args')
|
||||
return module.run_command(cmd)
|
||||
|
||||
|
||||
def compare_directories(dir1, dir2):
|
||||
test = filecmp.dircmp(dir1, dir2)
|
||||
if any([len(test.left_only) > 0, len(test.right_only) > 0, len(test.funny_files) > 0]):
|
||||
return False
|
||||
(t, mismatch, errors) = filecmp.cmpfiles(dir1, dir2, test.common_files, shallow=False)
|
||||
if len(mismatch) > 0 or len(errors) > 0:
|
||||
return False
|
||||
for common_dir in test.common_dirs:
|
||||
new_dir1 = os.path.join(dir1, common_dir)
|
||||
new_dir2 = os.path.join(dir2, common_dir)
|
||||
if not compare_directories(new_dir1, new_dir2):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def execute_module(module):
|
||||
|
||||
args = module.params.get('args')
|
||||
local_path = module.params.get('local_path')
|
||||
namespace = module.params.get('namespace')
|
||||
pod = module.params.get('pod')
|
||||
file = module.params.get('remote_path')
|
||||
content = module.params.get('content')
|
||||
if args:
|
||||
pod_rc, pod_out, pod_err = kubectl_run_from_pod(module)
|
||||
rc, out, err = module.run_command([module.params.get('local_path')] + args)
|
||||
if rc == pod_rc and out == pod_out:
|
||||
module.exit_json(msg="{0} and {1}/{2}:{3} are same.".format(
|
||||
local_path, namespace, pod, file
|
||||
), rc=rc, stderr=err, stdout=out)
|
||||
result = dict(local=dict(rc=rc, out=out, err=err), remote=dict(rc=pod_rc, out=pod_out, err=pod_err))
|
||||
module.fail_json(msg=f"{local_path} and {namespace}/{pod}:{file} are same.", **result)
|
||||
else:
|
||||
with TemporaryDirectory() as tmpdirname:
|
||||
file_from_pod, err, rc, out = kubectl_get_content(module=module, dest_dir=tmpdirname)
|
||||
if not os.path.exists(file_from_pod):
|
||||
module.fail_json(msg="failed to copy content from pod", error=err, output=out)
|
||||
|
||||
if content is not None:
|
||||
with NamedTemporaryFile(mode="w") as tmp_file:
|
||||
tmp_file.write(content)
|
||||
tmp_file.flush()
|
||||
if filecmp.cmp(file_from_pod, tmp_file.name):
|
||||
module.exit_json(msg=f"defined content and {namespace}/{pod}:{file} are same.")
|
||||
module.fail_json(msg=f"defined content and {namespace}/{pod}:{file} are same.")
|
||||
|
||||
if os.path.isfile(local_path):
|
||||
if filecmp.cmp(file_from_pod, local_path):
|
||||
module.exit_json(msg=f"{local_path} and {namespace}/{pod}:{file} are same.")
|
||||
module.fail_json(msg=f"{local_path} and {namespace}/{pod}:{file} are same.")
|
||||
|
||||
if os.path.isdir(local_path):
|
||||
if compare_directories(file_from_pod, local_path):
|
||||
module.exit_json(msg=f"{local_path} and {namespace}/{pod}:{file} are same.")
|
||||
module.fail_json(msg=f"{local_path} and {namespace}/{pod}:{file} are same.")
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = {}
|
||||
argument_spec['namespace'] = {'type': 'str', 'required': True}
|
||||
argument_spec['pod'] = {'type': 'str', 'required': True}
|
||||
argument_spec['container'] = {}
|
||||
argument_spec['remote_path'] = {'type': 'path', 'required': True}
|
||||
argument_spec['local_path'] = {'type': 'path'}
|
||||
argument_spec['content'] = {'type': 'str'}
|
||||
argument_spec['kubectl_path'] = {'type': 'path'}
|
||||
argument_spec['args'] = {'type': 'list'}
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
mutually_exclusive=[('local_path', 'content')],
|
||||
required_one_of=[['local_path', 'content']])
|
||||
|
||||
execute_module(module)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
- vars:
|
||||
exec_namespace: k8s-exec
|
||||
pod: sleep-pod
|
||||
exec_pod_definition:
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ pod }}"
|
||||
namespace: "{{ exec_namespace }}"
|
||||
spec:
|
||||
containers:
|
||||
- name: sleeper
|
||||
image: busybox
|
||||
command: ["sleep", "infinity"]
|
||||
|
||||
block:
|
||||
- name: "Ensure that {{ exec_namespace }} namespace exists"
|
||||
k8s:
|
||||
kind: Namespace
|
||||
name: "{{ exec_namespace }}"
|
||||
|
||||
- name: "Create a pod"
|
||||
k8s:
|
||||
definition: "{{ exec_pod_definition }}"
|
||||
wait: yes
|
||||
wait_sleep: 1
|
||||
wait_timeout: 30
|
||||
|
||||
- name: "Execute a command"
|
||||
k8s_exec:
|
||||
pod: "{{ pod }}"
|
||||
namespace: "{{ exec_namespace }}"
|
||||
command: cat /etc/resolv.conf
|
||||
register: output
|
||||
|
||||
- name: "Show k8s_exec output"
|
||||
debug:
|
||||
var: output
|
||||
|
||||
- name: "Assert k8s_exec output is correct"
|
||||
assert:
|
||||
that:
|
||||
- "'nameserver' in output.stdout"
|
||||
|
||||
- name: Check if rc is returned for the given command
|
||||
k8s_exec:
|
||||
namespace: "{{ exec_namespace }}"
|
||||
pod: "{{ pod }}"
|
||||
command: 'false'
|
||||
register: command_status
|
||||
ignore_errors: True
|
||||
|
||||
- name: Check last command status
|
||||
assert:
|
||||
that:
|
||||
- command_status.rc != 0
|
||||
- command_status.return_code != 0
|
||||
|
||||
always:
|
||||
- name: "Cleanup namespace"
|
||||
k8s:
|
||||
kind: Namespace
|
||||
name: "{{ exec_namespace }}"
|
||||
state: absent
|
||||
@@ -1,124 +0,0 @@
|
||||
---
|
||||
- block:
|
||||
- name: ensure that k8s-log namespace exists
|
||||
k8s:
|
||||
kind: Namespace
|
||||
name: k8s-log
|
||||
|
||||
- name: create hello-world deployment
|
||||
k8s:
|
||||
wait: yes
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hello-world
|
||||
namespace: k8s-log
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hello-world
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hello-world
|
||||
spec:
|
||||
containers:
|
||||
- image: busybox
|
||||
name: hello-world
|
||||
command: ['sh']
|
||||
args: ['-c', 'while true ; do echo "hello world" && sleep 10 ; done']
|
||||
restartPolicy: Always
|
||||
|
||||
- name: retrieve the log by providing the deployment
|
||||
k8s_log:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: k8s-log
|
||||
name: hello-world
|
||||
register: deployment_log
|
||||
|
||||
- name: verify that the log can be retrieved via the deployment
|
||||
assert:
|
||||
that:
|
||||
- "'hello world' in deployment_log.log"
|
||||
- item == 'hello world' or item == ''
|
||||
with_items: '{{ deployment_log.log_lines }}'
|
||||
|
||||
- name: retrieve the log with a label selector
|
||||
k8s_log:
|
||||
namespace: k8s-log
|
||||
label_selectors:
|
||||
- 'app=hello-world'
|
||||
register: label_selector_log
|
||||
|
||||
- name: verify that the log can be retrieved via the label
|
||||
assert:
|
||||
that:
|
||||
- "'hello world' in label_selector_log.log"
|
||||
- item == 'hello world' or item == ''
|
||||
with_items: '{{ label_selector_log.log_lines }}'
|
||||
|
||||
- name: get the hello-world pod
|
||||
k8s_info:
|
||||
kind: Pod
|
||||
namespace: k8s-log
|
||||
label_selectors:
|
||||
- 'app=hello-world'
|
||||
register: k8s_log_pods
|
||||
|
||||
- name: retrieve the log directly with the pod name
|
||||
k8s_log:
|
||||
namespace: k8s-log
|
||||
name: '{{ k8s_log_pods.resources.0.metadata.name }}'
|
||||
register: pod_log
|
||||
|
||||
- name: verify that the log can be retrieved via the pod name
|
||||
assert:
|
||||
that:
|
||||
- "'hello world' in pod_log.log"
|
||||
- item == 'hello world' or item == ''
|
||||
with_items: '{{ pod_log.log_lines }}'
|
||||
|
||||
- name: Create a job that calculates 7
|
||||
k8s:
|
||||
state: present
|
||||
wait: yes
|
||||
wait_timeout: 120
|
||||
wait_condition:
|
||||
type: Complete
|
||||
status: 'True'
|
||||
definition:
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: int-log
|
||||
namespace: k8s-log
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: busybox
|
||||
image: busybox
|
||||
command: ["echo", "7"]
|
||||
restartPolicy: Never
|
||||
backoffLimit: 4
|
||||
|
||||
- name: retrieve logs from the job
|
||||
k8s_log:
|
||||
api_version: batch/v1
|
||||
kind: Job
|
||||
namespace: k8s-log
|
||||
name: int-log
|
||||
register: job_logs
|
||||
|
||||
- name: verify the log was successfully retrieved
|
||||
assert:
|
||||
that: job_logs.log_lines[0] == "7"
|
||||
|
||||
always:
|
||||
- name: ensure that namespace is removed
|
||||
k8s:
|
||||
kind: Namespace
|
||||
name: k8s-log
|
||||
state: absent
|
||||
@@ -1,112 +0,0 @@
|
||||
---
|
||||
- block:
|
||||
- set_fact:
|
||||
pre_test1: "{{ lookup('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development') }}"
|
||||
pre_test2: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one') }}"
|
||||
pre_test3: "{{ query('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development') }}"
|
||||
pre_test4: "{{ query('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one') }}"
|
||||
|
||||
# https://github.com/ansible-collections/kubernetes.core/issues/147
|
||||
- name: Create a namespace with label
|
||||
kubernetes.core.k8s:
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "app-development-one"
|
||||
labels:
|
||||
namespace_label: "app_development"
|
||||
|
||||
- set_fact:
|
||||
test1: "{{ lookup('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development', wantlist=True) }}"
|
||||
test2: "{{ query('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development') }}"
|
||||
test3: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one', wantlist=True) }}"
|
||||
test4: "{{ query('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one') }}"
|
||||
test5: "{{ lookup('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development') }}"
|
||||
test6: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one') }}"
|
||||
test7: "{{ lookup('kubernetes.core.k8s', kind='Ingress', api_version='networking.k8s.io/vINVALID', errors='ignore') }}"
|
||||
|
||||
- set_fact:
|
||||
test8: "{{ lookup('kubernetes.core.k8s', kind='Ingress', api_version='networking.k8s.io/vINVALID') }}"
|
||||
ignore_errors: true
|
||||
|
||||
- name: Assert that every test is passed
|
||||
assert:
|
||||
that:
|
||||
# Before creating object
|
||||
- pre_test1 is sequence and pre_test1 is not string
|
||||
- pre_test1 | length == 0
|
||||
- pre_test2 is sequence and pre_test2 is not string
|
||||
- pre_test2 | length == 0
|
||||
- pre_test3 is sequence and pre_test3 is not string
|
||||
- pre_test3 | length == 0
|
||||
- pre_test4 is sequence and pre_test4 is not string
|
||||
- pre_test4 | length == 0
|
||||
# After creating object
|
||||
- test1 is sequence and test1 is not string
|
||||
- test1 | length == 1
|
||||
- test2 is sequence and test2 is not string
|
||||
- test2 | length == 1
|
||||
- test3 is sequence and test3 is not string
|
||||
- test3 | length == 1
|
||||
- test4 is sequence and test4 is not string
|
||||
- test4 | length == 1
|
||||
# Without wantlist=True lookup should return mapping
|
||||
- test5 is mapping
|
||||
- test6 is mapping
|
||||
# errors='ignore'
|
||||
- test7 is string
|
||||
- test8 is not defined
|
||||
|
||||
- name: Create another namespace with label
|
||||
kubernetes.core.k8s:
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "app-development-two"
|
||||
labels:
|
||||
namespace_label: "app_development"
|
||||
|
||||
- set_fact:
|
||||
test1: "{{ lookup('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development', wantlist=True) }}"
|
||||
test2: "{{ query('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development') }}"
|
||||
test3: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one', wantlist=True) }}"
|
||||
test4: "{{ query('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one') }}"
|
||||
test5: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-two', wantlist=True) }}"
|
||||
test6: "{{ query('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-two') }}"
|
||||
test7: "{{ lookup('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development') }}"
|
||||
test8: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-one') }}"
|
||||
test9: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name='app-development-two') }}"
|
||||
|
||||
- name: Assert that every test is passed after creating second object
|
||||
assert:
|
||||
that:
|
||||
# After creating second object
|
||||
- test1 is sequence and test1 is not string
|
||||
- test1 | length == 2
|
||||
- test2 is sequence and test2 is not string
|
||||
- test2 | length == 2
|
||||
- test3 is sequence and test3 is not string
|
||||
- test3 | length == 1
|
||||
- test4 is sequence and test4 is not string
|
||||
- test4 | length == 1
|
||||
- test5 is sequence and test5 is not string
|
||||
- test5 | length == 1
|
||||
- test6 is sequence and test6 is not string
|
||||
- test6 | length == 1
|
||||
# When label_selector is used it returns list irrespective of wantlist=True
|
||||
- test7 is sequence and test7 is not string
|
||||
# Without wantlist=True lookup should return mapping
|
||||
- test8 is mapping
|
||||
- test9 is mapping
|
||||
|
||||
always:
|
||||
- name: Ensure that namespace is removed
|
||||
k8s:
|
||||
kind: Namespace
|
||||
name: "app-development-{{ item }}"
|
||||
state: absent
|
||||
with_items:
|
||||
- one
|
||||
- two
|
||||
@@ -3,23 +3,57 @@
|
||||
# Copyright (c) 2020, Ansible Project
|
||||
# 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)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import copy
|
||||
import traceback
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
import platform
|
||||
|
||||
from ansible.config.manager import ensure_type
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleAction, AnsibleActionFail
|
||||
from ansible.errors import (
|
||||
AnsibleError,
|
||||
AnsibleFileNotFound,
|
||||
AnsibleAction,
|
||||
AnsibleActionFail,
|
||||
)
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.module_utils.six import string_types, iteritems
|
||||
from ansible.module_utils._text import to_text, to_bytes, to_native
|
||||
from ansible.plugins.action import ActionBase
|
||||
|
||||
|
||||
class RemoveOmit(object):
|
||||
def __init__(self, buffer, omit_value):
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
raise AnsibleError("Failed to import the required Python library (PyYAML).")
|
||||
self.data = yaml.safe_load_all(buffer)
|
||||
self.omit = omit_value
|
||||
|
||||
def remove_omit(self, data):
|
||||
if isinstance(data, dict):
|
||||
result = dict()
|
||||
for key, value in iteritems(data):
|
||||
if value == self.omit:
|
||||
continue
|
||||
result[key] = self.remove_omit(value)
|
||||
return result
|
||||
if isinstance(data, list):
|
||||
return [self.remove_omit(v) for v in data if v != self.omit]
|
||||
return data
|
||||
|
||||
def output(self):
|
||||
return [self.remove_omit(d) for d in self.data]
|
||||
|
||||
|
||||
ENV_KUBECONFIG_PATH_SEPARATOR = ";" if platform.system() == "Windows" else ":"
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
TRANSFERS_FILES = True
|
||||
@@ -28,19 +62,19 @@ class ActionModule(ActionBase):
|
||||
def _ensure_invocation(self, result):
|
||||
# NOTE: adding invocation arguments here needs to be kept in sync with
|
||||
# any no_log specified in the argument_spec in the module.
|
||||
if 'invocation' not in result:
|
||||
if "invocation" not in result:
|
||||
if self._play_context.no_log:
|
||||
result['invocation'] = "CENSORED: no_log is set"
|
||||
result["invocation"] = "CENSORED: no_log is set"
|
||||
else:
|
||||
result['invocation'] = self._task.args.copy()
|
||||
result['invocation']['module_args'] = self._task.args.copy()
|
||||
result["invocation"] = self._task.args.copy()
|
||||
result["invocation"]["module_args"] = self._task.args.copy()
|
||||
|
||||
return result
|
||||
|
||||
@contextmanager
|
||||
def get_template_data(self, template_path):
|
||||
try:
|
||||
source = self._find_needle('templates', template_path)
|
||||
source = self._find_needle("templates", template_path)
|
||||
except AnsibleError as e:
|
||||
raise AnsibleActionFail(to_text(e))
|
||||
|
||||
@@ -48,15 +82,19 @@ class ActionModule(ActionBase):
|
||||
try:
|
||||
tmp_source = self._loader.get_real_file(source)
|
||||
except AnsibleFileNotFound as e:
|
||||
raise AnsibleActionFail("could not find template=%s, %s" % (source, to_text(e)))
|
||||
b_tmp_source = to_bytes(tmp_source, errors='surrogate_or_strict')
|
||||
raise AnsibleActionFail(
|
||||
"could not find template=%s, %s" % (source, to_text(e))
|
||||
)
|
||||
b_tmp_source = to_bytes(tmp_source, errors="surrogate_or_strict")
|
||||
|
||||
try:
|
||||
with open(b_tmp_source, 'rb') as f:
|
||||
with open(b_tmp_source, "rb") as f:
|
||||
try:
|
||||
template_data = to_text(f.read(), errors='surrogate_or_strict')
|
||||
template_data = to_text(f.read(), errors="surrogate_or_strict")
|
||||
except UnicodeError:
|
||||
raise AnsibleActionFail("Template source files must be utf-8 encoded")
|
||||
raise AnsibleActionFail(
|
||||
"Template source files must be utf-8 encoded"
|
||||
)
|
||||
yield template_data
|
||||
except AnsibleAction:
|
||||
raise
|
||||
@@ -73,63 +111,104 @@ class ActionModule(ActionBase):
|
||||
"block_start_string": None,
|
||||
"block_end_string": None,
|
||||
"trim_blocks": True,
|
||||
"lstrip_blocks": False
|
||||
"lstrip_blocks": False,
|
||||
}
|
||||
if isinstance(template, string_types):
|
||||
# treat this as raw_params
|
||||
template_param['path'] = template
|
||||
template_param["path"] = template
|
||||
elif isinstance(template, dict):
|
||||
template_args = template
|
||||
template_path = template_args.get('path', None)
|
||||
template_path = template_args.get("path", None)
|
||||
if not template_path:
|
||||
raise AnsibleActionFail("Please specify path for template.")
|
||||
template_param['path'] = template_path
|
||||
template_param["path"] = template_path
|
||||
|
||||
# Options type validation strings
|
||||
for s_type in ('newline_sequence', 'variable_start_string', 'variable_end_string', 'block_start_string',
|
||||
'block_end_string'):
|
||||
for s_type in (
|
||||
"newline_sequence",
|
||||
"variable_start_string",
|
||||
"variable_end_string",
|
||||
"block_start_string",
|
||||
"block_end_string",
|
||||
):
|
||||
if s_type in template_args:
|
||||
value = ensure_type(template_args[s_type], 'string')
|
||||
value = ensure_type(template_args[s_type], "string")
|
||||
if value is not None and not isinstance(value, string_types):
|
||||
raise AnsibleActionFail("%s is expected to be a string, but got %s instead" % (s_type, type(value)))
|
||||
raise AnsibleActionFail(
|
||||
"%s is expected to be a string, but got %s instead"
|
||||
% (s_type, type(value))
|
||||
)
|
||||
try:
|
||||
template_param.update({
|
||||
"trim_blocks": boolean(template_args.get('trim_blocks', True), strict=False),
|
||||
"lstrip_blocks": boolean(template_args.get('lstrip_blocks', False), strict=False)
|
||||
})
|
||||
template_param.update(
|
||||
{
|
||||
"trim_blocks": boolean(
|
||||
template_args.get("trim_blocks", True), strict=False
|
||||
),
|
||||
"lstrip_blocks": boolean(
|
||||
template_args.get("lstrip_blocks", False), strict=False
|
||||
),
|
||||
}
|
||||
)
|
||||
except TypeError as e:
|
||||
raise AnsibleActionFail(to_native(e))
|
||||
|
||||
template_param.update({
|
||||
"newline_sequence": template_args.get('newline_sequence', self.DEFAULT_NEWLINE_SEQUENCE),
|
||||
"variable_start_string": template_args.get('variable_start_string', None),
|
||||
"variable_end_string": template_args.get('variable_end_string', None),
|
||||
"block_start_string": template_args.get('block_start_string', None),
|
||||
"block_end_string": template_args.get('block_end_string', None)
|
||||
})
|
||||
template_param.update(
|
||||
{
|
||||
"newline_sequence": template_args.get(
|
||||
"newline_sequence", self.DEFAULT_NEWLINE_SEQUENCE
|
||||
),
|
||||
"variable_start_string": template_args.get(
|
||||
"variable_start_string", None
|
||||
),
|
||||
"variable_end_string": template_args.get(
|
||||
"variable_end_string", None
|
||||
),
|
||||
"block_start_string": template_args.get("block_start_string", None),
|
||||
"block_end_string": template_args.get("block_end_string", None),
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise AnsibleActionFail("Error while reading template file - "
|
||||
"a string or dict for template expected, but got %s instead" % type(template))
|
||||
raise AnsibleActionFail(
|
||||
"Error while reading template file - "
|
||||
"a string or dict for template expected, but got %s instead"
|
||||
% type(template)
|
||||
)
|
||||
return template_param
|
||||
|
||||
def import_jinja2_lstrip(self, templates):
|
||||
# Option `lstrip_blocks' was added in Jinja2 version 2.7.
|
||||
if any(tmp['lstrip_blocks'] for tmp in templates):
|
||||
if any(tmp["lstrip_blocks"] for tmp in templates):
|
||||
try:
|
||||
import jinja2.defaults
|
||||
except ImportError:
|
||||
raise AnsibleError('Unable to import Jinja2 defaults for determining Jinja2 features.')
|
||||
raise AnsibleError(
|
||||
"Unable to import Jinja2 defaults for determining Jinja2 features."
|
||||
)
|
||||
|
||||
try:
|
||||
jinja2.defaults.LSTRIP_BLOCKS
|
||||
except AttributeError:
|
||||
raise AnsibleError("Option `lstrip_blocks' is only available in Jinja2 versions >=2.7")
|
||||
raise AnsibleError(
|
||||
"Option `lstrip_blocks' is only available in Jinja2 versions >=2.7"
|
||||
)
|
||||
|
||||
def load_template(self, template, new_module_args, task_vars):
|
||||
# template is only supported by k8s module.
|
||||
if self._task.action not in ('k8s', 'kubernetes.core.k8s', 'community.okd.k8s', 'redhat.openshift.k8s', 'community.kubernetes.k8s'):
|
||||
raise AnsibleActionFail("'template' is only a supported parameter for the 'k8s' module.")
|
||||
if self._task.action not in (
|
||||
"k8s",
|
||||
"kubernetes.core.k8s",
|
||||
"community.okd.k8s",
|
||||
"redhat.openshift.k8s",
|
||||
"community.kubernetes.k8s",
|
||||
"openshift_adm_groups_sync",
|
||||
"community.okd.openshift_adm_groups_sync",
|
||||
"redhat.openshift.openshift_adm_groups_sync",
|
||||
):
|
||||
raise AnsibleActionFail(
|
||||
"'template' is only a supported parameter for the 'k8s' module."
|
||||
)
|
||||
|
||||
omit_value = task_vars.get("omit")
|
||||
template_params = []
|
||||
if isinstance(template, string_types) or isinstance(template, dict):
|
||||
template_params.append(self.get_template_args(template))
|
||||
@@ -137,8 +216,11 @@ class ActionModule(ActionBase):
|
||||
for element in template:
|
||||
template_params.append(self.get_template_args(element))
|
||||
else:
|
||||
raise AnsibleActionFail("Error while reading template file - "
|
||||
"a string or dict for template expected, but got %s instead" % type(template))
|
||||
raise AnsibleActionFail(
|
||||
"Error while reading template file - "
|
||||
"a string or dict for template expected, but got %s instead"
|
||||
% type(template)
|
||||
)
|
||||
|
||||
self.import_jinja2_lstrip(template_params)
|
||||
|
||||
@@ -149,20 +231,31 @@ class ActionModule(ActionBase):
|
||||
old_vars = self._templar.available_variables
|
||||
|
||||
default_environment = {}
|
||||
for key in ("newline_sequence", "variable_start_string", "variable_end_string",
|
||||
"block_start_string", "block_end_string", "trim_blocks", "lstrip_blocks"):
|
||||
for key in (
|
||||
"newline_sequence",
|
||||
"variable_start_string",
|
||||
"variable_end_string",
|
||||
"block_start_string",
|
||||
"block_end_string",
|
||||
"trim_blocks",
|
||||
"lstrip_blocks",
|
||||
):
|
||||
if hasattr(self._templar.environment, key):
|
||||
default_environment[key] = getattr(self._templar.environment, key)
|
||||
for template_item in template_params:
|
||||
# We need to convert unescaped sequences to proper escaped sequences for Jinja2
|
||||
newline_sequence = template_item['newline_sequence']
|
||||
newline_sequence = template_item["newline_sequence"]
|
||||
if newline_sequence in wrong_sequences:
|
||||
template_item['newline_sequence'] = allowed_sequences[wrong_sequences.index(newline_sequence)]
|
||||
template_item["newline_sequence"] = allowed_sequences[
|
||||
wrong_sequences.index(newline_sequence)
|
||||
]
|
||||
elif newline_sequence not in allowed_sequences:
|
||||
raise AnsibleActionFail("newline_sequence needs to be one of: \n, \r or \r\n")
|
||||
raise AnsibleActionFail(
|
||||
"newline_sequence needs to be one of: \n, \r or \r\n"
|
||||
)
|
||||
|
||||
# template the source data locally & get ready to transfer
|
||||
with self.get_template_data(template_item['path']) as template_data:
|
||||
with self.get_template_data(template_item["path"]) as template_data:
|
||||
# add ansible 'template' vars
|
||||
temp_vars = copy.deepcopy(task_vars)
|
||||
for key, value in iteritems(template_item):
|
||||
@@ -170,29 +263,48 @@ class ActionModule(ActionBase):
|
||||
if value is not None:
|
||||
setattr(self._templar.environment, key, value)
|
||||
else:
|
||||
setattr(self._templar.environment, key, default_environment.get(key))
|
||||
setattr(
|
||||
self._templar.environment,
|
||||
key,
|
||||
default_environment.get(key),
|
||||
)
|
||||
self._templar.available_variables = temp_vars
|
||||
result = self._templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False)
|
||||
result_template.append(result)
|
||||
result = self._templar.do_template(
|
||||
template_data,
|
||||
preserve_trailing_newlines=True,
|
||||
escape_backslashes=False,
|
||||
)
|
||||
if omit_value is not None:
|
||||
result_template.extend(RemoveOmit(result, omit_value).output())
|
||||
else:
|
||||
result_template.append(result)
|
||||
self._templar.available_variables = old_vars
|
||||
resource_definition = self._task.args.get('definition', None)
|
||||
resource_definition = self._task.args.get("definition", None)
|
||||
if not resource_definition:
|
||||
new_module_args.pop('template')
|
||||
new_module_args['definition'] = result_template
|
||||
new_module_args.pop("template")
|
||||
new_module_args["definition"] = result_template
|
||||
|
||||
def get_file_realpath(self, local_path):
|
||||
# local_path is only supported by k8s_cp module.
|
||||
if self._task.action not in ('k8s_cp', 'kubernetes.core.k8s_cp', 'community.kubernetes.k8s_cp'):
|
||||
raise AnsibleActionFail("'local_path' is only supported parameter for 'k8s_cp' module.")
|
||||
if self._task.action not in (
|
||||
"k8s_cp",
|
||||
"kubernetes.core.k8s_cp",
|
||||
"community.kubernetes.k8s_cp",
|
||||
):
|
||||
raise AnsibleActionFail(
|
||||
"'local_path' is only supported parameter for 'k8s_cp' module."
|
||||
)
|
||||
|
||||
if os.path.exists(local_path):
|
||||
return local_path
|
||||
|
||||
try:
|
||||
# find in expected paths
|
||||
return self._find_needle('files', local_path)
|
||||
return self._find_needle("files", local_path)
|
||||
except AnsibleError:
|
||||
raise AnsibleActionFail("%s does not exist in local filesystem" % local_path)
|
||||
raise AnsibleActionFail(
|
||||
"%s does not exist in local filesystem" % local_path
|
||||
)
|
||||
|
||||
def get_kubeconfig(self, kubeconfig, remote_transport, new_module_args):
|
||||
if isinstance(kubeconfig, string_types):
|
||||
@@ -200,20 +312,26 @@ class ActionModule(ActionBase):
|
||||
if not remote_transport:
|
||||
# kubeconfig is local
|
||||
# find in expected paths
|
||||
kubeconfig = self._find_needle('files', kubeconfig)
|
||||
configs = []
|
||||
for config in kubeconfig.split(ENV_KUBECONFIG_PATH_SEPARATOR):
|
||||
config = self._find_needle("files", config)
|
||||
|
||||
# decrypt kubeconfig found
|
||||
actual_file = self._loader.get_real_file(kubeconfig, decrypt=True)
|
||||
new_module_args['kubeconfig'] = actual_file
|
||||
# decrypt kubeconfig found
|
||||
configs.append(self._loader.get_real_file(config, decrypt=True))
|
||||
new_module_args["kubeconfig"] = ENV_KUBECONFIG_PATH_SEPARATOR.join(
|
||||
configs
|
||||
)
|
||||
|
||||
elif isinstance(kubeconfig, dict):
|
||||
new_module_args['kubeconfig'] = kubeconfig
|
||||
new_module_args["kubeconfig"] = kubeconfig
|
||||
else:
|
||||
raise AnsibleActionFail("Error while reading kubeconfig parameter - "
|
||||
"a string or dict expected, but got %s instead" % type(kubeconfig))
|
||||
raise AnsibleActionFail(
|
||||
"Error while reading kubeconfig parameter - "
|
||||
"a string or dict expected, but got %s instead" % type(kubeconfig)
|
||||
)
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
''' handler for k8s options '''
|
||||
"""handler for k8s options"""
|
||||
if task_vars is None:
|
||||
task_vars = dict()
|
||||
|
||||
@@ -224,53 +342,61 @@ class ActionModule(ActionBase):
|
||||
# look for kubeconfig and src
|
||||
# 'local' => look files on Ansible Controller
|
||||
# Transport other than 'local' => look files on remote node
|
||||
remote_transport = self._connection.transport != 'local'
|
||||
remote_transport = self._connection.transport != "local"
|
||||
|
||||
new_module_args = copy.deepcopy(self._task.args)
|
||||
|
||||
kubeconfig = self._task.args.get('kubeconfig', None)
|
||||
kubeconfig = self._task.args.get("kubeconfig", None)
|
||||
if kubeconfig:
|
||||
try:
|
||||
self.get_kubeconfig(kubeconfig, remote_transport, new_module_args)
|
||||
except AnsibleError as e:
|
||||
result['failed'] = True
|
||||
result['msg'] = to_text(e)
|
||||
result['exception'] = traceback.format_exc()
|
||||
result["failed"] = True
|
||||
result["msg"] = to_text(e)
|
||||
result["exception"] = traceback.format_exc()
|
||||
return result
|
||||
|
||||
# find the file in the expected search path
|
||||
src = self._task.args.get('src', None)
|
||||
src = self._task.args.get("src", None)
|
||||
|
||||
if src:
|
||||
if src and not src.startswith(("http://", "https://", "ftp://")):
|
||||
if remote_transport:
|
||||
# src is on remote node
|
||||
result.update(self._execute_module(module_name=self._task.action, task_vars=task_vars))
|
||||
result.update(
|
||||
self._execute_module(
|
||||
module_name=self._task.action, task_vars=task_vars
|
||||
)
|
||||
)
|
||||
return self._ensure_invocation(result)
|
||||
|
||||
# src is local
|
||||
try:
|
||||
# find in expected paths
|
||||
src = self._find_needle('files', src)
|
||||
src = self._find_needle("files", src)
|
||||
except AnsibleError as e:
|
||||
result['failed'] = True
|
||||
result['msg'] = to_text(e)
|
||||
result['exception'] = traceback.format_exc()
|
||||
result["failed"] = True
|
||||
result["msg"] = to_text(e)
|
||||
result["exception"] = traceback.format_exc()
|
||||
return result
|
||||
|
||||
if src:
|
||||
new_module_args['src'] = src
|
||||
new_module_args["src"] = src
|
||||
|
||||
template = self._task.args.get('template', None)
|
||||
template = self._task.args.get("template", None)
|
||||
if template:
|
||||
self.load_template(template, new_module_args, task_vars)
|
||||
|
||||
local_path = self._task.args.get('local_path')
|
||||
state = self._task.args.get('state', None)
|
||||
if local_path and state == 'to_pod':
|
||||
new_module_args['local_path'] = self.get_file_realpath(local_path)
|
||||
local_path = self._task.args.get("local_path")
|
||||
state = self._task.args.get("state", None)
|
||||
if local_path and state == "to_pod" and not remote_transport:
|
||||
new_module_args["local_path"] = self.get_file_realpath(local_path)
|
||||
|
||||
# Execute the k8s_* module.
|
||||
module_return = self._execute_module(module_name=self._task.action, module_args=new_module_args, task_vars=task_vars)
|
||||
module_return = self._execute_module(
|
||||
module_name=self._task.action,
|
||||
module_args=new_module_args,
|
||||
task_vars=task_vars,
|
||||
)
|
||||
|
||||
# Delete tmp path
|
||||
self._remove_tmp_path(self._connection._shell.tmpdir)
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
author:
|
||||
- xuxinkun
|
||||
- xuxinkun (@xuxinkun)
|
||||
|
||||
connection: kubectl
|
||||
name: kubectl
|
||||
|
||||
short_description: Execute tasks in pods running on Kubernetes.
|
||||
|
||||
@@ -74,6 +75,7 @@ DOCUMENTATION = r"""
|
||||
kubectl_kubeconfig:
|
||||
description:
|
||||
- Path to a kubectl config file. Defaults to I(~/.kube/config)
|
||||
- The configuration can be provided as dictionary. Added in version 2.4.0.
|
||||
default: ''
|
||||
vars:
|
||||
- name: ansible_kubectl_kubeconfig
|
||||
@@ -170,10 +172,12 @@ DOCUMENTATION = r"""
|
||||
aliases: [ kubectl_verify_ssl ]
|
||||
"""
|
||||
|
||||
import distutils.spawn
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import json
|
||||
|
||||
from ansible.parsing.yaml.loader import AnsibleLoader
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
||||
@@ -185,26 +189,26 @@ from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
|
||||
CONNECTION_TRANSPORT = 'kubectl'
|
||||
CONNECTION_TRANSPORT = "kubectl"
|
||||
|
||||
CONNECTION_OPTIONS = {
|
||||
'kubectl_container': '-c',
|
||||
'kubectl_namespace': '-n',
|
||||
'kubectl_kubeconfig': '--kubeconfig',
|
||||
'kubectl_context': '--context',
|
||||
'kubectl_host': '--server',
|
||||
'kubectl_username': '--username',
|
||||
'kubectl_password': '--password',
|
||||
'client_cert': '--client-certificate',
|
||||
'client_key': '--client-key',
|
||||
'ca_cert': '--certificate-authority',
|
||||
'validate_certs': '--insecure-skip-tls-verify',
|
||||
'kubectl_token': '--token'
|
||||
"kubectl_container": "-c",
|
||||
"kubectl_namespace": "-n",
|
||||
"kubectl_kubeconfig": "--kubeconfig",
|
||||
"kubectl_context": "--context",
|
||||
"kubectl_host": "--server",
|
||||
"kubectl_username": "--username",
|
||||
"kubectl_password": "--password",
|
||||
"client_cert": "--client-certificate",
|
||||
"client_key": "--client-key",
|
||||
"ca_cert": "--certificate-authority",
|
||||
"validate_certs": "--insecure-skip-tls-verify",
|
||||
"kubectl_token": "--token",
|
||||
}
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' Local kubectl based connections '''
|
||||
"""Local kubectl based connections"""
|
||||
|
||||
transport = CONNECTION_TRANSPORT
|
||||
connection_options = CONNECTION_OPTIONS
|
||||
@@ -217,155 +221,224 @@ class Connection(ConnectionBase):
|
||||
|
||||
# Note: kubectl runs commands as the user that started the container.
|
||||
# It is impossible to set the remote user for a kubectl connection.
|
||||
cmd_arg = '{0}_command'.format(self.transport)
|
||||
if cmd_arg in kwargs:
|
||||
self.transport_cmd = kwargs[cmd_arg]
|
||||
else:
|
||||
self.transport_cmd = distutils.spawn.find_executable(self.transport)
|
||||
if not self.transport_cmd:
|
||||
raise AnsibleError("{0} command not found in PATH".format(self.transport))
|
||||
cmd_arg = "{0}_command".format(self.transport)
|
||||
self.transport_cmd = kwargs.get(cmd_arg, shutil.which(self.transport))
|
||||
if not self.transport_cmd:
|
||||
raise AnsibleError("{0} command not found in PATH".format(self.transport))
|
||||
self._file_to_delete = None
|
||||
|
||||
def delete_temporary_file(self):
|
||||
if self._file_to_delete is not None:
|
||||
os.remove(self._file_to_delete)
|
||||
self._file_to_delete = None
|
||||
|
||||
def _build_exec_cmd(self, cmd):
|
||||
""" Build the local kubectl exec command to run cmd on remote_host
|
||||
"""
|
||||
"""Build the local kubectl exec command to run cmd on remote_host"""
|
||||
local_cmd = [self.transport_cmd]
|
||||
censored_local_cmd = [self.transport_cmd]
|
||||
|
||||
# Build command options based on doc string
|
||||
doc_yaml = AnsibleLoader(self.documentation).get_single_data()
|
||||
for key in doc_yaml.get('options'):
|
||||
if key.endswith('verify_ssl') and self.get_option(key) != '':
|
||||
for key in doc_yaml.get("options"):
|
||||
if key.endswith("verify_ssl") and self.get_option(key) != "":
|
||||
# Translate verify_ssl to skip_verify_ssl, and output as string
|
||||
skip_verify_ssl = not self.get_option(key)
|
||||
local_cmd.append(u'{0}={1}'.format(self.connection_options[key], str(skip_verify_ssl).lower()))
|
||||
censored_local_cmd.append(u'{0}={1}'.format(self.connection_options[key], str(skip_verify_ssl).lower()))
|
||||
elif not key.endswith('container') and self.get_option(key) and self.connection_options.get(key):
|
||||
local_cmd.append(
|
||||
"{0}={1}".format(
|
||||
self.connection_options[key], str(skip_verify_ssl).lower()
|
||||
)
|
||||
)
|
||||
censored_local_cmd.append(
|
||||
"{0}={1}".format(
|
||||
self.connection_options[key], str(skip_verify_ssl).lower()
|
||||
)
|
||||
)
|
||||
elif key.endswith("kubeconfig") and self.get_option(key) != "":
|
||||
kubeconfig_path = self.get_option(key)
|
||||
if isinstance(kubeconfig_path, dict):
|
||||
fd, tmpfile = tempfile.mkstemp()
|
||||
with os.fdopen(fd, "w") as fp:
|
||||
json.dump(kubeconfig_path, fp)
|
||||
kubeconfig_path = tmpfile
|
||||
self._file_to_delete = tmpfile
|
||||
|
||||
cmd_arg = self.connection_options[key]
|
||||
local_cmd += [cmd_arg, kubeconfig_path]
|
||||
censored_local_cmd += [cmd_arg, kubeconfig_path]
|
||||
elif (
|
||||
not key.endswith("container")
|
||||
and self.get_option(key)
|
||||
and self.connection_options.get(key)
|
||||
):
|
||||
cmd_arg = self.connection_options[key]
|
||||
local_cmd += [cmd_arg, self.get_option(key)]
|
||||
# Redact password and token from console log
|
||||
if key.endswith(('_token', '_password')):
|
||||
censored_local_cmd += [cmd_arg, '********']
|
||||
if key.endswith(("_token", "_password")):
|
||||
censored_local_cmd += [cmd_arg, "********"]
|
||||
else:
|
||||
censored_local_cmd += [cmd_arg, self.get_option(key)]
|
||||
|
||||
extra_args_name = u'{0}_extra_args'.format(self.transport)
|
||||
extra_args_name = "{0}_extra_args".format(self.transport)
|
||||
if self.get_option(extra_args_name):
|
||||
local_cmd += self.get_option(extra_args_name).split(' ')
|
||||
censored_local_cmd += self.get_option(extra_args_name).split(' ')
|
||||
local_cmd += self.get_option(extra_args_name).split(" ")
|
||||
censored_local_cmd += self.get_option(extra_args_name).split(" ")
|
||||
|
||||
pod = self.get_option(u'{0}_pod'.format(self.transport))
|
||||
pod = self.get_option("{0}_pod".format(self.transport))
|
||||
if not pod:
|
||||
pod = self._play_context.remote_addr
|
||||
# -i is needed to keep stdin open which allows pipelining to work
|
||||
local_cmd += ['exec', '-i', pod]
|
||||
censored_local_cmd += ['exec', '-i', pod]
|
||||
local_cmd += ["exec", "-i", pod]
|
||||
censored_local_cmd += ["exec", "-i", pod]
|
||||
|
||||
# if the pod has more than one container, then container is required
|
||||
container_arg_name = u'{0}_container'.format(self.transport)
|
||||
container_arg_name = "{0}_container".format(self.transport)
|
||||
if self.get_option(container_arg_name):
|
||||
local_cmd += ['-c', self.get_option(container_arg_name)]
|
||||
censored_local_cmd += ['-c', self.get_option(container_arg_name)]
|
||||
local_cmd += ["-c", self.get_option(container_arg_name)]
|
||||
censored_local_cmd += ["-c", self.get_option(container_arg_name)]
|
||||
|
||||
local_cmd += ['--'] + cmd
|
||||
censored_local_cmd += ['--'] + cmd
|
||||
local_cmd += ["--"] + cmd
|
||||
censored_local_cmd += ["--"] + cmd
|
||||
|
||||
return local_cmd, censored_local_cmd
|
||||
|
||||
def _connect(self, port=None):
|
||||
""" Connect to the container. Nothing to do """
|
||||
"""Connect to the container. Nothing to do"""
|
||||
super(Connection, self)._connect()
|
||||
if not self._connected:
|
||||
display.vvv(u"ESTABLISH {0} CONNECTION".format(self.transport), host=self._play_context.remote_addr)
|
||||
display.vvv(
|
||||
"ESTABLISH {0} CONNECTION".format(self.transport),
|
||||
host=self._play_context.remote_addr,
|
||||
)
|
||||
self._connected = True
|
||||
|
||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||
""" Run a command in the container """
|
||||
"""Run a command in the container"""
|
||||
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
local_cmd, censored_local_cmd = self._build_exec_cmd([self._play_context.executable, '-c', cmd])
|
||||
local_cmd, censored_local_cmd = self._build_exec_cmd(
|
||||
[self._play_context.executable, "-c", cmd]
|
||||
)
|
||||
|
||||
display.vvv("EXEC %s" % (censored_local_cmd,), host=self._play_context.remote_addr)
|
||||
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||
p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
display.vvv(
|
||||
"EXEC %s" % (censored_local_cmd,), host=self._play_context.remote_addr
|
||||
)
|
||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
||||
p = subprocess.Popen(
|
||||
local_cmd,
|
||||
shell=False,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
stdout, stderr = p.communicate(in_data)
|
||||
self.delete_temporary_file()
|
||||
return (p.returncode, stdout, stderr)
|
||||
|
||||
def _prefix_login_path(self, remote_path):
|
||||
''' Make sure that we put files into a standard path
|
||||
"""Make sure that we put files into a standard path
|
||||
|
||||
If a path is relative, then we need to choose where to put it.
|
||||
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
||||
exist in any given chroot. So for now we're choosing "/" instead.
|
||||
This also happens to be the former default.
|
||||
If a path is relative, then we need to choose where to put it.
|
||||
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
||||
exist in any given chroot. So for now we're choosing "/" instead.
|
||||
This also happens to be the former default.
|
||||
|
||||
Can revisit using $HOME instead if it's a problem
|
||||
'''
|
||||
Can revisit using $HOME instead if it's a problem
|
||||
"""
|
||||
if not remote_path.startswith(os.path.sep):
|
||||
remote_path = os.path.join(os.path.sep, remote_path)
|
||||
return os.path.normpath(remote_path)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
""" Transfer a file from local to the container """
|
||||
"""Transfer a file from local to the container"""
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
display.vvv(
|
||||
"PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr
|
||||
)
|
||||
|
||||
out_path = self._prefix_login_path(out_path)
|
||||
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
|
||||
raise AnsibleFileNotFound(
|
||||
"file or module does not exist: %s" % in_path)
|
||||
if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
|
||||
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
|
||||
out_path = shlex_quote(out_path)
|
||||
# kubectl doesn't have native support for copying files into
|
||||
# running containers, so we use kubectl exec to implement this
|
||||
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
|
||||
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
|
||||
if not os.fstat(in_file.fileno()).st_size:
|
||||
count = ' count=0'
|
||||
count = " count=0"
|
||||
else:
|
||||
count = ''
|
||||
args, dummy = self._build_exec_cmd([self._play_context.executable, "-c", "dd of=%s bs=%s%s" % (out_path, BUFSIZE, count)])
|
||||
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
|
||||
count = ""
|
||||
args, dummy = self._build_exec_cmd(
|
||||
[
|
||||
self._play_context.executable,
|
||||
"-c",
|
||||
"dd of=%s bs=%s%s && sleep 0" % (out_path, BUFSIZE, count),
|
||||
]
|
||||
)
|
||||
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
|
||||
try:
|
||||
p = subprocess.Popen(args, stdin=in_file,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
args, stdin=in_file, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
except OSError:
|
||||
raise AnsibleError("kubectl connection requires dd command in the container to put files")
|
||||
raise AnsibleError(
|
||||
"kubectl connection requires dd command in the container to put files"
|
||||
)
|
||||
stdout, stderr = p.communicate()
|
||||
self.delete_temporary_file()
|
||||
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
raise AnsibleError(
|
||||
"failed to transfer file %s to %s:\n%s\n%s"
|
||||
% (in_path, out_path, stdout, stderr)
|
||||
)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
""" Fetch a file from container to local. """
|
||||
"""Fetch a file from container to local."""
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
display.vvv(
|
||||
"FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr
|
||||
)
|
||||
|
||||
in_path = self._prefix_login_path(in_path)
|
||||
out_dir = os.path.dirname(out_path)
|
||||
|
||||
# kubectl doesn't have native support for fetching files from
|
||||
# running containers, so we use kubectl exec to implement this
|
||||
args, dummy = self._build_exec_cmd([self._play_context.executable, "-c", "dd if=%s bs=%s" % (in_path, BUFSIZE)])
|
||||
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
|
||||
args, dummy = self._build_exec_cmd(
|
||||
[self._play_context.executable, "-c", "dd if=%s bs=%s" % (in_path, BUFSIZE)]
|
||||
)
|
||||
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
|
||||
actual_out_path = os.path.join(out_dir, os.path.basename(in_path))
|
||||
with open(to_bytes(actual_out_path, errors='surrogate_or_strict'), 'wb') as out_file:
|
||||
with open(
|
||||
to_bytes(actual_out_path, errors="surrogate_or_strict"), "wb"
|
||||
) as out_file:
|
||||
try:
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE,
|
||||
stdout=out_file, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
args, stdin=subprocess.PIPE, stdout=out_file, stderr=subprocess.PIPE
|
||||
)
|
||||
except OSError:
|
||||
raise AnsibleError(
|
||||
"{0} connection requires dd command in the container to fetch files".format(self.transport)
|
||||
"{0} connection requires dd command in the container to fetch files".format(
|
||||
self.transport
|
||||
)
|
||||
)
|
||||
stdout, stderr = p.communicate()
|
||||
self.delete_temporary_file()
|
||||
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError("failed to fetch file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
raise AnsibleError(
|
||||
"failed to fetch file %s to %s:\n%s\n%s"
|
||||
% (in_path, out_path, stdout, stderr)
|
||||
)
|
||||
|
||||
if actual_out_path != out_path:
|
||||
os.rename(to_bytes(actual_out_path, errors='strict'), to_bytes(out_path, errors='strict'))
|
||||
os.rename(
|
||||
to_bytes(actual_out_path, errors="strict"),
|
||||
to_bytes(out_path, errors="strict"),
|
||||
)
|
||||
|
||||
def close(self):
|
||||
""" Terminate the connection. Nothing to do for kubectl"""
|
||||
"""Terminate the connection. Nothing to do for kubectl"""
|
||||
super(Connection, self).close()
|
||||
self._connected = False
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
|
||||
# Options for common Helm modules
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
binary_path:
|
||||
description:
|
||||
@@ -29,7 +30,8 @@ options:
|
||||
description:
|
||||
- Helm option to specify kubeconfig path to use.
|
||||
- If the value is not specified in the task, the value of environment variable C(K8S_AUTH_KUBECONFIG) will be used instead.
|
||||
type: path
|
||||
- The configuration can be provided as dictionary. Added in version 2.4.0.
|
||||
type: raw
|
||||
aliases: [ kubeconfig_path ]
|
||||
host:
|
||||
description:
|
||||
@@ -56,4 +58,4 @@ options:
|
||||
type: path
|
||||
aliases: [ ssl_ca_cert ]
|
||||
version_added: "1.2.0"
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
# Options for authenticating with the API.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
@@ -27,6 +28,7 @@ options:
|
||||
options are provided, the Kubernetes client will attempt to load the default
|
||||
configuration file from I(~/.kube/config). Can also be specified via K8S_AUTH_KUBECONFIG environment
|
||||
variable.
|
||||
- Multiple Kubernetes config file can be provided using separator ';' for Windows platform or ':' for others platforms.
|
||||
- The kubernetes configuration can be provided as dictionary. This feature requires a python kubernetes client version >= 17.17.0. Added in version 2.2.0.
|
||||
type: raw
|
||||
context:
|
||||
@@ -76,6 +78,14 @@ options:
|
||||
- The URL of an HTTP proxy to use for the connection. Can also be specified via K8S_AUTH_PROXY environment variable.
|
||||
- Please note that this module does not pick up typical proxy settings from the environment (e.g. HTTP_PROXY).
|
||||
type: str
|
||||
no_proxy:
|
||||
description:
|
||||
- The comma separated list of hosts/domains/IP/CIDR that shouldn't go through proxy. Can also be specified via K8S_AUTH_NO_PROXY environment variable.
|
||||
- Please note that this module does not pick up typical proxy settings from the environment (e.g. NO_PROXY).
|
||||
- This feature requires kubernetes>=19.15.0. When kubernetes library is less than 19.15.0, it fails even no_proxy set in correct.
|
||||
- example value is "localhost,.local,.example.com,127.0.0.1,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
||||
type: str
|
||||
version_added: 2.3.0
|
||||
proxy_headers:
|
||||
description:
|
||||
- The Header used for the HTTP proxy.
|
||||
@@ -110,8 +120,21 @@ options:
|
||||
- Please note that the current version of the k8s python client library does not support setting this flag to True yet.
|
||||
- "The fix for this k8s python library is here: https://github.com/kubernetes-client/python-base/pull/169"
|
||||
type: bool
|
||||
impersonate_user:
|
||||
description:
|
||||
- Username to impersonate for the operation.
|
||||
- Can also be specified via K8S_AUTH_IMPERSONATE_USER environment.
|
||||
type: str
|
||||
version_added: 2.3.0
|
||||
impersonate_groups:
|
||||
description:
|
||||
- Group(s) to impersonate for the operation.
|
||||
- "Can also be specified via K8S_AUTH_IMPERSONATE_GROUPS environment. Example: Group1,Group2"
|
||||
type: list
|
||||
elements: str
|
||||
version_added: 2.3.0
|
||||
notes:
|
||||
- "To avoid SSL certificate validation errors when C(validate_certs) is I(True), the full
|
||||
certificate chain for the API server must be provided via C(ca_cert) or in the
|
||||
kubeconfig file."
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
# Options for specifying object wait
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
delete_options:
|
||||
type: dict
|
||||
@@ -48,4 +49,4 @@ options:
|
||||
type: str
|
||||
description:
|
||||
- Specify the UID of the target object.
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
# Options for selecting or identifying a specific K8s object
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
api_version:
|
||||
description:
|
||||
@@ -49,4 +50,4 @@ options:
|
||||
- If I(resource definition) is provided, the I(metadata.namespace) value from the I(resource_definition)
|
||||
will override this option.
|
||||
type: str
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
# Options for providing an object configuration
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
resource_definition:
|
||||
description:
|
||||
@@ -28,6 +29,7 @@ options:
|
||||
- Reads from the local file system. To read from the Ansible controller's file system, including vaulted files, use the file lookup
|
||||
plugin or template lookup plugin, combined with the from_yaml filter, and pass the result to
|
||||
I(resource_definition). See Examples below.
|
||||
- Mutually exclusive with I(template) in case of M(k8s) module.
|
||||
- The URL to manifest files that can be used to create the resource. Added in version 2.4.0.
|
||||
- Mutually exclusive with I(template) in case of M(kubernetes.core.k8s) module.
|
||||
type: path
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
# Options used by scale modules.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
replicas:
|
||||
description:
|
||||
@@ -46,4 +47,4 @@ options:
|
||||
default: 5
|
||||
type: int
|
||||
version_added: 2.0.0
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
# Options for specifying object state
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
@@ -27,4 +28,4 @@ options:
|
||||
- If set to C(yes), and I(state) is C(present), an existing object will be replaced.
|
||||
type: bool
|
||||
default: no
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
# Options for specifying object wait
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
wait:
|
||||
description:
|
||||
@@ -64,4 +65,4 @@ options:
|
||||
- The possible reasons in a condition are specific to each resource type in Kubernetes.
|
||||
- See the API documentation of the status field for a given resource to see possible choices.
|
||||
type: dict
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -1,13 +1,54 @@
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING 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
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
|
||||
name: k8s_config_resource_name
|
||||
short_description: Generate resource name for the given resource of type ConfigMap, Secret
|
||||
description:
|
||||
- Generate resource name for the given resource of type ConfigMap, Secret.
|
||||
- Resource must have a C(metadata.name) key to generate a resource name
|
||||
options:
|
||||
_input:
|
||||
description:
|
||||
- A valid YAML definition for a ConfigMap or a Secret.
|
||||
type: dict
|
||||
required: true
|
||||
author:
|
||||
- ansible cloud team
|
||||
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
# Dump generated name for a configmap into a variable
|
||||
- set_fact:
|
||||
generated_name: '{{ definition | kubernetes.core.k8s_config_resource_name }}'
|
||||
vars:
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: myconfigmap
|
||||
namespace: mynamespace
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_value:
|
||||
description: Generated resource name.
|
||||
type: str
|
||||
"""
|
||||
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import generate_hash
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import (
|
||||
generate_hash,
|
||||
)
|
||||
|
||||
|
||||
def k8s_config_resource_name(resource):
|
||||
@@ -15,15 +56,14 @@ def k8s_config_resource_name(resource):
|
||||
Generate resource name for the given resource of type ConfigMap, Secret
|
||||
"""
|
||||
try:
|
||||
return resource['metadata']['name'] + '-' + generate_hash(resource)
|
||||
return resource["metadata"]["name"] + "-" + generate_hash(resource)
|
||||
except KeyError:
|
||||
raise AnsibleFilterError("resource must have a metadata.name key to generate a resource name")
|
||||
raise AnsibleFilterError(
|
||||
"resource must have a metadata.name key to generate a resource name"
|
||||
)
|
||||
|
||||
|
||||
# ---- Ansible filters ----
|
||||
class FilterModule(object):
|
||||
|
||||
def filters(self):
|
||||
return {
|
||||
'k8s_config_resource_name': k8s_config_resource_name
|
||||
}
|
||||
return {"k8s_config_resource_name": k8s_config_resource_name}
|
||||
|
||||
36
plugins/filter/k8s_config_resource_name.yml
Normal file
36
plugins/filter/k8s_config_resource_name.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
DOCUMENTATION:
|
||||
name: k8s_config_resource_name
|
||||
short_description: Generate resource name for the given resource of type ConfigMap, Secret
|
||||
description:
|
||||
- Generate resource name for the given resource of type ConfigMap, Secret.
|
||||
- Resource must have a C(metadata.name) key to generate a resource name
|
||||
options:
|
||||
_input:
|
||||
description:
|
||||
- A valid YAML definition for a ConfigMap or a Secret.
|
||||
type: dict
|
||||
required: true
|
||||
author:
|
||||
- ansible cloud team
|
||||
|
||||
EXAMPLES: |
|
||||
# Dump generated name for a configmap into a variable
|
||||
- set_fact:
|
||||
generated_name: '{{ definition | kubernetes.core.k8s_config_resource_name }}'
|
||||
vars:
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: myconfigmap
|
||||
namespace: mynamespace
|
||||
|
||||
RETURN:
|
||||
_value:
|
||||
description: Generated resource name.
|
||||
type: str
|
||||
@@ -1,15 +1,15 @@
|
||||
# Copyright (c) 2018 Ansible Project
|
||||
# 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)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
DOCUMENTATION = """
|
||||
name: k8s
|
||||
plugin_type: inventory
|
||||
author:
|
||||
- Chris Houseknecht <@chouseknecht>
|
||||
- Fabian von Feilitzsch <@fabianvf>
|
||||
- Chris Houseknecht (@chouseknecht)
|
||||
- Fabian von Feilitzsch (@fabianvf)
|
||||
|
||||
short_description: Kubernetes (K8s) inventory source
|
||||
|
||||
@@ -27,7 +27,7 @@ DOCUMENTATION = '''
|
||||
connections:
|
||||
description:
|
||||
- Optional list of cluster connection settings. If no connections are provided, the default
|
||||
I(~/.kube/config) and active context will be used, and objects will be returned for all namespaces
|
||||
'~/.kube/config' and active context will be used, and objects will be returned for all namespaces
|
||||
the active user is authorized to access.
|
||||
suboptions:
|
||||
name:
|
||||
@@ -38,7 +38,7 @@ DOCUMENTATION = '''
|
||||
description:
|
||||
- Path to an existing Kubernetes config file. If not provided, and no other connection
|
||||
options are provided, the Kubernetes client will attempt to load the default
|
||||
configuration file from I(~/.kube/config). Can also be specified via K8S_AUTH_KUBECONFIG
|
||||
configuration file from '~/.kube/config'. Can also be specified via K8S_AUTH_KUBECONFIG
|
||||
environment variable.
|
||||
context:
|
||||
description:
|
||||
@@ -89,35 +89,41 @@ DOCUMENTATION = '''
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
EXAMPLES = """
|
||||
# File must be named k8s.yaml or k8s.yml
|
||||
|
||||
# Authenticate with token, and return all pods and services for all namespaces
|
||||
plugin: kubernetes.core.k8s
|
||||
connections:
|
||||
- host: https://192.168.64.4:8443
|
||||
api_key: xxxxxxxxxxxxxxxx
|
||||
validate_certs: false
|
||||
- name: Authenticate with token, and return all pods and services for all namespaces
|
||||
plugin: kubernetes.core.k8s
|
||||
connections:
|
||||
- host: https://192.168.64.4:8443
|
||||
api_key: xxxxxxxxxxxxxxxx
|
||||
validate_certs: false
|
||||
|
||||
# Use default config (~/.kube/config) file and active context, and return objects for a specific namespace
|
||||
plugin: kubernetes.core.k8s
|
||||
connections:
|
||||
- namespaces:
|
||||
- testing
|
||||
- name: Use default config (~/.kube/config) file and active context, and return objects for a specific namespace
|
||||
plugin: kubernetes.core.k8s
|
||||
connections:
|
||||
- namespaces:
|
||||
- testing
|
||||
|
||||
# Use a custom config file, and a specific context.
|
||||
plugin: kubernetes.core.k8s
|
||||
connections:
|
||||
- kubeconfig: /path/to/config
|
||||
context: 'awx/192-168-64-4:8443/developer'
|
||||
'''
|
||||
- name: Use a custom config file, and a specific context.
|
||||
plugin: kubernetes.core.k8s
|
||||
connections:
|
||||
- kubeconfig: /path/to/config
|
||||
context: 'awx/192-168-64-4:8443/developer'
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER, k8s_import_exception, get_api_client
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
HAS_K8S_MODULE_HELPER,
|
||||
k8s_import_exception,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
|
||||
try:
|
||||
@@ -128,24 +134,24 @@ except ImportError:
|
||||
|
||||
def format_dynamic_api_exc(exc):
|
||||
if exc.body:
|
||||
if exc.headers and exc.headers.get('Content-Type') == 'application/json':
|
||||
message = json.loads(exc.body).get('message')
|
||||
if exc.headers and exc.headers.get("Content-Type") == "application/json":
|
||||
message = json.loads(exc.body).get("message")
|
||||
if message:
|
||||
return message
|
||||
return exc.body
|
||||
else:
|
||||
return '%s Reason: %s' % (exc.status, exc.reason)
|
||||
return "%s Reason: %s" % (exc.status, exc.reason)
|
||||
|
||||
|
||||
class K8sInventoryException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleMixin):
|
||||
NAME = 'kubernetes.core.k8s'
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
NAME = "kubernetes.core.k8s"
|
||||
|
||||
connection_plugin = 'kubernetes.core.kubectl'
|
||||
transport = 'kubectl'
|
||||
connection_plugin = "kubernetes.core.kubectl"
|
||||
transport = "kubectl"
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
@@ -154,11 +160,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
self.setup(config_data, cache, cache_key)
|
||||
|
||||
def setup(self, config_data, cache, cache_key):
|
||||
connections = config_data.get('connections')
|
||||
connections = config_data.get("connections")
|
||||
|
||||
if not HAS_K8S_MODULE_HELPER:
|
||||
raise K8sInventoryException(
|
||||
"This module requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(k8s_import_exception)
|
||||
"This module requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(
|
||||
k8s_import_exception
|
||||
)
|
||||
)
|
||||
|
||||
source_data = None
|
||||
@@ -179,11 +187,15 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
|
||||
for connection in connections:
|
||||
if not isinstance(connection, dict):
|
||||
raise K8sInventoryException("Expecting connection to be a dictionary.")
|
||||
raise K8sInventoryException(
|
||||
"Expecting connection to be a dictionary."
|
||||
)
|
||||
client = get_api_client(**connection)
|
||||
name = connection.get('name', self.get_default_host_name(client.configuration.host))
|
||||
if connection.get('namespaces'):
|
||||
namespaces = connection['namespaces']
|
||||
name = connection.get(
|
||||
"name", self.get_default_host_name(client.configuration.host)
|
||||
)
|
||||
if connection.get("namespaces"):
|
||||
namespaces = connection["namespaces"]
|
||||
else:
|
||||
namespaces = self.get_available_namespaces(client)
|
||||
for namespace in namespaces:
|
||||
@@ -199,27 +211,36 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
|
||||
@staticmethod
|
||||
def get_default_host_name(host):
|
||||
return host.replace('https://', '').replace('http://', '').replace('.', '-').replace(':', '_')
|
||||
return (
|
||||
host.replace("https://", "")
|
||||
.replace("http://", "")
|
||||
.replace(".", "-")
|
||||
.replace(":", "_")
|
||||
)
|
||||
|
||||
def get_available_namespaces(self, client):
|
||||
v1_namespace = client.resources.get(api_version='v1', kind='Namespace')
|
||||
v1_namespace = client.resources.get(api_version="v1", kind="Namespace")
|
||||
try:
|
||||
obj = v1_namespace.get()
|
||||
except DynamicApiError as exc:
|
||||
self.display.debug(exc)
|
||||
raise K8sInventoryException('Error fetching Namespace list: %s' % format_dynamic_api_exc(exc))
|
||||
raise K8sInventoryException(
|
||||
"Error fetching Namespace list: %s" % format_dynamic_api_exc(exc)
|
||||
)
|
||||
return [namespace.metadata.name for namespace in obj.items]
|
||||
|
||||
def get_pods_for_namespace(self, client, name, namespace):
|
||||
v1_pod = client.resources.get(api_version='v1', kind='Pod')
|
||||
v1_pod = client.resources.get(api_version="v1", kind="Pod")
|
||||
try:
|
||||
obj = v1_pod.get(namespace=namespace)
|
||||
except DynamicApiError as exc:
|
||||
self.display.debug(exc)
|
||||
raise K8sInventoryException('Error fetching Pod list: %s' % format_dynamic_api_exc(exc))
|
||||
raise K8sInventoryException(
|
||||
"Error fetching Pod list: %s" % format_dynamic_api_exc(exc)
|
||||
)
|
||||
|
||||
namespace_group = 'namespace_{0}'.format(namespace)
|
||||
namespace_pods_group = '{0}_pods'.format(namespace_group)
|
||||
namespace_group = "namespace_{0}".format(namespace)
|
||||
namespace_pods_group = "{0}_pods".format(namespace_group)
|
||||
|
||||
self.inventory.add_group(name)
|
||||
self.inventory.add_group(namespace_group)
|
||||
@@ -230,12 +251,14 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
for pod in obj.items:
|
||||
pod_name = pod.metadata.name
|
||||
pod_groups = []
|
||||
pod_annotations = {} if not pod.metadata.annotations else dict(pod.metadata.annotations)
|
||||
pod_annotations = (
|
||||
{} if not pod.metadata.annotations else dict(pod.metadata.annotations)
|
||||
)
|
||||
|
||||
if pod.metadata.labels:
|
||||
# create a group for each label_value
|
||||
for key, value in pod.metadata.labels:
|
||||
group_name = 'label_{0}_{1}'.format(key, value)
|
||||
group_name = "label_{0}_{1}".format(key, value)
|
||||
if group_name not in pod_groups:
|
||||
pod_groups.append(group_name)
|
||||
self.inventory.add_group(group_name)
|
||||
@@ -248,7 +271,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
|
||||
for container in pod.status.containerStatuses:
|
||||
# add each pod_container to the namespace group, and to each label_value group
|
||||
container_name = '{0}_{1}'.format(pod.metadata.name, container.name)
|
||||
container_name = "{0}_{1}".format(pod.metadata.name, container.name)
|
||||
self.inventory.add_host(container_name)
|
||||
self.inventory.add_child(namespace_pods_group, container_name)
|
||||
if pod_groups:
|
||||
@@ -256,46 +279,85 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
self.inventory.add_child(group, container_name)
|
||||
|
||||
# Add hostvars
|
||||
self.inventory.set_variable(container_name, 'object_type', 'pod')
|
||||
self.inventory.set_variable(container_name, 'labels', pod_labels)
|
||||
self.inventory.set_variable(container_name, 'annotations', pod_annotations)
|
||||
self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.clusterName)
|
||||
self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.nodeName)
|
||||
self.inventory.set_variable(container_name, 'pod_name', pod.spec.name)
|
||||
self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.hostIP)
|
||||
self.inventory.set_variable(container_name, 'pod_phase', pod.status.phase)
|
||||
self.inventory.set_variable(container_name, 'pod_ip', pod.status.podIP)
|
||||
self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.selfLink)
|
||||
self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resourceVersion)
|
||||
self.inventory.set_variable(container_name, 'pod_uid', pod.metadata.uid)
|
||||
self.inventory.set_variable(container_name, 'container_name', container.image)
|
||||
self.inventory.set_variable(container_name, 'container_image', container.image)
|
||||
self.inventory.set_variable(container_name, "object_type", "pod")
|
||||
self.inventory.set_variable(container_name, "labels", pod_labels)
|
||||
self.inventory.set_variable(
|
||||
container_name, "annotations", pod_annotations
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "cluster_name", pod.metadata.clusterName
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "pod_node_name", pod.spec.nodeName
|
||||
)
|
||||
self.inventory.set_variable(container_name, "pod_name", pod.spec.name)
|
||||
self.inventory.set_variable(
|
||||
container_name, "pod_host_ip", pod.status.hostIP
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "pod_phase", pod.status.phase
|
||||
)
|
||||
self.inventory.set_variable(container_name, "pod_ip", pod.status.podIP)
|
||||
self.inventory.set_variable(
|
||||
container_name, "pod_self_link", pod.metadata.selfLink
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "pod_resource_version", pod.metadata.resourceVersion
|
||||
)
|
||||
self.inventory.set_variable(container_name, "pod_uid", pod.metadata.uid)
|
||||
self.inventory.set_variable(
|
||||
container_name, "container_name", container.image
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "container_image", container.image
|
||||
)
|
||||
if container.state.running:
|
||||
self.inventory.set_variable(container_name, 'container_state', 'Running')
|
||||
self.inventory.set_variable(
|
||||
container_name, "container_state", "Running"
|
||||
)
|
||||
if container.state.terminated:
|
||||
self.inventory.set_variable(container_name, 'container_state', 'Terminated')
|
||||
self.inventory.set_variable(
|
||||
container_name, "container_state", "Terminated"
|
||||
)
|
||||
if container.state.waiting:
|
||||
self.inventory.set_variable(container_name, 'container_state', 'Waiting')
|
||||
self.inventory.set_variable(container_name, 'container_ready', container.ready)
|
||||
self.inventory.set_variable(container_name, 'ansible_remote_tmp', '/tmp/')
|
||||
self.inventory.set_variable(container_name, 'ansible_connection', self.connection_plugin)
|
||||
self.inventory.set_variable(container_name, 'ansible_{0}_pod'.format(self.transport),
|
||||
pod_name)
|
||||
self.inventory.set_variable(container_name, 'ansible_{0}_container'.format(self.transport),
|
||||
container.name)
|
||||
self.inventory.set_variable(container_name, 'ansible_{0}_namespace'.format(self.transport),
|
||||
namespace)
|
||||
self.inventory.set_variable(
|
||||
container_name, "container_state", "Waiting"
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "container_ready", container.ready
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "ansible_remote_tmp", "/tmp/"
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "ansible_connection", self.connection_plugin
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name, "ansible_{0}_pod".format(self.transport), pod_name
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name,
|
||||
"ansible_{0}_container".format(self.transport),
|
||||
container.name,
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
container_name,
|
||||
"ansible_{0}_namespace".format(self.transport),
|
||||
namespace,
|
||||
)
|
||||
|
||||
def get_services_for_namespace(self, client, name, namespace):
|
||||
v1_service = client.resources.get(api_version='v1', kind='Service')
|
||||
v1_service = client.resources.get(api_version="v1", kind="Service")
|
||||
try:
|
||||
obj = v1_service.get(namespace=namespace)
|
||||
except DynamicApiError as exc:
|
||||
self.display.debug(exc)
|
||||
raise K8sInventoryException('Error fetching Service list: %s' % format_dynamic_api_exc(exc))
|
||||
raise K8sInventoryException(
|
||||
"Error fetching Service list: %s" % format_dynamic_api_exc(exc)
|
||||
)
|
||||
|
||||
namespace_group = 'namespace_{0}'.format(namespace)
|
||||
namespace_services_group = '{0}_services'.format(namespace_group)
|
||||
namespace_group = "namespace_{0}".format(namespace)
|
||||
namespace_services_group = "{0}_services".format(namespace_group)
|
||||
|
||||
self.inventory.add_group(name)
|
||||
self.inventory.add_group(namespace_group)
|
||||
@@ -305,15 +367,21 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
|
||||
for service in obj.items:
|
||||
service_name = service.metadata.name
|
||||
service_labels = {} if not service.metadata.labels else dict(service.metadata.labels)
|
||||
service_annotations = {} if not service.metadata.annotations else dict(service.metadata.annotations)
|
||||
service_labels = (
|
||||
{} if not service.metadata.labels else dict(service.metadata.labels)
|
||||
)
|
||||
service_annotations = (
|
||||
{}
|
||||
if not service.metadata.annotations
|
||||
else dict(service.metadata.annotations)
|
||||
)
|
||||
|
||||
self.inventory.add_host(service_name)
|
||||
|
||||
if service.metadata.labels:
|
||||
# create a group for each label_value
|
||||
for key, value in service.metadata.labels:
|
||||
group_name = 'label_{0}_{1}'.format(key, value)
|
||||
group_name = "label_{0}_{1}".format(key, value)
|
||||
self.inventory.add_group(group_name)
|
||||
self.inventory.add_child(group_name, service_name)
|
||||
|
||||
@@ -322,42 +390,75 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM
|
||||
except AnsibleError:
|
||||
raise
|
||||
|
||||
ports = [{'name': port.name,
|
||||
'port': port.port,
|
||||
'protocol': port.protocol,
|
||||
'targetPort': port.targetPort,
|
||||
'nodePort': port.nodePort} for port in service.spec.ports or []]
|
||||
ports = [
|
||||
{
|
||||
"name": port.name,
|
||||
"port": port.port,
|
||||
"protocol": port.protocol,
|
||||
"targetPort": port.targetPort,
|
||||
"nodePort": port.nodePort,
|
||||
}
|
||||
for port in service.spec.ports or []
|
||||
]
|
||||
|
||||
# add hostvars
|
||||
self.inventory.set_variable(service_name, 'object_type', 'service')
|
||||
self.inventory.set_variable(service_name, 'labels', service_labels)
|
||||
self.inventory.set_variable(service_name, 'annotations', service_annotations)
|
||||
self.inventory.set_variable(service_name, 'cluster_name', service.metadata.clusterName)
|
||||
self.inventory.set_variable(service_name, 'ports', ports)
|
||||
self.inventory.set_variable(service_name, 'type', service.spec.type)
|
||||
self.inventory.set_variable(service_name, 'self_link', service.metadata.selfLink)
|
||||
self.inventory.set_variable(service_name, 'resource_version', service.metadata.resourceVersion)
|
||||
self.inventory.set_variable(service_name, 'uid', service.metadata.uid)
|
||||
self.inventory.set_variable(service_name, "object_type", "service")
|
||||
self.inventory.set_variable(service_name, "labels", service_labels)
|
||||
self.inventory.set_variable(
|
||||
service_name, "annotations", service_annotations
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
service_name, "cluster_name", service.metadata.clusterName
|
||||
)
|
||||
self.inventory.set_variable(service_name, "ports", ports)
|
||||
self.inventory.set_variable(service_name, "type", service.spec.type)
|
||||
self.inventory.set_variable(
|
||||
service_name, "self_link", service.metadata.selfLink
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
service_name, "resource_version", service.metadata.resourceVersion
|
||||
)
|
||||
self.inventory.set_variable(service_name, "uid", service.metadata.uid)
|
||||
|
||||
if service.spec.externalTrafficPolicy:
|
||||
self.inventory.set_variable(service_name, 'external_traffic_policy',
|
||||
service.spec.externalTrafficPolicy)
|
||||
self.inventory.set_variable(
|
||||
service_name,
|
||||
"external_traffic_policy",
|
||||
service.spec.externalTrafficPolicy,
|
||||
)
|
||||
if service.spec.externalIPs:
|
||||
self.inventory.set_variable(service_name, 'external_ips', service.spec.externalIPs)
|
||||
self.inventory.set_variable(
|
||||
service_name, "external_ips", service.spec.externalIPs
|
||||
)
|
||||
|
||||
if service.spec.externalName:
|
||||
self.inventory.set_variable(service_name, 'external_name', service.spec.externalName)
|
||||
self.inventory.set_variable(
|
||||
service_name, "external_name", service.spec.externalName
|
||||
)
|
||||
|
||||
if service.spec.healthCheckNodePort:
|
||||
self.inventory.set_variable(service_name, 'health_check_node_port',
|
||||
service.spec.healthCheckNodePort)
|
||||
self.inventory.set_variable(
|
||||
service_name,
|
||||
"health_check_node_port",
|
||||
service.spec.healthCheckNodePort,
|
||||
)
|
||||
if service.spec.loadBalancerIP:
|
||||
self.inventory.set_variable(service_name, 'load_balancer_ip',
|
||||
service.spec.loadBalancerIP)
|
||||
self.inventory.set_variable(
|
||||
service_name, "load_balancer_ip", service.spec.loadBalancerIP
|
||||
)
|
||||
if service.spec.selector:
|
||||
self.inventory.set_variable(service_name, 'selector', dict(service.spec.selector))
|
||||
self.inventory.set_variable(
|
||||
service_name, "selector", dict(service.spec.selector)
|
||||
)
|
||||
|
||||
if hasattr(service.status.loadBalancer, 'ingress') and service.status.loadBalancer.ingress:
|
||||
load_balancer = [{'hostname': ingress.hostname,
|
||||
'ip': ingress.ip} for ingress in service.status.loadBalancer.ingress]
|
||||
self.inventory.set_variable(service_name, 'load_balancer', load_balancer)
|
||||
if (
|
||||
hasattr(service.status.loadBalancer, "ingress")
|
||||
and service.status.loadBalancer.ingress
|
||||
):
|
||||
load_balancer = [
|
||||
{"hostname": ingress.hostname, "ip": ingress.ip}
|
||||
for ingress in service.status.loadBalancer.ingress
|
||||
]
|
||||
self.inventory.set_variable(
|
||||
service_name, "load_balancer", load_balancer
|
||||
)
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
#
|
||||
# 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)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: k8s
|
||||
DOCUMENTATION = """
|
||||
name: k8s
|
||||
|
||||
short_description: Query the K8s API
|
||||
|
||||
author:
|
||||
- Chris Houseknecht <@chouseknecht>
|
||||
- Fabian von Feilitzsch <@fabianvf>
|
||||
- Chris Houseknecht (@chouseknecht)
|
||||
- Fabian von Feilitzsch (@fabianvf)
|
||||
|
||||
description:
|
||||
- Uses the Kubernetes Python client to fetch a specific object by name, all matching objects within a
|
||||
@@ -117,7 +117,7 @@ DOCUMENTATION = '''
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Fetch a list of namespaces
|
||||
@@ -159,39 +159,52 @@ RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- One ore more object definitions returned from the API.
|
||||
type: complex
|
||||
contains:
|
||||
api_version:
|
||||
description: The versioned schema of this representation of an object.
|
||||
returned: success
|
||||
type: str
|
||||
kind:
|
||||
description: Represents the REST resource this object represents.
|
||||
returned: success
|
||||
type: str
|
||||
metadata:
|
||||
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
|
||||
returned: success
|
||||
type: complex
|
||||
spec:
|
||||
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
|
||||
returned: success
|
||||
type: complex
|
||||
status:
|
||||
description: Current status details for the object.
|
||||
returned: success
|
||||
type: complex
|
||||
type: list
|
||||
elements: dict
|
||||
sample:
|
||||
- kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
creationTimestamp: "2022-03-04T13:59:49Z"
|
||||
name: my-config-map
|
||||
namespace: default
|
||||
resourceVersion: "418"
|
||||
uid: 5714b011-d090-4eac-8272-a0ea82ec0abd
|
||||
data:
|
||||
key1: val1
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.common._collections_compat import KeysView
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.common.validation import check_type_bool
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import K8sAnsibleMixin, get_api_client
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
||||
create_definitions,
|
||||
)
|
||||
|
||||
try:
|
||||
enable_turbo_mode = check_type_bool(os.environ.get("ENABLE_TURBO_MODE"))
|
||||
except TypeError:
|
||||
enable_turbo_mode = False
|
||||
|
||||
if enable_turbo_mode:
|
||||
try:
|
||||
from ansible_collections.cloud.common.plugins.plugin_utils.turbo.lookup import (
|
||||
TurboLookupBase as LookupBase,
|
||||
)
|
||||
except ImportError:
|
||||
from ansible.plugins.lookup import LookupBase # noqa: F401
|
||||
else:
|
||||
from ansible.plugins.lookup import LookupBase # noqa: F401
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import NotFoundError
|
||||
|
||||
HAS_K8S_MODULE_HELPER = True
|
||||
k8s_import_exception = None
|
||||
except ImportError as e:
|
||||
@@ -199,13 +212,14 @@ except ImportError as e:
|
||||
k8s_import_exception = e
|
||||
|
||||
|
||||
class KubernetesLookup(K8sAnsibleMixin):
|
||||
|
||||
class KubernetesLookup(object):
|
||||
def __init__(self):
|
||||
|
||||
if not HAS_K8S_MODULE_HELPER:
|
||||
raise Exception(
|
||||
"Requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(k8s_import_exception)
|
||||
"Requires the Kubernetes Python client. Try `pip install kubernetes`. Detail: {0}".format(
|
||||
k8s_import_exception
|
||||
)
|
||||
)
|
||||
|
||||
self.kind = None
|
||||
@@ -226,31 +240,38 @@ class KubernetesLookup(K8sAnsibleMixin):
|
||||
self.params = kwargs
|
||||
self.client = get_api_client(**kwargs)
|
||||
|
||||
cluster_info = kwargs.get('cluster_info')
|
||||
if cluster_info == 'version':
|
||||
return [self.client.version]
|
||||
if cluster_info == 'api_groups':
|
||||
cluster_info = kwargs.get("cluster_info")
|
||||
if cluster_info == "version":
|
||||
return [self.client.client.version]
|
||||
if cluster_info == "api_groups":
|
||||
if isinstance(self.client.resources.api_groups, KeysView):
|
||||
return [list(self.client.resources.api_groups)]
|
||||
return [self.client.resources.api_groups]
|
||||
|
||||
self.kind = kwargs.get('kind')
|
||||
self.name = kwargs.get('resource_name')
|
||||
self.namespace = kwargs.get('namespace')
|
||||
self.api_version = kwargs.get('api_version', 'v1')
|
||||
self.label_selector = kwargs.get('label_selector')
|
||||
self.field_selector = kwargs.get('field_selector')
|
||||
self.include_uninitialized = kwargs.get('include_uninitialized', False)
|
||||
self.kind = kwargs.get("kind")
|
||||
self.name = kwargs.get("resource_name")
|
||||
self.namespace = kwargs.get("namespace")
|
||||
self.api_version = kwargs.get("api_version", "v1")
|
||||
self.label_selector = kwargs.get("label_selector")
|
||||
self.field_selector = kwargs.get("field_selector")
|
||||
self.include_uninitialized = kwargs.get("include_uninitialized", False)
|
||||
|
||||
resource_definition = kwargs.get('resource_definition')
|
||||
src = kwargs.get('src')
|
||||
resource_definition = kwargs.get("resource_definition")
|
||||
src = kwargs.get("src")
|
||||
if src:
|
||||
resource_definition = self.load_resource_definitions(src)[0]
|
||||
definitions = create_definitions(params=dict(src=src))
|
||||
if definitions:
|
||||
self.kind = definitions[0].kind
|
||||
self.name = definitions[0].name
|
||||
self.namespace = definitions[0].namespace
|
||||
self.api_version = definitions[0].api_version or "v1"
|
||||
if resource_definition:
|
||||
self.kind = resource_definition.get('kind', self.kind)
|
||||
self.api_version = resource_definition.get('apiVersion', self.api_version)
|
||||
self.name = resource_definition.get('metadata', {}).get('name', self.name)
|
||||
self.namespace = resource_definition.get('metadata', {}).get('namespace', self.namespace)
|
||||
self.kind = resource_definition.get("kind", self.kind)
|
||||
self.api_version = resource_definition.get("apiVersion", self.api_version)
|
||||
self.name = resource_definition.get("metadata", {}).get("name", self.name)
|
||||
self.namespace = resource_definition.get("metadata", {}).get(
|
||||
"namespace", self.namespace
|
||||
)
|
||||
|
||||
if not self.kind:
|
||||
raise AnsibleError(
|
||||
@@ -258,19 +279,26 @@ class KubernetesLookup(K8sAnsibleMixin):
|
||||
"using the 'resource_definition' parameter."
|
||||
)
|
||||
|
||||
resource = self.find_resource(self.kind, self.api_version, fail=True)
|
||||
resource = self.client.resource(self.kind, self.api_version)
|
||||
try:
|
||||
k8s_obj = resource.get(name=self.name, namespace=self.namespace, label_selector=self.label_selector, field_selector=self.field_selector)
|
||||
params = dict(
|
||||
name=self.name,
|
||||
namespace=self.namespace,
|
||||
label_selector=self.label_selector,
|
||||
field_selector=self.field_selector,
|
||||
)
|
||||
k8s_obj = self.client.get(resource, **params)
|
||||
except NotFoundError:
|
||||
return []
|
||||
|
||||
if self.name:
|
||||
return [k8s_obj.to_dict()]
|
||||
|
||||
return k8s_obj.to_dict().get('items')
|
||||
return k8s_obj.to_dict().get("items")
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
def _run(self, terms, variables=None, **kwargs):
|
||||
return KubernetesLookup().run(terms, variables=variables, **kwargs)
|
||||
|
||||
run = _run if not hasattr(LookupBase, "run_on_daemon") else LookupBase.run_on_daemon
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
DOCUMENTATION = '''
|
||||
lookup: kustomize
|
||||
DOCUMENTATION = """
|
||||
name: kustomize
|
||||
|
||||
short_description: Build a set of kubernetes resources using a 'kustomization.yaml' file.
|
||||
|
||||
version_added: "2.2.0"
|
||||
|
||||
author:
|
||||
- Aubin Bikouo <@abikouo>
|
||||
- Aubin Bikouo (@abikouo)
|
||||
notes:
|
||||
- If both kustomize and kubectl are part of the PATH, kustomize will be used by the plugin.
|
||||
description:
|
||||
@@ -33,7 +33,7 @@ DOCUMENTATION = '''
|
||||
|
||||
requirements:
|
||||
- "python >= 3.6"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Run lookup using kustomize
|
||||
@@ -52,29 +52,16 @@ EXAMPLES = """
|
||||
RETURN = """
|
||||
_list:
|
||||
description:
|
||||
- One ore more object definitions returned from the tool execution.
|
||||
type: complex
|
||||
contains:
|
||||
api_version:
|
||||
description: The versioned schema of this representation of an object.
|
||||
returned: success
|
||||
type: str
|
||||
kind:
|
||||
description: Represents the REST resource this object represents.
|
||||
returned: success
|
||||
type: str
|
||||
- YAML string for the object definitions returned from the tool execution.
|
||||
type: str
|
||||
sample:
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
|
||||
returned: success
|
||||
type: complex
|
||||
spec:
|
||||
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
|
||||
returned: success
|
||||
type: complex
|
||||
status:
|
||||
description: Current status details for the object.
|
||||
returned: success
|
||||
type: complex
|
||||
name: my-config-map
|
||||
namespace: default
|
||||
data:
|
||||
key1: val1
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
@@ -91,7 +78,7 @@ def get_binary_from_path(name, opt_dirs=None):
|
||||
if opt_dirs is not None:
|
||||
if not isinstance(opt_dirs, list):
|
||||
opt_dirs = [opt_dirs]
|
||||
opt_arg['opt_dirs'] = opt_dirs
|
||||
opt_arg["opt_dirs"] = opt_dirs
|
||||
bin_path = get_bin_path(name, **opt_arg)
|
||||
return bin_path
|
||||
except ValueError:
|
||||
@@ -104,30 +91,41 @@ def run_command(command):
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
def run(self, terms, variables=None, dir=".", binary_path=None, opt_dirs=None, **kwargs):
|
||||
def run(
|
||||
self, terms, variables=None, dir=".", binary_path=None, opt_dirs=None, **kwargs
|
||||
):
|
||||
executable_path = binary_path
|
||||
if executable_path is None:
|
||||
executable_path = get_binary_from_path(name="kustomize", opt_dirs=opt_dirs)
|
||||
if executable_path is None:
|
||||
executable_path = get_binary_from_path(name="kubectl", opt_dirs=opt_dirs)
|
||||
executable_path = get_binary_from_path(
|
||||
name="kubectl", opt_dirs=opt_dirs
|
||||
)
|
||||
|
||||
# validate that at least one tool was found
|
||||
if executable_path is None:
|
||||
raise AnsibleLookupError("Failed to find required executable 'kubectl' and 'kustomize' in paths")
|
||||
raise AnsibleLookupError(
|
||||
"Failed to find required executable 'kubectl' and 'kustomize' in paths"
|
||||
)
|
||||
|
||||
# check input directory
|
||||
kustomization_dir = dir
|
||||
|
||||
command = [executable_path]
|
||||
if executable_path.endswith('kustomize'):
|
||||
command += ['build', kustomization_dir]
|
||||
elif executable_path.endswith('kubectl'):
|
||||
command += ['kustomize', kustomization_dir]
|
||||
if executable_path.endswith("kustomize"):
|
||||
command += ["build", kustomization_dir]
|
||||
elif executable_path.endswith("kubectl"):
|
||||
command += ["kustomize", kustomization_dir]
|
||||
else:
|
||||
raise AnsibleLookupError("unexpected tool provided as parameter {0}, expected one of kustomize, kubectl.".format(executable_path))
|
||||
raise AnsibleLookupError(
|
||||
"unexpected tool provided as parameter {0}, expected one of kustomize, kubectl.".format(
|
||||
executable_path
|
||||
)
|
||||
)
|
||||
|
||||
(out, err) = run_command(command)
|
||||
if err:
|
||||
raise AnsibleLookupError("kustomize command failed with: {0}".format(err.decode("utf-8")))
|
||||
return [out.decode('utf-8')]
|
||||
raise AnsibleLookupError(
|
||||
"kustomize command failed with: {0}".format(err.decode("utf-8"))
|
||||
)
|
||||
return [out.decode("utf-8")]
|
||||
|
||||
344
plugins/module_utils/_version.py
Normal file
344
plugins/module_utils/_version.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# Vendored copy of distutils/version.py from CPython 3.9.5
|
||||
#
|
||||
# Implements multiple version numbering conventions for the
|
||||
# Python Module Distribution Utilities.
|
||||
#
|
||||
# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0)
|
||||
#
|
||||
|
||||
"""Provides classes to represent module version numbers (one class for
|
||||
each style of version numbering). There are currently two such classes
|
||||
implemented: StrictVersion and LooseVersion.
|
||||
|
||||
Every version number class implements the following interface:
|
||||
* the 'parse' method takes a string and parses it to some internal
|
||||
representation; if the string is an invalid version number,
|
||||
'parse' raises a ValueError exception
|
||||
* the class constructor takes an optional string argument which,
|
||||
if supplied, is passed to 'parse'
|
||||
* __str__ reconstructs the string that was passed to 'parse' (or
|
||||
an equivalent string -- ie. one that will generate an equivalent
|
||||
version number instance)
|
||||
* __repr__ generates Python code to recreate the version number instance
|
||||
* _cmp compares the current instance with either another instance
|
||||
of the same class or a string (which will be parsed to an instance
|
||||
of the same class, thus must follow the same rules)
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
try:
|
||||
RE_FLAGS = re.VERBOSE | re.ASCII
|
||||
except AttributeError:
|
||||
RE_FLAGS = re.VERBOSE
|
||||
|
||||
|
||||
class Version:
|
||||
"""Abstract base class for version numbering classes. Just provides
|
||||
constructor (__init__) and reproducer (__repr__), because those
|
||||
seem to be the same for all version numbering classes; and route
|
||||
rich comparisons to _cmp.
|
||||
"""
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s ('%s')" % (self.__class__.__name__, str(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c == 0
|
||||
|
||||
def __lt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c < 0
|
||||
|
||||
def __le__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c <= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c > 0
|
||||
|
||||
def __ge__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c >= 0
|
||||
|
||||
|
||||
# Interface for version-number classes -- must be implemented
|
||||
# by the following classes (the concrete ones -- Version should
|
||||
# be treated as an abstract class).
|
||||
# __init__ (string) - create and take same action as 'parse'
|
||||
# (string parameter is optional)
|
||||
# parse (string) - convert a string representation to whatever
|
||||
# internal representation is appropriate for
|
||||
# this style of version numbering
|
||||
# __str__ (self) - convert back to a string; should be very similar
|
||||
# (if not identical to) the string supplied to parse
|
||||
# __repr__ (self) - generate Python code to recreate
|
||||
# the instance
|
||||
# _cmp (self, other) - compare two version numbers ('other' may
|
||||
# be an unparsed version string, or another
|
||||
# instance of your version class)
|
||||
|
||||
|
||||
class StrictVersion(Version):
|
||||
"""Version numbering for anal retentives and software idealists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of two or three
|
||||
dot-separated numeric components, with an optional "pre-release" tag
|
||||
on the end. The pre-release tag consists of the letter 'a' or 'b'
|
||||
followed by a number. If the numeric components of two version
|
||||
numbers are equal, then one with a pre-release tag will always
|
||||
be deemed earlier (lesser) than one without.
|
||||
|
||||
The following are valid version numbers (shown in the order that
|
||||
would be obtained by sorting according to the supplied cmp function):
|
||||
|
||||
0.4 0.4.0 (these two are equivalent)
|
||||
0.4.1
|
||||
0.5a1
|
||||
0.5b3
|
||||
0.5
|
||||
0.9.6
|
||||
1.0
|
||||
1.0.4a3
|
||||
1.0.4b1
|
||||
1.0.4
|
||||
|
||||
The following are examples of invalid version numbers:
|
||||
|
||||
1
|
||||
2.7.2.2
|
||||
1.3.a4
|
||||
1.3pl1
|
||||
1.3c4
|
||||
|
||||
The rationale for this version numbering system will be explained
|
||||
in the distutils documentation.
|
||||
"""
|
||||
|
||||
version_re = re.compile(r"^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$", RE_FLAGS)
|
||||
|
||||
def parse(self, vstring):
|
||||
match = self.version_re.match(vstring)
|
||||
if not match:
|
||||
raise ValueError("invalid version number '%s'" % vstring)
|
||||
|
||||
(major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6)
|
||||
|
||||
if patch:
|
||||
self.version = tuple(map(int, [major, minor, patch]))
|
||||
else:
|
||||
self.version = tuple(map(int, [major, minor])) + (0,)
|
||||
|
||||
if prerelease:
|
||||
self.prerelease = (prerelease[0], int(prerelease_num))
|
||||
else:
|
||||
self.prerelease = None
|
||||
|
||||
def __str__(self):
|
||||
if self.version[2] == 0:
|
||||
vstring = ".".join(map(str, self.version[0:2]))
|
||||
else:
|
||||
vstring = ".".join(map(str, self.version))
|
||||
|
||||
if self.prerelease:
|
||||
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
|
||||
|
||||
return vstring
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = StrictVersion(other)
|
||||
elif not isinstance(other, StrictVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version != other.version:
|
||||
# numeric versions don't match
|
||||
# prerelease stuff doesn't matter
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
# have to compare prerelease
|
||||
# case 1: neither has prerelease; they're equal
|
||||
# case 2: self has prerelease, other doesn't; other is greater
|
||||
# case 3: self doesn't have prerelease, other does: self is greater
|
||||
# case 4: both have prerelease: must compare them!
|
||||
|
||||
if not self.prerelease and not other.prerelease:
|
||||
return 0
|
||||
elif self.prerelease and not other.prerelease:
|
||||
return -1
|
||||
elif not self.prerelease and other.prerelease:
|
||||
return 1
|
||||
elif self.prerelease and other.prerelease:
|
||||
if self.prerelease == other.prerelease:
|
||||
return 0
|
||||
elif self.prerelease < other.prerelease:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
else:
|
||||
raise AssertionError("never get here")
|
||||
|
||||
|
||||
# end class StrictVersion
|
||||
|
||||
# The rules according to Greg Stein:
|
||||
# 1) a version number has 1 or more numbers separated by a period or by
|
||||
# sequences of letters. If only periods, then these are compared
|
||||
# left-to-right to determine an ordering.
|
||||
# 2) sequences of letters are part of the tuple for comparison and are
|
||||
# compared lexicographically
|
||||
# 3) recognize the numeric components may have leading zeroes
|
||||
#
|
||||
# The LooseVersion class below implements these rules: a version number
|
||||
# string is split up into a tuple of integer and string components, and
|
||||
# comparison is a simple tuple comparison. This means that version
|
||||
# numbers behave in a predictable and obvious way, but a way that might
|
||||
# not necessarily be how people *want* version numbers to behave. There
|
||||
# wouldn't be a problem if people could stick to purely numeric version
|
||||
# numbers: just split on period and compare the numbers as tuples.
|
||||
# However, people insist on putting letters into their version numbers;
|
||||
# the most common purpose seems to be:
|
||||
# - indicating a "pre-release" version
|
||||
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
|
||||
# - indicating a post-release patch ('p', 'pl', 'patch')
|
||||
# but of course this can't cover all version number schemes, and there's
|
||||
# no way to know what a programmer means without asking him.
|
||||
#
|
||||
# The problem is what to do with letters (and other non-numeric
|
||||
# characters) in a version number. The current implementation does the
|
||||
# obvious and predictable thing: keep them as strings and compare
|
||||
# lexically within a tuple comparison. This has the desired effect if
|
||||
# an appended letter sequence implies something "post-release":
|
||||
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
|
||||
#
|
||||
# However, if letters in a version number imply a pre-release version,
|
||||
# the "obvious" thing isn't correct. Eg. you would expect that
|
||||
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
|
||||
# implemented here, this just isn't so.
|
||||
#
|
||||
# Two possible solutions come to mind. The first is to tie the
|
||||
# comparison algorithm to a particular set of semantic rules, as has
|
||||
# been done in the StrictVersion class above. This works great as long
|
||||
# as everyone can go along with bondage and discipline. Hopefully a
|
||||
# (large) subset of Python module programmers will agree that the
|
||||
# particular flavour of bondage and discipline provided by StrictVersion
|
||||
# provides enough benefit to be worth using, and will submit their
|
||||
# version numbering scheme to its domination. The free-thinking
|
||||
# anarchists in the lot will never give in, though, and something needs
|
||||
# to be done to accommodate them.
|
||||
#
|
||||
# Perhaps a "moderately strict" version class could be implemented that
|
||||
# lets almost anything slide (syntactically), and makes some heuristic
|
||||
# assumptions about non-digits in version number strings. This could
|
||||
# sink into special-case-hell, though; if I was as talented and
|
||||
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
|
||||
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
|
||||
# just as happy dealing with things like "2g6" and "1.13++". I don't
|
||||
# think I'm smart enough to do it right though.
|
||||
#
|
||||
# In any case, I've coded the test suite for this module (see
|
||||
# ../test/test_version.py) specifically to fail on things like comparing
|
||||
# "1.2a2" and "1.2". That's not because the *code* is doing anything
|
||||
# wrong, it's because the simple, obvious design doesn't match my
|
||||
# complicated, hairy expectations for real-world version numbers. It
|
||||
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
|
||||
# the Right Thing" (ie. the code matches the conception). But I'd rather
|
||||
# have a conception that matches common notions about version numbers.
|
||||
|
||||
|
||||
class LooseVersion(Version):
|
||||
"""Version numbering for anarchists and software realists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of a series of numbers,
|
||||
separated by either periods or strings of letters. When comparing
|
||||
version numbers, the numeric components will be compared
|
||||
numerically, and the alphabetic components lexically. The following
|
||||
are all valid version numbers, in no particular order:
|
||||
|
||||
1.5.1
|
||||
1.5.2b2
|
||||
161
|
||||
3.10a
|
||||
8.02
|
||||
3.4j
|
||||
1996.07.12
|
||||
3.2.pl0
|
||||
3.1.1.6
|
||||
2g6
|
||||
11g
|
||||
0.960923
|
||||
2.2beta29
|
||||
1.13++
|
||||
5.5.kw
|
||||
2.0b1pl0
|
||||
|
||||
In fact, there is no such thing as an invalid version number under
|
||||
this scheme; the rules for comparison are simple and predictable,
|
||||
but may not always give the results you want (for some definition
|
||||
of "want").
|
||||
"""
|
||||
|
||||
component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE)
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def parse(self, vstring):
|
||||
# I've given up on thinking I can reconstruct the version string
|
||||
# from the parsed tuple -- so I just store the string here for
|
||||
# use by __str__
|
||||
self.vstring = vstring
|
||||
components = [x for x in self.component_re.split(vstring) if x and x != "."]
|
||||
for i, obj in enumerate(components):
|
||||
try:
|
||||
components[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.version = components
|
||||
|
||||
def __str__(self):
|
||||
return self.vstring
|
||||
|
||||
def __repr__(self):
|
||||
return "LooseVersion ('%s')" % str(self)
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = LooseVersion(other)
|
||||
elif not isinstance(other, LooseVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version == other.version:
|
||||
return 0
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
if self.version > other.version:
|
||||
return 1
|
||||
|
||||
|
||||
# end class LooseVersion
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
@@ -17,6 +17,7 @@ if enable_turbo_mode:
|
||||
from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (
|
||||
AnsibleTurboModule as AnsibleModule,
|
||||
) # noqa: F401
|
||||
|
||||
AnsibleModule.collection_name = "kubernetes.core"
|
||||
except ImportError:
|
||||
from ansible.module_utils.basic import AnsibleModule # noqa: F401
|
||||
|
||||
@@ -14,13 +14,23 @@
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
|
||||
from ansible.module_utils.common.dict_transformations import dict_merge
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import ApplyException
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import (
|
||||
ApplyException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
gather_versions,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import NotFoundError
|
||||
@@ -28,50 +38,52 @@ except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
LAST_APPLIED_CONFIG_ANNOTATION = 'kubectl.kubernetes.io/last-applied-configuration'
|
||||
LAST_APPLIED_CONFIG_ANNOTATION = "kubectl.kubernetes.io/last-applied-configuration"
|
||||
|
||||
POD_SPEC_SUFFIXES = {
|
||||
'containers': 'name',
|
||||
'initContainers': 'name',
|
||||
'ephemeralContainers': 'name',
|
||||
'volumes': 'name',
|
||||
'imagePullSecrets': 'name',
|
||||
'containers.volumeMounts': 'mountPath',
|
||||
'containers.volumeDevices': 'devicePath',
|
||||
'containers.env': 'name',
|
||||
'containers.ports': 'containerPort',
|
||||
'initContainers.volumeMounts': 'mountPath',
|
||||
'initContainers.volumeDevices': 'devicePath',
|
||||
'initContainers.env': 'name',
|
||||
'initContainers.ports': 'containerPort',
|
||||
'ephemeralContainers.volumeMounts': 'mountPath',
|
||||
'ephemeralContainers.volumeDevices': 'devicePath',
|
||||
'ephemeralContainers.env': 'name',
|
||||
'ephemeralContainers.ports': 'containerPort',
|
||||
"containers": "name",
|
||||
"initContainers": "name",
|
||||
"ephemeralContainers": "name",
|
||||
"volumes": "name",
|
||||
"imagePullSecrets": "name",
|
||||
"containers.volumeMounts": "mountPath",
|
||||
"containers.volumeDevices": "devicePath",
|
||||
"containers.env": "name",
|
||||
"containers.ports": "containerPort",
|
||||
"initContainers.volumeMounts": "mountPath",
|
||||
"initContainers.volumeDevices": "devicePath",
|
||||
"initContainers.env": "name",
|
||||
"initContainers.ports": "containerPort",
|
||||
"ephemeralContainers.volumeMounts": "mountPath",
|
||||
"ephemeralContainers.volumeDevices": "devicePath",
|
||||
"ephemeralContainers.env": "name",
|
||||
"ephemeralContainers.ports": "containerPort",
|
||||
}
|
||||
|
||||
POD_SPEC_PREFIXES = [
|
||||
'Pod.spec',
|
||||
'Deployment.spec.template.spec',
|
||||
'DaemonSet.spec.template.spec',
|
||||
'StatefulSet.spec.template.spec',
|
||||
'Job.spec.template.spec',
|
||||
'Cronjob.spec.jobTemplate.spec.template.spec',
|
||||
"Pod.spec",
|
||||
"Deployment.spec.template.spec",
|
||||
"DaemonSet.spec.template.spec",
|
||||
"StatefulSet.spec.template.spec",
|
||||
"Job.spec.template.spec",
|
||||
"Cronjob.spec.jobTemplate.spec.template.spec",
|
||||
]
|
||||
|
||||
# patch merge keys taken from generated.proto files under
|
||||
# staging/src/k8s.io/api in kubernetes/kubernetes
|
||||
STRATEGIC_MERGE_PATCH_KEYS = {
|
||||
'Service.spec.ports': 'port',
|
||||
'ServiceAccount.secrets': 'name',
|
||||
'ValidatingWebhookConfiguration.webhooks': 'name',
|
||||
'MutatingWebhookConfiguration.webhooks': 'name',
|
||||
"Service.spec.ports": "port",
|
||||
"ServiceAccount.secrets": "name",
|
||||
"ValidatingWebhookConfiguration.webhooks": "name",
|
||||
"MutatingWebhookConfiguration.webhooks": "name",
|
||||
}
|
||||
|
||||
STRATEGIC_MERGE_PATCH_KEYS.update(
|
||||
{"%s.%s" % (prefix, key): value
|
||||
for prefix in POD_SPEC_PREFIXES
|
||||
for key, value in POD_SPEC_SUFFIXES.items()}
|
||||
{
|
||||
"%s.%s" % (prefix, key): value
|
||||
for prefix in POD_SPEC_PREFIXES
|
||||
for key, value in POD_SPEC_SUFFIXES.items()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -79,21 +91,28 @@ def annotate(desired):
|
||||
return dict(
|
||||
metadata=dict(
|
||||
annotations={
|
||||
LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(desired, separators=(',', ':'), indent=None, sort_keys=True)
|
||||
LAST_APPLIED_CONFIG_ANNOTATION: json.dumps(
|
||||
desired, separators=(",", ":"), indent=None, sort_keys=True
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def apply_patch(actual, desired):
|
||||
last_applied = actual['metadata'].get('annotations', {}).get(LAST_APPLIED_CONFIG_ANNOTATION)
|
||||
last_applied = (
|
||||
actual["metadata"].get("annotations", {}).get(LAST_APPLIED_CONFIG_ANNOTATION)
|
||||
)
|
||||
|
||||
if last_applied:
|
||||
# ensure that last_applied doesn't come back as a dict of unicode key/value pairs
|
||||
# json.loads can be used if we stop supporting python 2
|
||||
last_applied = json.loads(last_applied)
|
||||
patch = merge(dict_merge(last_applied, annotate(last_applied)),
|
||||
dict_merge(desired, annotate(desired)), actual)
|
||||
patch = merge(
|
||||
dict_merge(last_applied, annotate(last_applied)),
|
||||
dict_merge(desired, annotate(desired)),
|
||||
actual,
|
||||
)
|
||||
if patch:
|
||||
return actual, patch
|
||||
else:
|
||||
@@ -102,24 +121,52 @@ def apply_patch(actual, desired):
|
||||
return actual, dict_merge(desired, annotate(desired))
|
||||
|
||||
|
||||
def apply_object(resource, definition):
|
||||
def apply_object(resource, definition, server_side=False):
|
||||
try:
|
||||
actual = resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'))
|
||||
actual = resource.get(
|
||||
name=definition["metadata"]["name"],
|
||||
namespace=definition["metadata"].get("namespace"),
|
||||
)
|
||||
if server_side:
|
||||
return actual, None
|
||||
except NotFoundError:
|
||||
return None, dict_merge(definition, annotate(definition))
|
||||
return apply_patch(actual.to_dict(), definition)
|
||||
|
||||
|
||||
def k8s_apply(resource, definition):
|
||||
def k8s_apply(resource, definition, **kwargs):
|
||||
existing, desired = apply_object(resource, definition)
|
||||
server_side = kwargs.get("server_side", False)
|
||||
if server_side:
|
||||
versions = gather_versions()
|
||||
body = definition
|
||||
if LooseVersion(versions["kubernetes"]) < LooseVersion("25.0.0"):
|
||||
body = json.dumps(definition).encode()
|
||||
# server_side_apply is forces content_type to 'application/apply-patch+yaml'
|
||||
return resource.server_side_apply(
|
||||
body=body,
|
||||
name=definition["metadata"]["name"],
|
||||
namespace=definition["metadata"].get("namespace"),
|
||||
force_conflicts=kwargs.get("force_conflicts"),
|
||||
field_manager=kwargs.get("field_manager"),
|
||||
dry_run=kwargs.get("dry_run"),
|
||||
)
|
||||
if not existing:
|
||||
return resource.create(body=desired, namespace=definition['metadata'].get('namespace'))
|
||||
return resource.create(
|
||||
body=desired, namespace=definition["metadata"].get("namespace"), **kwargs
|
||||
)
|
||||
if existing == desired:
|
||||
return resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'))
|
||||
return resource.patch(body=desired,
|
||||
name=definition['metadata']['name'],
|
||||
namespace=definition['metadata'].get('namespace'),
|
||||
content_type='application/merge-patch+json')
|
||||
return resource.get(
|
||||
name=definition["metadata"]["name"],
|
||||
namespace=definition["metadata"].get("namespace"),
|
||||
)
|
||||
return resource.patch(
|
||||
body=desired,
|
||||
name=definition["metadata"]["name"],
|
||||
namespace=definition["metadata"].get("namespace"),
|
||||
content_type="application/merge-patch+json",
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
# The patch is the difference from actual to desired without deletions, plus deletions
|
||||
@@ -128,7 +175,7 @@ def k8s_apply(resource, definition):
|
||||
# deletions, and then apply delta to deletions as a patch, which should be strictly additive.
|
||||
def merge(last_applied, desired, actual, position=None):
|
||||
deletions = get_deletions(last_applied, desired)
|
||||
delta = get_delta(last_applied, actual, desired, position or desired['kind'])
|
||||
delta = get_delta(last_applied, actual, desired, position or desired["kind"])
|
||||
return dict_merge(deletions, delta)
|
||||
|
||||
|
||||
@@ -138,7 +185,9 @@ def list_to_dict(lst, key, position):
|
||||
try:
|
||||
result[item[key]] = item
|
||||
except KeyError:
|
||||
raise ApplyException("Expected key '%s' not found in position %s" % (key, position))
|
||||
raise ApplyException(
|
||||
"Expected key '%s' not found in position %s" % (key, position)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -157,7 +206,12 @@ def list_merge(last_applied, actual, desired, position):
|
||||
if key not in actual_dict or key not in last_applied_dict:
|
||||
result.append(desired_dict[key])
|
||||
else:
|
||||
patch = merge(last_applied_dict[key], desired_dict[key], actual_dict[key], position)
|
||||
patch = merge(
|
||||
last_applied_dict[key],
|
||||
desired_dict[key],
|
||||
actual_dict[key],
|
||||
position,
|
||||
)
|
||||
result.append(dict_merge(actual_dict[key], patch))
|
||||
for key in actual_dict:
|
||||
if key not in desired_dict and key not in last_applied_dict:
|
||||
@@ -197,11 +251,11 @@ def recursive_list_diff(list1, list2, position=None):
|
||||
|
||||
def recursive_diff(dict1, dict2, position=None):
|
||||
if not position:
|
||||
if 'kind' in dict1 and dict1.get('kind') == dict2.get('kind'):
|
||||
position = dict1['kind']
|
||||
if "kind" in dict1 and dict1.get("kind") == dict2.get("kind"):
|
||||
position = dict1["kind"]
|
||||
left = dict((k, v) for (k, v) in dict1.items() if k not in dict2)
|
||||
right = dict((k, v) for (k, v) in dict2.items() if k not in dict1)
|
||||
for k in (set(dict1.keys()) & set(dict2.keys())):
|
||||
for k in set(dict1.keys()) & set(dict2.keys()):
|
||||
if position:
|
||||
this_position = "%s.%s" % (position, k)
|
||||
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
|
||||
@@ -246,11 +300,15 @@ def get_delta(last_applied, actual, desired, position=None):
|
||||
if actual_value is None:
|
||||
patch[k] = desired_value
|
||||
elif isinstance(desired_value, dict):
|
||||
p = get_delta(last_applied.get(k, {}), actual_value, desired_value, this_position)
|
||||
p = get_delta(
|
||||
last_applied.get(k, {}), actual_value, desired_value, this_position
|
||||
)
|
||||
if p:
|
||||
patch[k] = p
|
||||
elif isinstance(desired_value, list):
|
||||
p = list_merge(last_applied.get(k, []), actual_value, desired_value, this_position)
|
||||
p = list_merge(
|
||||
last_applied.get(k, []), actual_value, desired_value, this_position
|
||||
)
|
||||
if p:
|
||||
patch[k] = [item for item in p if item is not None]
|
||||
elif actual_value != desired_value:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
@@ -12,133 +12,87 @@ def list_dict_str(value):
|
||||
|
||||
|
||||
AUTH_PROXY_HEADERS_SPEC = dict(
|
||||
proxy_basic_auth=dict(type='str', no_log=True),
|
||||
basic_auth=dict(type='str', no_log=True),
|
||||
user_agent=dict(type='str')
|
||||
proxy_basic_auth=dict(type="str", no_log=True),
|
||||
basic_auth=dict(type="str", no_log=True),
|
||||
user_agent=dict(type="str"),
|
||||
)
|
||||
|
||||
AUTH_ARG_SPEC = {
|
||||
'kubeconfig': {
|
||||
'type': 'raw',
|
||||
},
|
||||
'context': {},
|
||||
'host': {},
|
||||
'api_key': {
|
||||
'no_log': True,
|
||||
},
|
||||
'username': {},
|
||||
'password': {
|
||||
'no_log': True,
|
||||
},
|
||||
'validate_certs': {
|
||||
'type': 'bool',
|
||||
'aliases': ['verify_ssl'],
|
||||
},
|
||||
'ca_cert': {
|
||||
'type': 'path',
|
||||
'aliases': ['ssl_ca_cert'],
|
||||
},
|
||||
'client_cert': {
|
||||
'type': 'path',
|
||||
'aliases': ['cert_file'],
|
||||
},
|
||||
'client_key': {
|
||||
'type': 'path',
|
||||
'aliases': ['key_file'],
|
||||
},
|
||||
'proxy': {
|
||||
'type': 'str',
|
||||
},
|
||||
'proxy_headers': {
|
||||
'type': 'dict',
|
||||
'options': AUTH_PROXY_HEADERS_SPEC
|
||||
},
|
||||
'persist_config': {
|
||||
'type': 'bool',
|
||||
},
|
||||
"kubeconfig": {"type": "raw"},
|
||||
"context": {},
|
||||
"host": {},
|
||||
"api_key": {"no_log": True},
|
||||
"username": {},
|
||||
"password": {"no_log": True},
|
||||
"validate_certs": {"type": "bool", "aliases": ["verify_ssl"]},
|
||||
"ca_cert": {"type": "path", "aliases": ["ssl_ca_cert"]},
|
||||
"client_cert": {"type": "path", "aliases": ["cert_file"]},
|
||||
"client_key": {"type": "path", "aliases": ["key_file"]},
|
||||
"proxy": {"type": "str"},
|
||||
"no_proxy": {"type": "str"},
|
||||
"proxy_headers": {"type": "dict", "options": AUTH_PROXY_HEADERS_SPEC},
|
||||
"persist_config": {"type": "bool"},
|
||||
"impersonate_user": {},
|
||||
"impersonate_groups": {"type": "list", "elements": "str"},
|
||||
}
|
||||
|
||||
WAIT_ARG_SPEC = dict(
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_sleep=dict(type='int', default=5),
|
||||
wait_timeout=dict(type='int', default=120),
|
||||
wait=dict(type="bool", default=False),
|
||||
wait_sleep=dict(type="int", default=5),
|
||||
wait_timeout=dict(type="int", default=120),
|
||||
wait_condition=dict(
|
||||
type='dict',
|
||||
type="dict",
|
||||
default=None,
|
||||
options=dict(
|
||||
type=dict(),
|
||||
status=dict(default=True, choices=[True, False, "Unknown"]),
|
||||
reason=dict()
|
||||
)
|
||||
)
|
||||
reason=dict(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Map kubernetes-client parameters to ansible parameters
|
||||
AUTH_ARG_MAP = {
|
||||
'kubeconfig': 'kubeconfig',
|
||||
'context': 'context',
|
||||
'host': 'host',
|
||||
'api_key': 'api_key',
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
'verify_ssl': 'validate_certs',
|
||||
'ssl_ca_cert': 'ca_cert',
|
||||
'cert_file': 'client_cert',
|
||||
'key_file': 'client_key',
|
||||
'proxy': 'proxy',
|
||||
'proxy_headers': 'proxy_headers',
|
||||
'persist_config': 'persist_config',
|
||||
"kubeconfig": "kubeconfig",
|
||||
"context": "context",
|
||||
"host": "host",
|
||||
"api_key": "api_key",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"verify_ssl": "validate_certs",
|
||||
"ssl_ca_cert": "ca_cert",
|
||||
"cert_file": "client_cert",
|
||||
"key_file": "client_key",
|
||||
"proxy": "proxy",
|
||||
"no_proxy": "no_proxy",
|
||||
"proxy_headers": "proxy_headers",
|
||||
"persist_config": "persist_config",
|
||||
}
|
||||
|
||||
NAME_ARG_SPEC = {
|
||||
'kind': {},
|
||||
'name': {},
|
||||
'namespace': {},
|
||||
'api_version': {
|
||||
'default': 'v1',
|
||||
'aliases': ['api', 'version'],
|
||||
},
|
||||
"kind": {},
|
||||
"name": {},
|
||||
"namespace": {},
|
||||
"api_version": {"default": "v1", "aliases": ["api", "version"]},
|
||||
}
|
||||
|
||||
COMMON_ARG_SPEC = {
|
||||
'state': {
|
||||
'default': 'present',
|
||||
'choices': ['present', 'absent'],
|
||||
},
|
||||
'force': {
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
"state": {"default": "present", "choices": ["present", "absent"]},
|
||||
"force": {"type": "bool", "default": False},
|
||||
}
|
||||
|
||||
RESOURCE_ARG_SPEC = {
|
||||
'resource_definition': {
|
||||
'type': list_dict_str,
|
||||
'aliases': ['definition', 'inline']
|
||||
},
|
||||
'src': {
|
||||
'type': 'path',
|
||||
},
|
||||
"resource_definition": {"type": list_dict_str, "aliases": ["definition", "inline"]},
|
||||
"src": {"type": "path"},
|
||||
}
|
||||
|
||||
ARG_ATTRIBUTES_BLACKLIST = ('property_path',)
|
||||
ARG_ATTRIBUTES_BLACKLIST = ("property_path",)
|
||||
|
||||
DELETE_OPTS_ARG_SPEC = {
|
||||
'propagationPolicy': {
|
||||
'choices': ['Foreground', 'Background', 'Orphan'],
|
||||
"propagationPolicy": {"choices": ["Foreground", "Background", "Orphan"]},
|
||||
"gracePeriodSeconds": {"type": "int"},
|
||||
"preconditions": {
|
||||
"type": "dict",
|
||||
"options": {"resourceVersion": {"type": "str"}, "uid": {"type": "str"}},
|
||||
},
|
||||
'gracePeriodSeconds': {
|
||||
'type': 'int',
|
||||
},
|
||||
'preconditions': {
|
||||
'type': 'dict',
|
||||
'options': {
|
||||
'resourceVersion': {
|
||||
'type': 'str',
|
||||
},
|
||||
'uid': {
|
||||
'type': 'str',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,26 @@ from functools import partial
|
||||
import kubernetes.dynamic
|
||||
import kubernetes.dynamic.discovery
|
||||
from kubernetes import __version__
|
||||
from kubernetes.dynamic.exceptions import (ResourceNotFoundError, ResourceNotUniqueError,
|
||||
ServiceUnavailableError)
|
||||
from kubernetes.dynamic.exceptions import (
|
||||
ResourceNotFoundError,
|
||||
ResourceNotUniqueError,
|
||||
ServiceUnavailableError,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import ResourceList
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import (
|
||||
ResourceList,
|
||||
)
|
||||
|
||||
|
||||
class Discoverer(kubernetes.dynamic.discovery.Discoverer):
|
||||
def __init__(self, client, cache_file):
|
||||
self.client = client
|
||||
default_cache_file_name = 'k8srcp-{0}.json'.format(hashlib.sha256(self.__get_default_cache_id()).hexdigest())
|
||||
self.__cache_file = cache_file or os.path.join(tempfile.gettempdir(), default_cache_file_name)
|
||||
default_cache_file_name = "k8srcp-{0}.json".format(
|
||||
hashlib.sha256(self.__get_default_cache_id()).hexdigest()
|
||||
)
|
||||
self.__cache_file = cache_file or os.path.join(
|
||||
tempfile.gettempdir(), default_cache_file_name
|
||||
)
|
||||
self.__init_cache()
|
||||
|
||||
def __get_default_cache_id(self):
|
||||
@@ -42,21 +51,21 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
|
||||
cache_id = "{0}-{1}".format(self.client.configuration.host, user)
|
||||
else:
|
||||
cache_id = self.client.configuration.host
|
||||
return cache_id.encode('utf-8')
|
||||
return cache_id.encode("utf-8")
|
||||
|
||||
def __get_user(self):
|
||||
# This is intended to provide a portable method for getting a username.
|
||||
# It could, and maybe should, be replaced by getpass.getuser() but, due
|
||||
# to a lack of portability testing the original code is being left in
|
||||
# place.
|
||||
if hasattr(os, 'getlogin'):
|
||||
if hasattr(os, "getlogin"):
|
||||
try:
|
||||
user = os.getlogin()
|
||||
if user:
|
||||
return str(user)
|
||||
except OSError:
|
||||
pass
|
||||
if hasattr(os, 'getuid'):
|
||||
if hasattr(os, "getuid"):
|
||||
try:
|
||||
user = os.getuid()
|
||||
if user:
|
||||
@@ -70,13 +79,13 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
|
||||
|
||||
def __init_cache(self, refresh=False):
|
||||
if refresh or not os.path.exists(self.__cache_file):
|
||||
self._cache = {'library_version': __version__}
|
||||
self._cache = {"library_version": __version__}
|
||||
refresh = True
|
||||
else:
|
||||
try:
|
||||
with open(self.__cache_file, 'r') as f:
|
||||
with open(self.__cache_file, "r") as f:
|
||||
self._cache = json.load(f, cls=partial(CacheDecoder, self.client))
|
||||
if self._cache.get('library_version') != __version__:
|
||||
if self._cache.get("library_version") != __version__:
|
||||
# Version mismatch, need to refresh cache
|
||||
self.invalidate_cache()
|
||||
except Exception:
|
||||
@@ -87,26 +96,30 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
|
||||
self._write_cache()
|
||||
|
||||
def get_resources_for_api_version(self, prefix, group, version, preferred):
|
||||
""" returns a dictionary of resources associated with provided (prefix, group, version)"""
|
||||
"""returns a dictionary of resources associated with provided (prefix, group, version)"""
|
||||
|
||||
resources = defaultdict(list)
|
||||
subresources = defaultdict(dict)
|
||||
|
||||
path = '/'.join(filter(None, [prefix, group, version]))
|
||||
path = "/".join(filter(None, [prefix, group, version]))
|
||||
try:
|
||||
resources_response = self.client.request('GET', path).resources or []
|
||||
resources_response = self.client.request("GET", path).resources or []
|
||||
except ServiceUnavailableError:
|
||||
resources_response = []
|
||||
|
||||
resources_raw = list(filter(lambda resource: '/' not in resource['name'], resources_response))
|
||||
subresources_raw = list(filter(lambda resource: '/' in resource['name'], resources_response))
|
||||
resources_raw = list(
|
||||
filter(lambda resource: "/" not in resource["name"], resources_response)
|
||||
)
|
||||
subresources_raw = list(
|
||||
filter(lambda resource: "/" in resource["name"], resources_response)
|
||||
)
|
||||
for subresource in subresources_raw:
|
||||
resource, name = subresource['name'].split('/')
|
||||
resource, name = subresource["name"].split("/", 1)
|
||||
subresources[resource][name] = subresource
|
||||
|
||||
for resource in resources_raw:
|
||||
# Prevent duplicate keys
|
||||
for key in ('prefix', 'group', 'api_version', 'client', 'preferred'):
|
||||
for key in ("prefix", "group", "api_version", "client", "preferred"):
|
||||
resource.pop(key, None)
|
||||
|
||||
resourceobj = kubernetes.dynamic.Resource(
|
||||
@@ -115,19 +128,25 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
|
||||
api_version=version,
|
||||
client=self.client,
|
||||
preferred=preferred,
|
||||
subresources=subresources.get(resource['name']),
|
||||
subresources=subresources.get(resource["name"]),
|
||||
**resource
|
||||
)
|
||||
resources[resource['kind']].append(resourceobj)
|
||||
resources[resource["kind"]].append(resourceobj)
|
||||
|
||||
resource_lookup = {
|
||||
'prefix': prefix,
|
||||
'group': group,
|
||||
'api_version': version,
|
||||
'kind': resourceobj.kind,
|
||||
'name': resourceobj.name
|
||||
"prefix": prefix,
|
||||
"group": group,
|
||||
"api_version": version,
|
||||
"kind": resourceobj.kind,
|
||||
"name": resourceobj.name,
|
||||
}
|
||||
resource_list = ResourceList(self.client, group=group, api_version=version, base_kind=resource['kind'], base_resource_lookup=resource_lookup)
|
||||
resource_list = ResourceList(
|
||||
self.client,
|
||||
group=group,
|
||||
api_version=version,
|
||||
base_kind=resource["kind"],
|
||||
base_resource_lookup=resource_lookup,
|
||||
)
|
||||
resources[resource_list.kind].append(resource_list)
|
||||
return resources
|
||||
|
||||
@@ -139,23 +158,32 @@ class Discoverer(kubernetes.dynamic.discovery.Discoverer):
|
||||
"""
|
||||
results = self.search(**kwargs)
|
||||
# If there are multiple matches, prefer exact matches on api_version
|
||||
if len(results) > 1 and kwargs.get('api_version'):
|
||||
if len(results) > 1 and kwargs.get("api_version"):
|
||||
results = [
|
||||
result for result in results if result.group_version == kwargs['api_version']
|
||||
result
|
||||
for result in results
|
||||
if result.group_version == kwargs["api_version"]
|
||||
]
|
||||
# If there are multiple matches, prefer non-List kinds
|
||||
if len(results) > 1 and not all(isinstance(x, ResourceList) for x in results):
|
||||
results = [result for result in results if not isinstance(result, ResourceList)]
|
||||
results = [
|
||||
result for result in results if not isinstance(result, ResourceList)
|
||||
]
|
||||
# if multiple resources are found that share a GVK, prefer the one with the most supported verbs
|
||||
if len(results) > 1 and len(set((x.group_version, x.kind) for x in results)) == 1:
|
||||
if (
|
||||
len(results) > 1
|
||||
and len(set((x.group_version, x.kind) for x in results)) == 1
|
||||
):
|
||||
if len(set(len(x.verbs) for x in results)) != 1:
|
||||
results = [max(results, key=lambda x: len(x.verbs))]
|
||||
if len(results) == 1:
|
||||
return results[0]
|
||||
elif not results:
|
||||
raise ResourceNotFoundError('No matches found for {0}'.format(kwargs))
|
||||
raise ResourceNotFoundError("No matches found for {0}".format(kwargs))
|
||||
else:
|
||||
raise ResourceNotUniqueError('Multiple matches found for {0}: {1}'.format(kwargs, results))
|
||||
raise ResourceNotUniqueError(
|
||||
"Multiple matches found for {0}: {1}".format(kwargs, results)
|
||||
)
|
||||
|
||||
|
||||
class LazyDiscoverer(Discoverer, kubernetes.dynamic.LazyDiscoverer):
|
||||
@@ -174,13 +202,15 @@ class CacheDecoder(json.JSONDecoder):
|
||||
json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
|
||||
|
||||
def object_hook(self, obj):
|
||||
if '_type' not in obj:
|
||||
if "_type" not in obj:
|
||||
return obj
|
||||
_type = obj.pop('_type')
|
||||
if _type == 'Resource':
|
||||
_type = obj.pop("_type")
|
||||
if _type == "Resource":
|
||||
return kubernetes.dynamic.Resource(client=self.client, **obj)
|
||||
elif _type == 'ResourceList':
|
||||
elif _type == "ResourceList":
|
||||
return ResourceList(self.client, **obj)
|
||||
elif _type == 'ResourceGroup':
|
||||
return kubernetes.dynamic.discovery.ResourceGroup(obj['preferred'], resources=self.object_hook(obj['resources']))
|
||||
elif _type == "ResourceGroup":
|
||||
return kubernetes.dynamic.discovery.ResourceGroup(
|
||||
obj["preferred"], resources=self.object_hook(obj["resources"])
|
||||
)
|
||||
return obj
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
@@ -21,11 +22,19 @@ import kubernetes.dynamic
|
||||
|
||||
|
||||
class ResourceList(kubernetes.dynamic.resource.ResourceList):
|
||||
def __init__(self, client, group='', api_version='v1', base_kind='', kind=None, base_resource_lookup=None):
|
||||
def __init__(
|
||||
self,
|
||||
client,
|
||||
group="",
|
||||
api_version="v1",
|
||||
base_kind="",
|
||||
kind=None,
|
||||
base_resource_lookup=None,
|
||||
):
|
||||
self.client = client
|
||||
self.group = group
|
||||
self.api_version = api_version
|
||||
self.kind = kind or '{0}List'.format(base_kind)
|
||||
self.kind = kind or "{0}List".format(base_kind)
|
||||
self.base_kind = base_kind
|
||||
self.base_resource_lookup = base_resource_lookup
|
||||
self.__base_resource = None
|
||||
@@ -34,16 +43,18 @@ class ResourceList(kubernetes.dynamic.resource.ResourceList):
|
||||
if self.__base_resource:
|
||||
return self.__base_resource
|
||||
elif self.base_resource_lookup:
|
||||
self.__base_resource = self.client.resources.get(**self.base_resource_lookup)
|
||||
self.__base_resource = self.client.resources.get(
|
||||
**self.base_resource_lookup
|
||||
)
|
||||
return self.__base_resource
|
||||
return None
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'_type': 'ResourceList',
|
||||
'group': self.group,
|
||||
'api_version': self.api_version,
|
||||
'kind': self.kind,
|
||||
'base_kind': self.base_kind,
|
||||
'base_resource_lookup': self.base_resource_lookup
|
||||
"_type": "ResourceList",
|
||||
"group": self.group,
|
||||
"api_version": self.api_version,
|
||||
"kind": self.kind,
|
||||
"base_kind": self.base_kind,
|
||||
"base_resource_lookup": self.base_resource_lookup,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
444
plugins/module_utils/copy.py
Normal file
444
plugins/module_utils/copy.py
Normal file
@@ -0,0 +1,444 @@
|
||||
# Copyright [2021] [Red Hat, Inc.]
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
from tempfile import TemporaryFile, NamedTemporaryFile
|
||||
from select import select
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import tarfile
|
||||
|
||||
# from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
from kubernetes.client.api import core_v1_api
|
||||
from kubernetes.stream import stream
|
||||
from kubernetes.stream.ws_client import (
|
||||
STDOUT_CHANNEL,
|
||||
STDERR_CHANNEL,
|
||||
ERROR_CHANNEL,
|
||||
ABNF,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
# ImportError are managed by the common module already.
|
||||
pass
|
||||
|
||||
|
||||
class K8SCopy(metaclass=ABCMeta):
|
||||
def __init__(self, module, client):
|
||||
self.client = client
|
||||
self.module = module
|
||||
self.api_instance = core_v1_api.CoreV1Api(client.client)
|
||||
|
||||
self.local_path = module.params.get("local_path")
|
||||
self.name = module.params.get("pod")
|
||||
self.namespace = module.params.get("namespace")
|
||||
self.remote_path = module.params.get("remote_path")
|
||||
self.content = module.params.get("content")
|
||||
|
||||
self.no_preserve = module.params.get("no_preserve")
|
||||
self.container_arg = {}
|
||||
if module.params.get("container"):
|
||||
self.container_arg["container"] = module.params.get("container")
|
||||
self.check_mode = self.module.check_mode
|
||||
|
||||
def _run_from_pod(self, cmd):
|
||||
try:
|
||||
resp = stream(
|
||||
self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=cmd,
|
||||
async_req=False,
|
||||
stderr=True,
|
||||
stdin=False,
|
||||
stdout=True,
|
||||
tty=False,
|
||||
_preload_content=False,
|
||||
**self.container_arg,
|
||||
)
|
||||
|
||||
stderr, stdout = [], []
|
||||
while resp.is_open():
|
||||
resp.update(timeout=1)
|
||||
if resp.peek_stdout():
|
||||
stdout.extend(resp.read_stdout().rstrip("\n").split("\n"))
|
||||
if resp.peek_stderr():
|
||||
stderr.extend(resp.read_stderr().rstrip("\n").split("\n"))
|
||||
error = resp.read_channel(ERROR_CHANNEL)
|
||||
resp.close()
|
||||
error = yaml.safe_load(error)
|
||||
return error, stdout, stderr
|
||||
except Exception as e:
|
||||
self.module.fail_json(
|
||||
msg="Error while running/parsing from pod {1}/{2} command='{0}' : {3}".format(
|
||||
self.namespace, self.name, cmd, to_native(e)
|
||||
)
|
||||
)
|
||||
|
||||
def is_directory_path_from_pod(self, file_path, failed_if_not_exists=True):
|
||||
# check if file exists
|
||||
error, out, err = self._run_from_pod(cmd=["test", "-e", file_path])
|
||||
if error.get("status") != "Success":
|
||||
if failed_if_not_exists:
|
||||
return None, "%s does not exist in remote pod filesystem" % file_path
|
||||
return False, None
|
||||
error, out, err = self._run_from_pod(cmd=["test", "-d", file_path])
|
||||
return error.get("status") == "Success", None
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
|
||||
class K8SCopyFromPod(K8SCopy):
|
||||
"""
|
||||
Copy files/directory from Pod into local filesystem
|
||||
"""
|
||||
|
||||
def __init__(self, module, client):
|
||||
super(K8SCopyFromPod, self).__init__(module, client)
|
||||
self.is_remote_path_dir = None
|
||||
self.files_to_copy = []
|
||||
self._shellname = None
|
||||
|
||||
@property
|
||||
def pod_shell(self):
|
||||
if self._shellname is None:
|
||||
for s in ("/bin/sh", "/bin/bash"):
|
||||
error, out, err = self._run_from_pod(s)
|
||||
if error.get("status") == "Success":
|
||||
self._shellname = s
|
||||
break
|
||||
return self._shellname
|
||||
|
||||
def listfiles_with_find(self, path):
|
||||
find_cmd = ["find", path, "-type", "f"]
|
||||
error, files, err = self._run_from_pod(cmd=find_cmd)
|
||||
if error.get("status") != "Success":
|
||||
self.module.fail_json(msg=error.get("message"))
|
||||
return files
|
||||
|
||||
def listfile_with_echo(self, path):
|
||||
echo_cmd = [
|
||||
self.pod_shell,
|
||||
"-c",
|
||||
"echo {path}/* {path}/.*".format(
|
||||
path=path.translate(str.maketrans({" ": r"\ "}))
|
||||
),
|
||||
]
|
||||
error, out, err = self._run_from_pod(cmd=echo_cmd)
|
||||
if error.get("status") != "Success":
|
||||
self.module.fail_json(msg=error.get("message"))
|
||||
|
||||
files = []
|
||||
if out:
|
||||
output = out[0] + " "
|
||||
files = [
|
||||
os.path.join(path, p[:-1])
|
||||
for p in output.split(f"{path}/")
|
||||
if p and p[:-1] not in (".", "..")
|
||||
]
|
||||
|
||||
result = []
|
||||
for f in files:
|
||||
is_dir, err = self.is_directory_path_from_pod(f)
|
||||
if err:
|
||||
continue
|
||||
if not is_dir:
|
||||
result.append(f)
|
||||
continue
|
||||
result += self.listfile_with_echo(f)
|
||||
return result
|
||||
|
||||
def list_remote_files(self):
|
||||
"""
|
||||
This method will check if the remote path is a dir or file
|
||||
if it is a directory the file list will be updated accordingly
|
||||
"""
|
||||
# check is remote path exists and is a file or directory
|
||||
is_dir, error = self.is_directory_path_from_pod(self.remote_path)
|
||||
if error:
|
||||
self.module.fail_json(msg=error)
|
||||
|
||||
if not is_dir:
|
||||
return [self.remote_path]
|
||||
else:
|
||||
# find executable to list dir with
|
||||
executables = dict(
|
||||
find=self.listfiles_with_find,
|
||||
echo=self.listfile_with_echo,
|
||||
)
|
||||
for item in executables:
|
||||
error, out, err = self._run_from_pod(item)
|
||||
if error.get("status") == "Success":
|
||||
return executables.get(item)(self.remote_path)
|
||||
|
||||
def read(self):
|
||||
self.stdout = None
|
||||
self.stderr = None
|
||||
|
||||
if self.response.is_open():
|
||||
if not self.response.sock.connected:
|
||||
self.response._connected = False
|
||||
else:
|
||||
ret, out, err = select((self.response.sock.sock,), (), (), 0)
|
||||
if ret:
|
||||
code, frame = self.response.sock.recv_data_frame(True)
|
||||
if code == ABNF.OPCODE_CLOSE:
|
||||
self.response._connected = False
|
||||
elif (
|
||||
code in (ABNF.OPCODE_BINARY, ABNF.OPCODE_TEXT)
|
||||
and len(frame.data) > 1
|
||||
):
|
||||
channel = frame.data[0]
|
||||
content = frame.data[1:]
|
||||
if content:
|
||||
if channel == STDOUT_CHANNEL:
|
||||
self.stdout = content
|
||||
elif channel == STDERR_CHANNEL:
|
||||
self.stderr = content.decode("utf-8", "replace")
|
||||
|
||||
def copy(self):
|
||||
is_remote_path_dir = (
|
||||
len(self.files_to_copy) > 1 or self.files_to_copy[0] != self.remote_path
|
||||
)
|
||||
relpath_start = self.remote_path
|
||||
if is_remote_path_dir and os.path.isdir(self.local_path):
|
||||
relpath_start = os.path.dirname(self.remote_path)
|
||||
|
||||
if not self.check_mode:
|
||||
for remote_file in self.files_to_copy:
|
||||
dest_file = self.local_path
|
||||
if is_remote_path_dir:
|
||||
dest_file = os.path.join(
|
||||
self.local_path,
|
||||
os.path.relpath(remote_file, start=relpath_start),
|
||||
)
|
||||
# create directory to copy file in
|
||||
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
|
||||
|
||||
pod_command = ["cat", remote_file]
|
||||
self.response = stream(
|
||||
self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=pod_command,
|
||||
stderr=True,
|
||||
stdin=True,
|
||||
stdout=True,
|
||||
tty=False,
|
||||
_preload_content=False,
|
||||
**self.container_arg,
|
||||
)
|
||||
errors = []
|
||||
with open(dest_file, "wb") as fh:
|
||||
while self.response._connected:
|
||||
self.read()
|
||||
if self.stdout:
|
||||
fh.write(self.stdout)
|
||||
if self.stderr:
|
||||
errors.append(self.stderr)
|
||||
if errors:
|
||||
self.module.fail_json(
|
||||
msg="Failed to copy file from Pod: {0}".format("".join(errors))
|
||||
)
|
||||
self.module.exit_json(
|
||||
changed=True,
|
||||
result="{0} successfully copied locally into {1}".format(
|
||||
self.remote_path, self.local_path
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.files_to_copy = self.list_remote_files()
|
||||
if self.files_to_copy == []:
|
||||
self.module.exit_json(
|
||||
changed=False,
|
||||
warning="No file found from directory '{0}' into remote Pod.".format(
|
||||
self.remote_path
|
||||
),
|
||||
)
|
||||
self.copy()
|
||||
|
||||
|
||||
class K8SCopyToPod(K8SCopy):
|
||||
"""
|
||||
Copy files/directory from local filesystem into remote Pod
|
||||
"""
|
||||
|
||||
def __init__(self, module, client):
|
||||
super(K8SCopyToPod, self).__init__(module, client)
|
||||
self.files_to_copy = list()
|
||||
|
||||
def close_temp_file(self):
|
||||
if self.named_temp_file:
|
||||
self.named_temp_file.close()
|
||||
|
||||
def run(self):
|
||||
# remove trailing slash from destination path
|
||||
dest_file = self.remote_path.rstrip("/")
|
||||
src_file = self.local_path
|
||||
self.named_temp_file = None
|
||||
if self.content:
|
||||
self.named_temp_file = NamedTemporaryFile(mode="w")
|
||||
self.named_temp_file.write(self.content)
|
||||
self.named_temp_file.flush()
|
||||
src_file = self.named_temp_file.name
|
||||
else:
|
||||
if not os.path.exists(self.local_path):
|
||||
self.module.fail_json(
|
||||
msg="{0} does not exist in local filesystem".format(self.local_path)
|
||||
)
|
||||
if not os.access(self.local_path, os.R_OK):
|
||||
self.module.fail_json(msg="{0} not readable".format(self.local_path))
|
||||
|
||||
is_dir, err = self.is_directory_path_from_pod(
|
||||
self.remote_path, failed_if_not_exists=False
|
||||
)
|
||||
if err:
|
||||
self.module.fail_json(msg=err)
|
||||
if is_dir:
|
||||
if self.content:
|
||||
self.module.fail_json(
|
||||
msg="When content is specified, remote path should not be an existing directory"
|
||||
)
|
||||
else:
|
||||
dest_file = os.path.join(dest_file, os.path.basename(src_file))
|
||||
|
||||
if not self.check_mode:
|
||||
if self.no_preserve:
|
||||
tar_command = [
|
||||
"tar",
|
||||
"--no-same-permissions",
|
||||
"--no-same-owner",
|
||||
"-xmf",
|
||||
"-",
|
||||
]
|
||||
else:
|
||||
tar_command = ["tar", "-xmf", "-"]
|
||||
|
||||
if dest_file.startswith("/"):
|
||||
tar_command.extend(["-C", "/"])
|
||||
|
||||
response = stream(
|
||||
self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=tar_command,
|
||||
stderr=True,
|
||||
stdin=True,
|
||||
stdout=True,
|
||||
tty=False,
|
||||
_preload_content=False,
|
||||
**self.container_arg,
|
||||
)
|
||||
with TemporaryFile() as tar_buffer:
|
||||
with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
|
||||
tar.add(src_file, dest_file)
|
||||
tar_buffer.seek(0)
|
||||
commands = []
|
||||
# push command in chunk mode
|
||||
size = 1024 * 1024
|
||||
while True:
|
||||
data = tar_buffer.read(size)
|
||||
if not data:
|
||||
break
|
||||
commands.append(data)
|
||||
|
||||
stderr, stdout = [], []
|
||||
while response.is_open():
|
||||
if response.peek_stdout():
|
||||
stdout.append(response.read_stdout().rstrip("\n"))
|
||||
if response.peek_stderr():
|
||||
stderr.append(response.read_stderr().rstrip("\n"))
|
||||
if commands:
|
||||
cmd = commands.pop(0)
|
||||
response.write_stdin(cmd)
|
||||
else:
|
||||
break
|
||||
response.close()
|
||||
if stderr:
|
||||
self.close_temp_file()
|
||||
self.module.fail_json(
|
||||
command=tar_command,
|
||||
msg="Failed to copy local file/directory into Pod due to: {0}".format(
|
||||
"".join(stderr)
|
||||
),
|
||||
)
|
||||
self.close_temp_file()
|
||||
if self.content:
|
||||
self.module.exit_json(
|
||||
changed=True,
|
||||
result="Content successfully copied into {0} on remote Pod".format(
|
||||
self.remote_path
|
||||
),
|
||||
)
|
||||
self.module.exit_json(
|
||||
changed=True,
|
||||
result="{0} successfully copied into remote Pod into {1}".format(
|
||||
self.local_path, self.remote_path
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def check_pod(svc):
|
||||
module = svc.module
|
||||
namespace = module.params.get("namespace")
|
||||
name = module.params.get("pod")
|
||||
container = module.params.get("container")
|
||||
|
||||
try:
|
||||
resource = svc.find_resource("Pod", None, True)
|
||||
except CoreException as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
|
||||
def _fail(exc):
|
||||
arg = {}
|
||||
if hasattr(exc, "body"):
|
||||
msg = (
|
||||
"Namespace={0} Kind=Pod Name={1}: Failed requested object: {2}".format(
|
||||
namespace, name, exc.body
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = to_native(exc)
|
||||
for attr in ["status", "reason"]:
|
||||
if hasattr(exc, attr):
|
||||
arg[attr] = getattr(exc, attr)
|
||||
module.fail_json(msg=msg, **arg)
|
||||
|
||||
try:
|
||||
result = svc.client.get(resource, name=name, namespace=namespace)
|
||||
containers = [
|
||||
c["name"] for c in result.to_dict()["status"]["containerStatuses"]
|
||||
]
|
||||
if container and container not in containers:
|
||||
module.fail_json(msg="Pod has no container {0}".format(container))
|
||||
return containers
|
||||
except Exception as exc:
|
||||
_fail(exc)
|
||||
@@ -14,8 +14,9 @@
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ApplyException(Exception):
|
||||
""" Could not apply patch """
|
||||
"""Could not apply patch"""
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
# Implement ConfigMapHash and SecretHash equivalents
|
||||
# Based on https://github.com/kubernetes/kubernetes/pull/49961
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
@@ -23,6 +24,7 @@ import hashlib
|
||||
|
||||
try:
|
||||
import string
|
||||
|
||||
maketrans = string.maketrans
|
||||
except AttributeError:
|
||||
maketrans = str.maketrans
|
||||
@@ -44,14 +46,21 @@ def sorted_dict(unsorted_dict):
|
||||
|
||||
def generate_hash(resource):
|
||||
# Get name from metadata
|
||||
resource['name'] = resource.get('metadata', {}).get('name', '')
|
||||
if resource['kind'] == 'ConfigMap':
|
||||
marshalled = marshal(sorted_dict(resource), ['data', 'kind', 'name'])
|
||||
del(resource['name'])
|
||||
metada = resource.get("metadata", {})
|
||||
key = "name"
|
||||
resource["name"] = metada.get("name", "")
|
||||
generate_name = metada.get("generateName", "")
|
||||
if resource["name"] == "" and generate_name:
|
||||
del resource["name"]
|
||||
key = "generateName"
|
||||
resource["generateName"] = generate_name
|
||||
if resource["kind"] == "ConfigMap":
|
||||
marshalled = marshal(sorted_dict(resource), ["data", "kind", key])
|
||||
del resource[key]
|
||||
return encode(marshalled)
|
||||
if resource['kind'] == 'Secret':
|
||||
marshalled = marshal(sorted_dict(resource), ['data', 'kind', 'name', 'type'])
|
||||
del(resource['name'])
|
||||
if resource["kind"] == "Secret":
|
||||
marshalled = marshal(sorted_dict(resource), ["data", "kind", key, "type"])
|
||||
del resource[key]
|
||||
return encode(marshalled)
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -60,8 +69,10 @@ def marshal(data, keys):
|
||||
ordered = OrderedDict()
|
||||
for key in keys:
|
||||
ordered[key] = data.get(key, "")
|
||||
return json.dumps(ordered, separators=(',', ':')).encode('utf-8')
|
||||
return json.dumps(ordered, separators=(",", ":")).encode("utf-8")
|
||||
|
||||
|
||||
def encode(resource):
|
||||
return hashlib.sha256(resource).hexdigest()[:10].translate(maketrans("013ae", "ghkmt"))
|
||||
return (
|
||||
hashlib.sha256(resource).hexdigest()[:10].translate(maketrans("013ae", "ghkmt"))
|
||||
)
|
||||
|
||||
@@ -7,138 +7,31 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
import re
|
||||
import json
|
||||
import copy
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
||||
HAS_YAML = True
|
||||
YAML_IMP_ERR = None
|
||||
except ImportError:
|
||||
YAML_IMP_ERR = traceback.format_exc()
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def prepare_helm_environ_update(module):
|
||||
environ_update = {}
|
||||
file_to_cleam_up = None
|
||||
kubeconfig_path = module.params.get('kubeconfig')
|
||||
if module.params.get('context') is not None:
|
||||
environ_update["HELM_KUBECONTEXT"] = module.params.get('context')
|
||||
if module.params.get('release_namespace'):
|
||||
environ_update["HELM_NAMESPACE"] = module.params.get('release_namespace')
|
||||
if module.params.get("api_key"):
|
||||
environ_update["HELM_KUBETOKEN"] = module.params["api_key"]
|
||||
if module.params.get("host"):
|
||||
environ_update["HELM_KUBEAPISERVER"] = module.params["host"]
|
||||
if module.params.get("validate_certs") is False or module.params.get("ca_cert"):
|
||||
kubeconfig_path = write_temp_kubeconfig(
|
||||
module.params["host"],
|
||||
validate_certs=module.params["validate_certs"],
|
||||
ca_cert=module.params["ca_cert"])
|
||||
file_to_cleam_up = kubeconfig_path
|
||||
if kubeconfig_path is not None:
|
||||
environ_update["KUBECONFIG"] = kubeconfig_path
|
||||
|
||||
try:
|
||||
yield environ_update
|
||||
finally:
|
||||
if file_to_cleam_up:
|
||||
os.remove(file_to_cleam_up)
|
||||
|
||||
|
||||
def run_helm(module, command, fails_on_error=True):
|
||||
if not HAS_YAML:
|
||||
module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
||||
|
||||
with prepare_helm_environ_update(module) as environ_update:
|
||||
rc, out, err = module.run_command(command, environ_update=environ_update)
|
||||
if fails_on_error and rc != 0:
|
||||
module.fail_json(
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
command=command,
|
||||
)
|
||||
return rc, out, err
|
||||
|
||||
|
||||
def get_values(module, command, release_name):
|
||||
"""
|
||||
Get Values from deployed release
|
||||
"""
|
||||
if not HAS_YAML:
|
||||
module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
||||
|
||||
get_command = command + " get values --output=yaml " + release_name
|
||||
|
||||
rc, out, err = run_helm(module, get_command)
|
||||
# Helm 3 return "null" string when no values are set
|
||||
if out.rstrip("\n") == "null":
|
||||
return {}
|
||||
return yaml.safe_load(out)
|
||||
|
||||
|
||||
def write_temp_kubeconfig(server, validate_certs=True, ca_cert=None):
|
||||
# Workaround until https://github.com/helm/helm/pull/8622 is merged
|
||||
content = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Config",
|
||||
"clusters": [
|
||||
{
|
||||
"cluster": {
|
||||
"server": server,
|
||||
},
|
||||
"name": "generated-cluster"
|
||||
}
|
||||
],
|
||||
"contexts": [
|
||||
{
|
||||
"context": {
|
||||
"cluster": "generated-cluster"
|
||||
},
|
||||
"name": "generated-context"
|
||||
}
|
||||
],
|
||||
"current-context": "generated-context"
|
||||
}
|
||||
|
||||
if not validate_certs:
|
||||
content["clusters"][0]["cluster"]["insecure-skip-tls-verify"] = True
|
||||
if ca_cert:
|
||||
content["clusters"][0]["cluster"]["certificate-authority"] = ca_cert
|
||||
|
||||
_fd, file_name = tempfile.mkstemp()
|
||||
with os.fdopen(_fd, 'w') as fp:
|
||||
yaml.dump(content, fp)
|
||||
return file_name
|
||||
|
||||
|
||||
def get_helm_plugin_list(module, helm_bin=None):
|
||||
"""
|
||||
Return `helm plugin list`
|
||||
"""
|
||||
if not helm_bin:
|
||||
return []
|
||||
helm_plugin_list = helm_bin + " list"
|
||||
rc, out, err = run_helm(module, helm_plugin_list)
|
||||
if rc != 0 or (out == '' and err == ''):
|
||||
module.fail_json(
|
||||
msg="Failed to get Helm plugin info",
|
||||
command=helm_plugin_list,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
return (rc, out, err)
|
||||
|
||||
|
||||
def parse_helm_plugin_list(module, output=None):
|
||||
def parse_helm_plugin_list(output=None):
|
||||
"""
|
||||
Parse `helm plugin list`, return list of plugins
|
||||
"""
|
||||
@@ -149,12 +42,262 @@ def parse_helm_plugin_list(module, output=None):
|
||||
for line in output:
|
||||
if line.startswith("NAME"):
|
||||
continue
|
||||
name, version, description = line.split('\t', 3)
|
||||
name, version, description = line.split("\t", 3)
|
||||
name = name.strip()
|
||||
version = version.strip()
|
||||
description = description.strip()
|
||||
if name == '':
|
||||
if name == "":
|
||||
continue
|
||||
ret.append((name, version, description))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def write_temp_kubeconfig(server, validate_certs=True, ca_cert=None, kubeconfig=None):
|
||||
# Workaround until https://github.com/helm/helm/pull/8622 is merged
|
||||
content = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Config",
|
||||
"clusters": [{"cluster": {"server": server}, "name": "generated-cluster"}],
|
||||
"contexts": [
|
||||
{"context": {"cluster": "generated-cluster"}, "name": "generated-context"}
|
||||
],
|
||||
"current-context": "generated-context",
|
||||
}
|
||||
if kubeconfig:
|
||||
content = copy.deepcopy(kubeconfig)
|
||||
|
||||
for cluster in content["clusters"]:
|
||||
if server:
|
||||
cluster["cluster"]["server"] = server
|
||||
if not validate_certs:
|
||||
cluster["cluster"]["insecure-skip-tls-verify"] = True
|
||||
if ca_cert:
|
||||
cluster["cluster"]["certificate-authority"] = ca_cert
|
||||
return content
|
||||
|
||||
|
||||
class AnsibleHelmModule(object):
|
||||
|
||||
"""
|
||||
An Ansible module class for Kubernetes.core helm modules
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
self._module = None
|
||||
if "module" in kwargs:
|
||||
self._module = kwargs.get("module")
|
||||
else:
|
||||
self._module = AnsibleModule(**kwargs)
|
||||
|
||||
self.helm_env = None
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._module, name)
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
return self._module.params
|
||||
|
||||
def _prepare_helm_environment(self):
|
||||
param_to_env_mapping = [
|
||||
("context", "HELM_KUBECONTEXT"),
|
||||
("release_namespace", "HELM_NAMESPACE"),
|
||||
("api_key", "HELM_KUBETOKEN"),
|
||||
("host", "HELM_KUBEAPISERVER"),
|
||||
]
|
||||
|
||||
env_update = {}
|
||||
for p, env in param_to_env_mapping:
|
||||
if self.params.get(p):
|
||||
env_update[env] = self.params.get(p)
|
||||
|
||||
kubeconfig_content = None
|
||||
kubeconfig = self.params.get("kubeconfig")
|
||||
if kubeconfig:
|
||||
if isinstance(kubeconfig, string_types):
|
||||
with open(os.path.expanduser(kubeconfig)) as fd:
|
||||
kubeconfig_content = yaml.safe_load(fd)
|
||||
elif isinstance(kubeconfig, dict):
|
||||
kubeconfig_content = kubeconfig
|
||||
|
||||
if self.params.get("ca_cert"):
|
||||
ca_cert = self.params.get("ca_cert")
|
||||
if LooseVersion(self.get_helm_version()) < LooseVersion("3.5.0"):
|
||||
# update certs from kubeconfig
|
||||
kubeconfig_content = write_temp_kubeconfig(
|
||||
server=self.params.get("host"),
|
||||
ca_cert=ca_cert,
|
||||
kubeconfig=kubeconfig_content,
|
||||
)
|
||||
else:
|
||||
env_update["HELM_KUBECAFILE"] = ca_cert
|
||||
|
||||
if self.params.get("validate_certs") is False:
|
||||
validate_certs = self.params.get("validate_certs")
|
||||
if LooseVersion(self.get_helm_version()) < LooseVersion("3.10.0"):
|
||||
# update certs from kubeconfig
|
||||
kubeconfig_content = write_temp_kubeconfig(
|
||||
server=self.params.get("host"),
|
||||
validate_certs=validate_certs,
|
||||
kubeconfig=kubeconfig_content,
|
||||
)
|
||||
else:
|
||||
env_update["HELM_KUBEINSECURE_SKIP_TLS_VERIFY"] = "true"
|
||||
|
||||
if kubeconfig_content:
|
||||
fd, kubeconfig_path = tempfile.mkstemp()
|
||||
with os.fdopen(fd, "w") as fp:
|
||||
json.dump(kubeconfig_content, fp)
|
||||
|
||||
env_update["KUBECONFIG"] = kubeconfig_path
|
||||
self.add_cleanup_file(kubeconfig_path)
|
||||
|
||||
return env_update
|
||||
|
||||
@property
|
||||
def env_update(self):
|
||||
if self.helm_env is None:
|
||||
self.helm_env = self._prepare_helm_environment()
|
||||
return self.helm_env
|
||||
|
||||
def run_helm_command(self, command, fails_on_error=True):
|
||||
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)
|
||||
if fails_on_error and rc != 0:
|
||||
self.fail_json(
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
|
||||
rc, out, err
|
||||
),
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
command=command,
|
||||
)
|
||||
return rc, out, err
|
||||
|
||||
def get_helm_binary(self):
|
||||
return self.params.get("binary_path") or self.get_bin_path(
|
||||
"helm", required=True
|
||||
)
|
||||
|
||||
def get_helm_version(self):
|
||||
|
||||
command = self.get_helm_binary() + " version"
|
||||
rc, out, err = self.run_command(command)
|
||||
m = re.match(r'version.BuildInfo{Version:"v([0-9\.]*)",', out)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.match(r'Client: &version.Version{SemVer:"v([0-9\.]*)", ', out)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
def get_values(self, release_name, get_all=False):
|
||||
"""
|
||||
Get Values from deployed release
|
||||
"""
|
||||
if not HAS_YAML:
|
||||
self.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
||||
|
||||
get_command = (
|
||||
self.get_helm_binary() + " get values --output=yaml " + release_name
|
||||
)
|
||||
|
||||
if get_all:
|
||||
get_command += " -a"
|
||||
|
||||
rc, out, err = self.run_helm_command(get_command)
|
||||
# Helm 3 return "null" string when no values are set
|
||||
if out.rstrip("\n") == "null":
|
||||
return {}
|
||||
return yaml.safe_load(out)
|
||||
|
||||
def parse_yaml_content(self, content):
|
||||
|
||||
if not HAS_YAML:
|
||||
self.fail_json(msg=missing_required_lib("yaml"), exception=HAS_YAML)
|
||||
|
||||
try:
|
||||
return list(yaml.safe_load_all(content))
|
||||
except (IOError, yaml.YAMLError) as exc:
|
||||
self.fail_json(
|
||||
msg="Error parsing YAML content: {0}".format(exc), raw_data=content
|
||||
)
|
||||
|
||||
def get_manifest(self, release_name):
|
||||
|
||||
command = [
|
||||
self.get_helm_binary(),
|
||||
"get",
|
||||
"manifest",
|
||||
release_name,
|
||||
]
|
||||
rc, out, err = self.run_helm_command(" ".join(command))
|
||||
if rc != 0:
|
||||
self.fail_json(msg=err)
|
||||
return self.parse_yaml_content(out)
|
||||
|
||||
def get_notes(self, release_name):
|
||||
|
||||
command = [
|
||||
self.get_helm_binary(),
|
||||
"get",
|
||||
"notes",
|
||||
release_name,
|
||||
]
|
||||
rc, out, err = self.run_helm_command(" ".join(command))
|
||||
if rc != 0:
|
||||
self.fail_json(msg=err)
|
||||
return out
|
||||
|
||||
def get_hooks(self, release_name):
|
||||
command = [
|
||||
self.get_helm_binary(),
|
||||
"get",
|
||||
"hooks",
|
||||
release_name,
|
||||
]
|
||||
rc, out, err = self.run_helm_command(" ".join(command))
|
||||
if rc != 0:
|
||||
self.fail_json(msg=err)
|
||||
return self.parse_yaml_content(out)
|
||||
|
||||
def get_helm_plugin_list(self):
|
||||
"""
|
||||
Return `helm plugin list`
|
||||
"""
|
||||
helm_plugin_list = self.get_helm_binary() + " plugin list"
|
||||
rc, out, err = self.run_helm_command(helm_plugin_list)
|
||||
if rc != 0 or (out == "" and err == ""):
|
||||
self.fail_json(
|
||||
msg="Failed to get Helm plugin info",
|
||||
command=helm_plugin_list,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
return (rc, out, err, helm_plugin_list)
|
||||
|
||||
def get_helm_set_values_args(self, set_values):
|
||||
if any(v.get("value_type") == "json" for v in set_values):
|
||||
if LooseVersion(self.get_helm_version()) < LooseVersion("3.10.0"):
|
||||
self.fail_json(
|
||||
msg="This module requires helm >= 3.10.0, to use set_values parameter with value type set to 'json'. current version is {0}".format(
|
||||
self.get_helm_version()
|
||||
)
|
||||
)
|
||||
|
||||
options = []
|
||||
for opt in set_values:
|
||||
value_type = opt.get("value_type", "raw")
|
||||
value = opt.get("value")
|
||||
|
||||
if value_type == "raw":
|
||||
options.append("--set " + value)
|
||||
else:
|
||||
options.append("--set-{0} '{1}'".format(value_type, value))
|
||||
|
||||
return " ".join(options)
|
||||
|
||||
42
plugins/module_utils/helm_args_common.py
Normal file
42
plugins/module_utils/helm_args_common.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
HELM_AUTH_ARG_SPEC = dict(
|
||||
binary_path=dict(type="path"),
|
||||
context=dict(
|
||||
type="str",
|
||||
aliases=["kube_context"],
|
||||
fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]),
|
||||
),
|
||||
kubeconfig=dict(
|
||||
type="raw",
|
||||
aliases=["kubeconfig_path"],
|
||||
fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]),
|
||||
),
|
||||
host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])),
|
||||
ca_cert=dict(
|
||||
type="path",
|
||||
aliases=["ssl_ca_cert"],
|
||||
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]),
|
||||
),
|
||||
validate_certs=dict(
|
||||
type="bool",
|
||||
default=True,
|
||||
aliases=["verify_ssl"],
|
||||
fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]),
|
||||
),
|
||||
api_key=dict(
|
||||
type="str",
|
||||
no_log=True,
|
||||
fallback=(env_fallback, ["K8S_AUTH_API_KEY"]),
|
||||
),
|
||||
)
|
||||
|
||||
HELM_AUTH_MUTUALLY_EXCLUSIVE = [
|
||||
("context", "ca_cert"),
|
||||
("context", "validate_certs"),
|
||||
]
|
||||
368
plugins/module_utils/k8s/client.py
Normal file
368
plugins/module_utils/k8s/client.py
Normal file
@@ -0,0 +1,368 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ansible.module_utils.six import iteritems, string_types
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_MAP,
|
||||
AUTH_ARG_SPEC,
|
||||
AUTH_PROXY_HEADERS_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
requires as _requires,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils import (
|
||||
k8sdynamicclient,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.client.discovery import (
|
||||
LazyDiscoverer,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
try:
|
||||
import kubernetes
|
||||
from kubernetes.dynamic.exceptions import (
|
||||
ResourceNotFoundError,
|
||||
ResourceNotUniqueError,
|
||||
)
|
||||
from kubernetes.dynamic.resource import Resource
|
||||
except ImportError:
|
||||
# kubernetes import error is handled in module setup
|
||||
# This is defined only for the sake of Ansible's checked import requirement
|
||||
Resource = Any # type: ignore
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings()
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
|
||||
_pool = {}
|
||||
|
||||
|
||||
class unique_string(str):
|
||||
_low = None
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self is other
|
||||
|
||||
def lower(self):
|
||||
if self._low is None:
|
||||
lower = str.lower(self)
|
||||
if str.__eq__(lower, self):
|
||||
self._low = self
|
||||
else:
|
||||
self._low = unique_string(lower)
|
||||
return self._low
|
||||
|
||||
|
||||
def _create_auth_spec(module=None, **kwargs) -> Dict:
|
||||
auth: Dict = {}
|
||||
# If authorization variables aren't defined, look for them in environment variables
|
||||
for true_name, arg_name in AUTH_ARG_MAP.items():
|
||||
if module and module.params.get(arg_name) is not None:
|
||||
auth[true_name] = module.params.get(arg_name)
|
||||
elif arg_name in kwargs and kwargs.get(arg_name) is not None:
|
||||
auth[true_name] = kwargs.get(arg_name)
|
||||
elif true_name in kwargs and kwargs.get(true_name) is not None:
|
||||
# Aliases in kwargs
|
||||
auth[true_name] = kwargs.get(true_name)
|
||||
elif arg_name == "proxy_headers":
|
||||
# specific case for 'proxy_headers' which is a dictionary
|
||||
proxy_headers = {}
|
||||
for key in AUTH_PROXY_HEADERS_SPEC.keys():
|
||||
env_value = os.getenv(
|
||||
"K8S_AUTH_PROXY_HEADERS_{0}".format(key.upper()), None
|
||||
)
|
||||
if env_value is not None:
|
||||
if AUTH_PROXY_HEADERS_SPEC[key].get("type") == "bool":
|
||||
env_value = env_value.lower() not in ["0", "false", "no"]
|
||||
proxy_headers[key] = env_value
|
||||
if proxy_headers is not {}:
|
||||
auth[true_name] = proxy_headers
|
||||
else:
|
||||
env_value = os.getenv(
|
||||
"K8S_AUTH_{0}".format(arg_name.upper()), None
|
||||
) or os.getenv("K8S_AUTH_{0}".format(true_name.upper()), None)
|
||||
if env_value is not None:
|
||||
if AUTH_ARG_SPEC[arg_name].get("type") == "bool":
|
||||
env_value = env_value.lower() not in ["0", "false", "no"]
|
||||
auth[true_name] = env_value
|
||||
|
||||
return auth
|
||||
|
||||
|
||||
def _load_config(auth: Dict) -> None:
|
||||
kubeconfig = auth.get("kubeconfig")
|
||||
optional_arg = {
|
||||
"context": auth.get("context"),
|
||||
"persist_config": auth.get("persist_config"),
|
||||
}
|
||||
if kubeconfig:
|
||||
if isinstance(kubeconfig, string_types):
|
||||
kubernetes.config.load_kube_config(config_file=kubeconfig, **optional_arg)
|
||||
elif isinstance(kubeconfig, dict):
|
||||
kubernetes.config.load_kube_config_from_dict(
|
||||
config_dict=kubeconfig, **optional_arg
|
||||
)
|
||||
else:
|
||||
kubernetes.config.load_kube_config(config_file=None, **optional_arg)
|
||||
|
||||
|
||||
def _create_configuration(auth: Dict):
|
||||
def auth_set(*names: list) -> bool:
|
||||
return all(auth.get(name) for name in names)
|
||||
|
||||
if auth_set("host"):
|
||||
# Removing trailing slashes if any from hostname
|
||||
auth["host"] = auth.get("host").rstrip("/")
|
||||
|
||||
if (
|
||||
auth_set("username", "password", "host")
|
||||
or auth_set("api_key", "host")
|
||||
or auth_set("cert_file", "key_file", "host")
|
||||
):
|
||||
# We have enough in the parameters to authenticate, no need to load incluster or kubeconfig
|
||||
pass
|
||||
elif auth_set("kubeconfig") or auth_set("context"):
|
||||
try:
|
||||
_load_config(auth)
|
||||
except Exception as err:
|
||||
raise err
|
||||
|
||||
else:
|
||||
# First try to do incluster config, then kubeconfig
|
||||
try:
|
||||
kubernetes.config.load_incluster_config()
|
||||
except kubernetes.config.ConfigException:
|
||||
try:
|
||||
_load_config(auth)
|
||||
except Exception as err:
|
||||
raise err
|
||||
|
||||
# Override any values in the default configuration with Ansible parameters
|
||||
# As of kubernetes-client v12.0.0, get_default_copy() is required here
|
||||
try:
|
||||
configuration = kubernetes.client.Configuration().get_default_copy()
|
||||
except AttributeError:
|
||||
configuration = kubernetes.client.Configuration()
|
||||
|
||||
for key, value in iteritems(auth):
|
||||
if key in AUTH_ARG_MAP.keys() and value is not None:
|
||||
if key == "api_key":
|
||||
setattr(
|
||||
configuration, key, {"authorization": "Bearer {0}".format(value)}
|
||||
)
|
||||
elif key == "proxy_headers":
|
||||
headers = urllib3.util.make_headers(**value)
|
||||
setattr(configuration, key, headers)
|
||||
else:
|
||||
setattr(configuration, key, value)
|
||||
|
||||
return configuration
|
||||
|
||||
|
||||
def _create_headers(module=None, **kwargs):
|
||||
header_map = {
|
||||
"impersonate_user": "Impersonate-User",
|
||||
"impersonate_groups": "Impersonate-Group",
|
||||
}
|
||||
|
||||
headers = {}
|
||||
for arg_name, header_name in header_map.items():
|
||||
value = None
|
||||
if module and module.params.get(arg_name) is not None:
|
||||
value = module.params.get(arg_name)
|
||||
elif arg_name in kwargs and kwargs.get(arg_name) is not None:
|
||||
value = kwargs.get(arg_name)
|
||||
else:
|
||||
value = os.getenv("K8S_AUTH_{0}".format(arg_name.upper()), None)
|
||||
if value is not None:
|
||||
if AUTH_ARG_SPEC[arg_name].get("type") == "list":
|
||||
value = [x for x in value.split(",") if x != ""]
|
||||
if value:
|
||||
headers[header_name] = value
|
||||
return headers
|
||||
|
||||
|
||||
def _configuration_digest(configuration, **kwargs) -> str:
|
||||
m = hashlib.sha256()
|
||||
for k in AUTH_ARG_MAP:
|
||||
if not hasattr(configuration, k):
|
||||
v = None
|
||||
else:
|
||||
v = getattr(configuration, k)
|
||||
if v and k in ["ssl_ca_cert", "cert_file", "key_file"]:
|
||||
with open(str(v), "r") as fd:
|
||||
content = fd.read()
|
||||
m.update(content.encode())
|
||||
else:
|
||||
m.update(str(v).encode())
|
||||
for k, v in kwargs.items():
|
||||
content = "{0}: {1}".format(k, v)
|
||||
m.update(content.encode())
|
||||
digest = m.hexdigest()
|
||||
|
||||
return digest
|
||||
|
||||
|
||||
def _set_header(client, header, value):
|
||||
if isinstance(value, list):
|
||||
for v in value:
|
||||
client.set_default_header(header_name=unique_string(header), header_value=v)
|
||||
else:
|
||||
client.set_default_header(header_name=header, header_value=value)
|
||||
|
||||
|
||||
def cache(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
client = None
|
||||
hashable_kwargs = {}
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, list):
|
||||
hashable_kwargs[k] = ",".join(sorted(v))
|
||||
else:
|
||||
hashable_kwargs[k] = v
|
||||
digest = _configuration_digest(*args, **hashable_kwargs)
|
||||
if digest in _pool:
|
||||
client = _pool[digest]
|
||||
else:
|
||||
client = func(*args, **kwargs)
|
||||
_pool[digest] = client
|
||||
|
||||
return client
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@cache
|
||||
def create_api_client(configuration, **headers):
|
||||
client = kubernetes.client.ApiClient(configuration)
|
||||
for header, value in headers.items():
|
||||
_set_header(client, header, value)
|
||||
return k8sdynamicclient.K8SDynamicClient(client, discoverer=LazyDiscoverer)
|
||||
|
||||
|
||||
class K8SClient:
|
||||
"""A Client class for K8S modules.
|
||||
|
||||
This class has the primary purpose to proxy the kubernetes client and resource objects.
|
||||
If there is a need for other methods or attributes to be proxied, they can be added here.
|
||||
"""
|
||||
|
||||
K8S_SERVER_DRY_RUN = "All"
|
||||
|
||||
def __init__(self, configuration, client, dry_run: bool = False) -> None:
|
||||
self.configuration = configuration
|
||||
self.client = client
|
||||
self.dry_run = dry_run
|
||||
|
||||
@property
|
||||
def resources(self) -> List[Any]:
|
||||
return self.client.resources
|
||||
|
||||
def _find_resource_with_prefix(
|
||||
self, prefix: str, kind: str, api_version: str
|
||||
) -> Resource:
|
||||
for attribute in ["kind", "name", "singular_name"]:
|
||||
try:
|
||||
return self.client.resources.get(
|
||||
**{"prefix": prefix, "api_version": api_version, attribute: kind}
|
||||
)
|
||||
except (ResourceNotFoundError, ResourceNotUniqueError):
|
||||
pass
|
||||
return self.client.resources.get(
|
||||
prefix=prefix, api_version=api_version, short_names=[kind]
|
||||
)
|
||||
|
||||
def resource(self, kind: str, api_version: str) -> Resource:
|
||||
"""Fetch a kubernetes client resource.
|
||||
|
||||
This will attempt to find a kubernetes resource trying, in order, kind,
|
||||
name, singular_name and short_names.
|
||||
"""
|
||||
try:
|
||||
if api_version == "v1":
|
||||
return self._find_resource_with_prefix("api", kind, api_version)
|
||||
except ResourceNotFoundError:
|
||||
pass
|
||||
return self._find_resource_with_prefix(None, kind, api_version)
|
||||
|
||||
def _ensure_dry_run(self, params: Dict) -> Dict:
|
||||
if self.dry_run:
|
||||
params["dry_run"] = self.K8S_SERVER_DRY_RUN
|
||||
return params
|
||||
|
||||
def validate(
|
||||
self, resource, version: Optional[str] = None, strict: Optional[bool] = False
|
||||
):
|
||||
return self.client.validate(resource, version, strict)
|
||||
|
||||
def get(self, resource, **params):
|
||||
return resource.get(**params)
|
||||
|
||||
def delete(self, resource, **params):
|
||||
return resource.delete(**self._ensure_dry_run(params))
|
||||
|
||||
def apply(self, resource, definition, namespace, **params):
|
||||
return resource.apply(
|
||||
definition, namespace=namespace, **self._ensure_dry_run(params)
|
||||
)
|
||||
|
||||
def create(self, resource, definition, **params):
|
||||
return resource.create(definition, **self._ensure_dry_run(params))
|
||||
|
||||
def replace(self, resource, definition, **params):
|
||||
return resource.replace(definition, **self._ensure_dry_run(params))
|
||||
|
||||
def patch(self, resource, definition, **params):
|
||||
return resource.patch(definition, **self._ensure_dry_run(params))
|
||||
|
||||
|
||||
def get_api_client(module=None, **kwargs: Optional[Any]) -> K8SClient:
|
||||
auth_spec = _create_auth_spec(module, **kwargs)
|
||||
if module:
|
||||
requires = module.requires
|
||||
else:
|
||||
requires = _requires
|
||||
if isinstance(auth_spec.get("kubeconfig"), dict):
|
||||
requires("kubernetes", "17.17.0", "to use in-memory config")
|
||||
if auth_spec.get("no_proxy"):
|
||||
requires("kubernetes", "19.15.0", "to use the no_proxy feature")
|
||||
|
||||
try:
|
||||
configuration = _create_configuration(auth_spec)
|
||||
headers = _create_headers(module, **kwargs)
|
||||
client = create_api_client(configuration, **headers)
|
||||
except kubernetes.config.ConfigException as e:
|
||||
msg = "Could not create API client: {0}".format(e)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
dry_run = False
|
||||
if module and module.server_side_dry_run:
|
||||
dry_run = True
|
||||
|
||||
k8s_client = K8SClient(
|
||||
configuration=configuration,
|
||||
client=client,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
return k8s_client
|
||||
175
plugins/module_utils/k8s/core.py
Normal file
175
plugins/module_utils/k8s/core.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import traceback
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
|
||||
class AnsibleK8SModule:
|
||||
"""A base module class for K8S modules.
|
||||
|
||||
This class should be used instead of directly using AnsibleModule. If there
|
||||
is a need for other methods or attributes to be proxied, they can be added
|
||||
here.
|
||||
"""
|
||||
|
||||
default_settings = {
|
||||
"check_k8s": True,
|
||||
"check_pyyaml": True,
|
||||
"module_class": AnsibleModule,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
local_settings = {}
|
||||
for key in AnsibleK8SModule.default_settings:
|
||||
try:
|
||||
local_settings[key] = kwargs.pop(key)
|
||||
except KeyError:
|
||||
local_settings[key] = AnsibleK8SModule.default_settings[key]
|
||||
self.settings = local_settings
|
||||
|
||||
self._module = self.settings["module_class"](**kwargs)
|
||||
|
||||
if self.settings["check_k8s"]:
|
||||
self.requires("kubernetes")
|
||||
self.has_at_least("kubernetes", "12.0.0", warn=True)
|
||||
|
||||
if self.settings["check_pyyaml"]:
|
||||
self.requires("pyyaml")
|
||||
|
||||
@property
|
||||
def check_mode(self):
|
||||
return self._module.check_mode
|
||||
|
||||
@property
|
||||
def server_side_dry_run(self):
|
||||
return self.check_mode and self.has_at_least("kubernetes", "18.20.0")
|
||||
|
||||
@property
|
||||
def _diff(self):
|
||||
return self._module._diff
|
||||
|
||||
@property
|
||||
def _name(self):
|
||||
return self._module._name
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
return self._module.params
|
||||
|
||||
def warn(self, *args, **kwargs):
|
||||
return self._module.warn(*args, **kwargs)
|
||||
|
||||
def deprecate(self, *args, **kwargs):
|
||||
return self._module.deprecate(*args, **kwargs)
|
||||
|
||||
def debug(self, *args, **kwargs):
|
||||
return self._module.debug(*args, **kwargs)
|
||||
|
||||
def exit_json(self, *args, **kwargs):
|
||||
return self._module.exit_json(*args, **kwargs)
|
||||
|
||||
def fail_json(self, *args, **kwargs):
|
||||
return self._module.fail_json(*args, **kwargs)
|
||||
|
||||
def fail_from_exception(self, exception):
|
||||
msg = to_text(exception)
|
||||
tb = "".join(
|
||||
traceback.format_exception(None, exception, exception.__traceback__)
|
||||
)
|
||||
return self.fail_json(msg=msg, exception=tb)
|
||||
|
||||
def has_at_least(
|
||||
self, dependency: str, minimum: Optional[str] = None, warn: bool = False
|
||||
) -> bool:
|
||||
supported = has_at_least(dependency, minimum)
|
||||
if not supported and warn:
|
||||
self.warn(
|
||||
"{0}<{1} is not supported or tested. Some features may not work.".format(
|
||||
dependency, minimum
|
||||
)
|
||||
)
|
||||
return supported
|
||||
|
||||
def requires(
|
||||
self,
|
||||
dependency: str,
|
||||
minimum: Optional[str] = None,
|
||||
reason: Optional[str] = None,
|
||||
) -> None:
|
||||
try:
|
||||
requires(dependency, minimum, reason=reason)
|
||||
except Exception as e:
|
||||
self.fail_json(msg=to_text(e))
|
||||
|
||||
|
||||
def gather_versions() -> dict:
|
||||
versions = {}
|
||||
try:
|
||||
import jsonpatch
|
||||
|
||||
versions["jsonpatch"] = jsonpatch.__version__
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import kubernetes
|
||||
|
||||
versions["kubernetes"] = kubernetes.__version__
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import kubernetes_validate
|
||||
|
||||
versions["kubernetes-validate"] = kubernetes_validate.__version__
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
||||
versions["pyyaml"] = yaml.__version__
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
def has_at_least(dependency: str, minimum: Optional[str] = None) -> bool:
|
||||
"""Check if a specific dependency is present at a minimum version.
|
||||
|
||||
If a minimum version is not specified it will check only that the
|
||||
dependency is present.
|
||||
"""
|
||||
dependencies = gather_versions()
|
||||
current = dependencies.get(dependency)
|
||||
if current is not None:
|
||||
if minimum is None:
|
||||
return True
|
||||
supported = LooseVersion(current) >= LooseVersion(minimum)
|
||||
return supported
|
||||
return False
|
||||
|
||||
|
||||
def requires(
|
||||
dependency: str, minimum: Optional[str] = None, reason: Optional[str] = None
|
||||
) -> None:
|
||||
"""Fail if a specific dependency is not present at a minimum version.
|
||||
|
||||
If a minimum version is not specified it will require only that the
|
||||
dependency is present. This function raises an exception when the
|
||||
dependency is not found at the required version.
|
||||
"""
|
||||
if not has_at_least(dependency, minimum):
|
||||
if minimum is not None:
|
||||
lib = "{0}>={1}".format(dependency, minimum)
|
||||
else:
|
||||
lib = dependency
|
||||
raise Exception(missing_required_lib(lib, reason=reason))
|
||||
12
plugins/module_utils/k8s/exceptions.py
Normal file
12
plugins/module_utils/k8s/exceptions.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
|
||||
class CoreException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ResourceTimeout(CoreException):
|
||||
def __init__(self, message="", result=None):
|
||||
self.result = result or {}
|
||||
super().__init__(message)
|
||||
134
plugins/module_utils/k8s/resource.py
Normal file
134
plugins/module_utils/k8s/resource.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
import os
|
||||
from typing import cast, Dict, Iterable, List, Optional, Union
|
||||
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils.urls import Request
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
|
||||
class ResourceDefinition(dict):
|
||||
"""Representation of a resource definition.
|
||||
|
||||
This is a thin wrapper around a dictionary representation of a resource
|
||||
definition, with a few properties defined for conveniently accessing the
|
||||
commonly used fields.
|
||||
"""
|
||||
|
||||
@property
|
||||
def kind(self) -> Optional[str]:
|
||||
return self.get("kind")
|
||||
|
||||
@property
|
||||
def api_version(self) -> Optional[str]:
|
||||
return self.get("apiVersion")
|
||||
|
||||
@property
|
||||
def namespace(self) -> Optional[str]:
|
||||
metadata = self.get("metadata", {})
|
||||
return metadata.get("namespace")
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
metadata = self.get("metadata", {})
|
||||
return metadata.get("name")
|
||||
|
||||
|
||||
def create_definitions(params: Dict) -> List[ResourceDefinition]:
|
||||
"""Create a list of ResourceDefinitions from module inputs.
|
||||
|
||||
This will take the module's inputs and return a list of ResourceDefintion
|
||||
objects. The resource definitions returned by this function should be as
|
||||
complete a definition as we can create based on the input. Any *List kinds
|
||||
will be removed and replaced by the resources contained in it.
|
||||
"""
|
||||
if params.get("resource_definition"):
|
||||
d = cast(Union[str, List, Dict], params.get("resource_definition"))
|
||||
definitions = from_yaml(d)
|
||||
elif params.get("src"):
|
||||
d = cast(str, params.get("src"))
|
||||
if hasattr(d, "startswith") and d.startswith(("https://", "http://", "ftp://")):
|
||||
data = Request().open("GET", d).read().decode("utf8")
|
||||
definitions = from_yaml(data)
|
||||
else:
|
||||
definitions = from_file(d)
|
||||
else:
|
||||
# We'll create an empty definition and let merge_params set values
|
||||
# from the module parameters.
|
||||
definitions = [{}]
|
||||
|
||||
resource_definitions: List[Dict] = []
|
||||
for definition in definitions:
|
||||
merge_params(definition, params)
|
||||
kind = cast(Optional[str], definition.get("kind"))
|
||||
if kind and kind.endswith("List"):
|
||||
resource_definitions += flatten_list_kind(definition, params)
|
||||
else:
|
||||
resource_definitions.append(definition)
|
||||
return list(map(ResourceDefinition, resource_definitions))
|
||||
|
||||
|
||||
def from_yaml(definition: Union[str, List, Dict]) -> Iterable[Dict]:
|
||||
"""Load resource definitions from a yaml definition."""
|
||||
definitions: List[Dict] = []
|
||||
if isinstance(definition, string_types):
|
||||
definitions += yaml.safe_load_all(definition)
|
||||
elif isinstance(definition, list):
|
||||
for item in definition:
|
||||
if isinstance(item, string_types):
|
||||
definitions += yaml.safe_load_all(item)
|
||||
else:
|
||||
definitions.append(item)
|
||||
else:
|
||||
definition = cast(Dict, definition)
|
||||
definitions.append(definition)
|
||||
return filter(None, definitions)
|
||||
|
||||
|
||||
def from_file(filepath: str) -> Iterable[Dict]:
|
||||
"""Load resource definitions from a path to a yaml file."""
|
||||
path = os.path.normpath(filepath)
|
||||
with open(path, "rb") as f:
|
||||
definitions = list(yaml.safe_load_all(f))
|
||||
return filter(None, definitions)
|
||||
|
||||
|
||||
def merge_params(definition: Dict, params: Dict) -> Dict:
|
||||
"""Merge module parameters with the resource definition.
|
||||
|
||||
Fields in the resource definition take precedence over module parameters.
|
||||
"""
|
||||
definition.setdefault("kind", params.get("kind"))
|
||||
definition.setdefault("apiVersion", params.get("api_version"))
|
||||
metadata = definition.setdefault("metadata", {})
|
||||
# The following should only be set if we have values for them
|
||||
if params.get("namespace"):
|
||||
metadata.setdefault("namespace", params.get("namespace"))
|
||||
if params.get("name"):
|
||||
metadata.setdefault("name", params.get("name"))
|
||||
if params.get("generate_name"):
|
||||
metadata.setdefault("generateName", params.get("generate_name"))
|
||||
return definition
|
||||
|
||||
|
||||
def flatten_list_kind(definition: Dict, params: Dict) -> List[Dict]:
|
||||
"""Replace *List kind with the items it contains.
|
||||
|
||||
This will take a definition for a *List resource and return a list of
|
||||
definitions for the items contained within the List.
|
||||
"""
|
||||
items = []
|
||||
kind = cast(str, definition.get("kind"))[:-4]
|
||||
api_version = definition.get("apiVersion")
|
||||
for item in definition.get("items", []):
|
||||
item.setdefault("kind", kind)
|
||||
item.setdefault("apiVersion", api_version)
|
||||
items.append(merge_params(item, params))
|
||||
return items
|
||||
199
plugins/module_utils/k8s/runner.py
Normal file
199
plugins/module_utils/k8s/runner.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
||||
create_definitions,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
K8sService,
|
||||
diff_objects,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
ResourceTimeout,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import exists
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.selector import (
|
||||
LabelSelectorFilter,
|
||||
)
|
||||
|
||||
|
||||
def validate(client, module, resource):
|
||||
def _prepend_resource_info(resource, msg):
|
||||
return "%s %s: %s" % (resource["kind"], resource["metadata"]["name"], msg)
|
||||
|
||||
module.requires("kubernetes-validate")
|
||||
|
||||
warnings, errors = client.validate(
|
||||
resource,
|
||||
module.params["validate"].get("version"),
|
||||
module.params["validate"].get("strict"),
|
||||
)
|
||||
|
||||
if errors and module.params["validate"]["fail_on_error"]:
|
||||
module.fail_json(
|
||||
msg="\n".join([_prepend_resource_info(resource, error) for error in errors])
|
||||
)
|
||||
return [_prepend_resource_info(resource, msg) for msg in warnings + errors]
|
||||
|
||||
|
||||
def run_module(module) -> None:
|
||||
results = []
|
||||
changed = False
|
||||
client = get_api_client(module)
|
||||
svc = K8sService(client, module)
|
||||
try:
|
||||
definitions = create_definitions(module.params)
|
||||
except Exception as e:
|
||||
msg = "Failed to load resource definition: {0}".format(e)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
for definition in definitions:
|
||||
result = {"changed": False, "result": {}}
|
||||
warnings = []
|
||||
|
||||
if module.params.get("validate") is not None:
|
||||
warnings = validate(client, module, definition)
|
||||
|
||||
try:
|
||||
result = perform_action(svc, definition, module.params)
|
||||
except Exception as e:
|
||||
try:
|
||||
error = e.result
|
||||
except AttributeError:
|
||||
error = {}
|
||||
try:
|
||||
error["reason"] = e.__cause__.reason
|
||||
except AttributeError:
|
||||
pass
|
||||
error["msg"] = to_native(e)
|
||||
if warnings:
|
||||
error.setdefault("warnings", []).extend(warnings)
|
||||
|
||||
if module.params.get("continue_on_error"):
|
||||
result["error"] = error
|
||||
else:
|
||||
module.fail_json(**error)
|
||||
|
||||
if warnings:
|
||||
result.setdefault("warnings", []).extend(warnings)
|
||||
changed |= result["changed"]
|
||||
results.append(result)
|
||||
|
||||
if len(results) == 1:
|
||||
module.exit_json(**results[0])
|
||||
|
||||
module.exit_json(**{"changed": changed, "result": {"results": results}})
|
||||
|
||||
|
||||
def perform_action(svc, definition: Dict, params: Dict) -> Dict:
|
||||
origin_name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
label_selectors = params.get("label_selectors")
|
||||
state = params.get("state", None)
|
||||
kind = definition.get("kind")
|
||||
api_version = definition.get("apiVersion")
|
||||
|
||||
result = {"changed": False, "result": {}}
|
||||
instance = {}
|
||||
|
||||
resource = svc.find_resource(kind, api_version, fail=True)
|
||||
definition["kind"] = resource.kind
|
||||
definition["apiVersion"] = resource.group_version
|
||||
existing = svc.retrieve(resource, definition)
|
||||
|
||||
if state == "absent":
|
||||
if exists(existing) and existing.kind.endswith("List"):
|
||||
instance = []
|
||||
for item in existing.items:
|
||||
r = svc.delete(resource, item, existing)
|
||||
instance.append(r)
|
||||
else:
|
||||
instance = svc.delete(resource, definition, existing)
|
||||
result["method"] = "delete"
|
||||
if exists(existing):
|
||||
result["changed"] = True
|
||||
else:
|
||||
if label_selectors:
|
||||
filter_selector = LabelSelectorFilter(label_selectors)
|
||||
if not filter_selector.isMatching(definition):
|
||||
result["changed"] = False
|
||||
result["msg"] = (
|
||||
"resource 'kind={kind},name={name},namespace={namespace}' "
|
||||
"filtered by label_selectors.".format(
|
||||
kind=kind,
|
||||
name=origin_name,
|
||||
namespace=namespace,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
if params.get("apply"):
|
||||
instance = svc.apply(resource, definition, existing)
|
||||
result["method"] = "apply"
|
||||
elif not existing:
|
||||
if state == "patched":
|
||||
result.setdefault("warnings", []).append(
|
||||
"resource 'kind={kind},name={name}' was not found but will not be "
|
||||
"created as 'state' parameter has been set to '{state}'".format(
|
||||
kind=kind, name=definition["metadata"].get("name"), state=state
|
||||
)
|
||||
)
|
||||
return result
|
||||
instance = svc.create(resource, definition)
|
||||
result["method"] = "create"
|
||||
result["changed"] = True
|
||||
elif params.get("force", False):
|
||||
instance = svc.replace(resource, definition, existing)
|
||||
result["method"] = "replace"
|
||||
else:
|
||||
instance = svc.update(resource, definition, existing)
|
||||
result["method"] = "update"
|
||||
|
||||
# If needed, wait and/or create diff
|
||||
success = True
|
||||
|
||||
if result["method"] == "delete":
|
||||
# wait logic is a bit different for delete as `instance` may be a status object
|
||||
if params.get("wait") and not svc.module.check_mode:
|
||||
success, waited, duration = svc.wait(resource, definition)
|
||||
result["duration"] = duration
|
||||
else:
|
||||
if params.get("wait") and not svc.module.check_mode:
|
||||
success, instance, duration = svc.wait(resource, instance)
|
||||
result["duration"] = duration
|
||||
|
||||
if result["method"] not in ("create", "delete"):
|
||||
if existing:
|
||||
existing = existing.to_dict()
|
||||
else:
|
||||
existing = {}
|
||||
match, diffs = diff_objects(existing, instance)
|
||||
if match and diffs:
|
||||
result.setdefault("warnings", []).append(
|
||||
"No meaningful diff was generated, but the API may not be idempotent "
|
||||
"(only metadata.generation or metadata.resourceVersion were changed)"
|
||||
)
|
||||
result["changed"] = not match
|
||||
if svc.module._diff:
|
||||
result["diff"] = diffs
|
||||
|
||||
result["result"] = instance
|
||||
if not success:
|
||||
raise ResourceTimeout(
|
||||
'"{0}" "{1}": Timed out waiting on resource'.format(
|
||||
definition["kind"], origin_name
|
||||
),
|
||||
result,
|
||||
)
|
||||
|
||||
return result
|
||||
496
plugins/module_utils/k8s/service.py
Normal file
496
plugins/module_utils/k8s/service.py
Normal file
@@ -0,0 +1,496 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import (
|
||||
generate_hash,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import (
|
||||
Waiter,
|
||||
exists,
|
||||
resource_absent,
|
||||
get_waiter,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
requires,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
from ansible.module_utils.common.dict_transformations import dict_merge
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import (
|
||||
NotFoundError,
|
||||
ResourceNotFoundError,
|
||||
ResourceNotUniqueError,
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
MethodNotAllowedError,
|
||||
BadRequestError,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.resource import Resource, ResourceInstance
|
||||
except ImportError:
|
||||
# These are defined only for the sake of Ansible's checked import requirement
|
||||
Resource = Any # type: ignore
|
||||
ResourceInstance = Any # type: ignore
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.apply import (
|
||||
apply_object,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.apply import (
|
||||
recursive_diff,
|
||||
)
|
||||
except ImportError:
|
||||
from ansible.module_utils.common.dict_transformations import recursive_diff
|
||||
|
||||
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
_encode_stringdata,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
|
||||
class K8sService:
|
||||
"""A Service class for K8S modules.
|
||||
This class has the primary purpose is to perform work on the cluster (e.g., create, apply, replace, update, delete).
|
||||
"""
|
||||
|
||||
def __init__(self, client, module) -> None:
|
||||
self.client = client
|
||||
self.module = module
|
||||
|
||||
@property
|
||||
def _client_side_dry_run(self):
|
||||
return self.module.check_mode and not self.client.dry_run
|
||||
|
||||
def find_resource(
|
||||
self, kind: str, api_version: str, fail: bool = False
|
||||
) -> Optional[Resource]:
|
||||
try:
|
||||
return self.client.resource(kind, api_version)
|
||||
except (ResourceNotFoundError, ResourceNotUniqueError):
|
||||
if fail:
|
||||
raise CoreException(
|
||||
"Failed to find exact match for %s.%s by [kind, name, singularName, shortNames]"
|
||||
% (api_version, kind)
|
||||
)
|
||||
|
||||
def wait(
|
||||
self, resource: Resource, instance: Dict
|
||||
) -> Tuple[bool, Optional[Dict], int]:
|
||||
wait_sleep = self.module.params.get("wait_sleep")
|
||||
wait_timeout = self.module.params.get("wait_timeout")
|
||||
wait_condition = None
|
||||
if self.module.params.get("wait_condition") and self.module.params[
|
||||
"wait_condition"
|
||||
].get("type"):
|
||||
wait_condition = self.module.params["wait_condition"]
|
||||
state = "present"
|
||||
if self.module.params.get("state") == "absent":
|
||||
state = "absent"
|
||||
label_selectors = self.module.params.get("label_selectors")
|
||||
|
||||
waiter = get_waiter(
|
||||
self.client, resource, condition=wait_condition, state=state
|
||||
)
|
||||
return waiter.wait(
|
||||
timeout=wait_timeout,
|
||||
sleep=wait_sleep,
|
||||
name=instance["metadata"].get("name"),
|
||||
namespace=instance["metadata"].get("namespace"),
|
||||
label_selectors=label_selectors,
|
||||
)
|
||||
|
||||
def create_project_request(self, definition: Dict) -> Dict:
|
||||
definition["kind"] = "ProjectRequest"
|
||||
results = {"changed": False, "result": {}}
|
||||
resource = self.find_resource(
|
||||
"ProjectRequest", definition["apiVersion"], fail=True
|
||||
)
|
||||
if not self.module.check_mode:
|
||||
try:
|
||||
k8s_obj = self.client.create(resource, definition)
|
||||
results["result"] = k8s_obj.to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to create object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
results["changed"] = True
|
||||
|
||||
return results
|
||||
|
||||
def patch_resource(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
name: str,
|
||||
namespace: str,
|
||||
merge_type: str = None,
|
||||
) -> Dict:
|
||||
if merge_type == "json":
|
||||
self.module.deprecate(
|
||||
msg="json as a merge_type value is deprecated. Please use the k8s_json_patch module instead.",
|
||||
version="3.0.0",
|
||||
collection_name="kubernetes.core",
|
||||
)
|
||||
try:
|
||||
params = dict(name=name, namespace=namespace)
|
||||
if merge_type:
|
||||
params["content_type"] = "application/{0}-patch+json".format(merge_type)
|
||||
return self.client.patch(resource, definition, **params).to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to patch object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
def retrieve(self, resource: Resource, definition: Dict) -> ResourceInstance:
|
||||
state = self.module.params.get("state", None)
|
||||
append_hash = self.module.params.get("append_hash", False)
|
||||
name = definition["metadata"].get("name")
|
||||
generate_name = definition["metadata"].get("generateName")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
label_selectors = self.module.params.get("label_selectors")
|
||||
existing: ResourceInstance = None
|
||||
|
||||
try:
|
||||
# ignore append_hash for resources other than ConfigMap and Secret
|
||||
if append_hash and definition["kind"] in ["ConfigMap", "Secret"]:
|
||||
if name:
|
||||
name = "%s-%s" % (name, generate_hash(definition))
|
||||
definition["metadata"]["name"] = name
|
||||
elif generate_name:
|
||||
definition["metadata"]["generateName"] = "%s-%s" % (
|
||||
generate_name,
|
||||
generate_hash(definition),
|
||||
)
|
||||
params = {}
|
||||
if name:
|
||||
params["name"] = name
|
||||
if namespace:
|
||||
params["namespace"] = namespace
|
||||
if label_selectors:
|
||||
params["label_selector"] = ",".join(label_selectors)
|
||||
if "name" in params or "label_selector" in params:
|
||||
existing = self.client.get(resource, **params)
|
||||
except (NotFoundError, MethodNotAllowedError):
|
||||
pass
|
||||
except ForbiddenError as e:
|
||||
if (
|
||||
definition["kind"] in ["Project", "ProjectRequest"]
|
||||
and state != "absent"
|
||||
):
|
||||
return self.create_project_request(definition)
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to retrieve requested object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to retrieve requested object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
return existing
|
||||
|
||||
def find(
|
||||
self,
|
||||
kind: str,
|
||||
api_version: str,
|
||||
name: str = None,
|
||||
namespace: Optional[str] = None,
|
||||
label_selectors: Optional[List[str]] = None,
|
||||
field_selectors: Optional[List[str]] = None,
|
||||
wait: Optional[bool] = False,
|
||||
wait_sleep: Optional[int] = 5,
|
||||
wait_timeout: Optional[int] = 120,
|
||||
state: Optional[str] = "present",
|
||||
condition: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
resource = self.find_resource(kind, api_version)
|
||||
api_found = bool(resource)
|
||||
if not api_found:
|
||||
return dict(
|
||||
resources=[],
|
||||
msg='Failed to find API for resource with apiVersion "{0}" and kind "{1}"'.format(
|
||||
api_version, kind
|
||||
),
|
||||
api_found=False,
|
||||
)
|
||||
|
||||
if not label_selectors:
|
||||
label_selectors = []
|
||||
if not field_selectors:
|
||||
field_selectors = []
|
||||
|
||||
result = {"resources": [], "api_found": True}
|
||||
|
||||
# With a timeout of 0 the waiter will do a single check and return, effectively not waiting.
|
||||
if not wait:
|
||||
wait_timeout = 0
|
||||
|
||||
if state == "present":
|
||||
predicate = exists
|
||||
else:
|
||||
predicate = resource_absent
|
||||
|
||||
waiter = Waiter(self.client, resource, predicate)
|
||||
|
||||
# This is an initial check to get the resource or resources that we then need to wait on individually.
|
||||
try:
|
||||
success, resources, duration = waiter.wait(
|
||||
timeout=wait_timeout,
|
||||
sleep=wait_sleep,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
label_selectors=label_selectors,
|
||||
field_selectors=field_selectors,
|
||||
)
|
||||
except BadRequestError:
|
||||
return result
|
||||
except CoreException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise CoreException(
|
||||
"Exception '{0}' raised while trying to get resource using (name={1}, namespace={2}, label_selectors={3}, field_selectors={4})".format(
|
||||
e, name, namespace, label_selectors, field_selectors
|
||||
)
|
||||
)
|
||||
|
||||
# There is either no result or there is a List resource with no items
|
||||
if (
|
||||
not resources
|
||||
or resources["kind"].endswith("List")
|
||||
and not resources.get("items")
|
||||
):
|
||||
return result
|
||||
|
||||
instances = resources.get("items") or [resources]
|
||||
|
||||
if not wait:
|
||||
result["resources"] = instances
|
||||
return result
|
||||
|
||||
# Now wait for the specified state of any resource instances we have found.
|
||||
waiter = get_waiter(self.client, resource, state=state, condition=condition)
|
||||
for instance in instances:
|
||||
name = instance["metadata"].get("name")
|
||||
namespace = instance["metadata"].get("namespace")
|
||||
success, res, duration = waiter.wait(
|
||||
timeout=wait_timeout,
|
||||
sleep=wait_sleep,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
)
|
||||
if not success:
|
||||
raise CoreException(
|
||||
"Failed to gather information about %s(s) even"
|
||||
" after waiting for %s seconds" % (res.get("kind"), duration)
|
||||
)
|
||||
result["resources"].append(res)
|
||||
return result
|
||||
|
||||
def create(self, resource: Resource, definition: Dict) -> Dict:
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
name = definition["metadata"].get("name")
|
||||
|
||||
if self._client_side_dry_run:
|
||||
k8s_obj = _encode_stringdata(definition)
|
||||
else:
|
||||
try:
|
||||
k8s_obj = self.client.create(
|
||||
resource, definition, namespace=namespace
|
||||
).to_dict()
|
||||
except ConflictError:
|
||||
# Some resources, like ProjectRequests, can't be created multiple times,
|
||||
# because the resources that they create don't match their kind
|
||||
# In this case we'll mark it as unchanged and warn the user
|
||||
self.module.warn(
|
||||
"{0} was not found, but creating it returned a 409 Conflict error. This can happen \
|
||||
if the resource you are creating does not directly create a resource of the same kind.".format(
|
||||
name
|
||||
)
|
||||
)
|
||||
return dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to create object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
return k8s_obj
|
||||
|
||||
def apply(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
existing: Optional[ResourceInstance] = None,
|
||||
) -> Dict:
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
|
||||
server_side_apply = self.module.params.get("server_side_apply")
|
||||
if server_side_apply:
|
||||
requires("kubernetes", "19.15.0", reason="to use server side apply")
|
||||
if self._client_side_dry_run:
|
||||
ignored, patch = apply_object(resource, _encode_stringdata(definition))
|
||||
if existing:
|
||||
k8s_obj = dict_merge(existing.to_dict(), patch)
|
||||
else:
|
||||
k8s_obj = patch
|
||||
else:
|
||||
try:
|
||||
params = {}
|
||||
if server_side_apply:
|
||||
params["server_side"] = True
|
||||
params.update(server_side_apply)
|
||||
k8s_obj = self.client.apply(
|
||||
resource, definition, namespace=namespace, **params
|
||||
).to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to apply object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
return k8s_obj
|
||||
|
||||
def replace(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
existing: ResourceInstance,
|
||||
) -> Dict:
|
||||
append_hash = self.module.params.get("append_hash", False)
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
|
||||
if self._client_side_dry_run:
|
||||
k8s_obj = _encode_stringdata(definition)
|
||||
else:
|
||||
try:
|
||||
k8s_obj = self.client.replace(
|
||||
resource,
|
||||
definition,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
append_hash=append_hash,
|
||||
).to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to replace object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
return k8s_obj
|
||||
|
||||
def update(
|
||||
self, resource: Resource, definition: Dict, existing: ResourceInstance
|
||||
) -> Dict:
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
|
||||
if self._client_side_dry_run:
|
||||
k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition))
|
||||
else:
|
||||
exception = None
|
||||
for merge_type in self.module.params.get("merge_type") or [
|
||||
"strategic-merge",
|
||||
"merge",
|
||||
]:
|
||||
try:
|
||||
k8s_obj = self.patch_resource(
|
||||
resource,
|
||||
definition,
|
||||
name,
|
||||
namespace,
|
||||
merge_type=merge_type,
|
||||
)
|
||||
exception = None
|
||||
except CoreException as e:
|
||||
exception = e
|
||||
continue
|
||||
break
|
||||
if exception:
|
||||
raise exception
|
||||
return k8s_obj
|
||||
|
||||
def delete(
|
||||
self,
|
||||
resource: Resource,
|
||||
definition: Dict,
|
||||
existing: Optional[ResourceInstance] = None,
|
||||
) -> Dict:
|
||||
delete_options = self.module.params.get("delete_options")
|
||||
label_selectors = self.module.params.get("label_selectors")
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
params = {}
|
||||
|
||||
if not exists(existing):
|
||||
return {}
|
||||
|
||||
# Delete the object
|
||||
if self._client_side_dry_run:
|
||||
return {}
|
||||
|
||||
if name:
|
||||
params["name"] = name
|
||||
|
||||
if namespace:
|
||||
params["namespace"] = namespace
|
||||
|
||||
if label_selectors:
|
||||
params["label_selector"] = ",".join(label_selectors)
|
||||
|
||||
if delete_options:
|
||||
body = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "DeleteOptions",
|
||||
}
|
||||
body.update(delete_options)
|
||||
params["body"] = body
|
||||
|
||||
try:
|
||||
k8s_obj = self.client.delete(resource, **params).to_dict()
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to delete object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
return k8s_obj
|
||||
|
||||
|
||||
def diff_objects(existing: Dict, new: Dict) -> Tuple[bool, Dict]:
|
||||
result = {}
|
||||
diff = recursive_diff(existing, new)
|
||||
if not diff:
|
||||
return True, result
|
||||
|
||||
result["before"] = diff[0]
|
||||
result["after"] = diff[1]
|
||||
|
||||
if list(result["after"].keys()) != ["metadata"] or list(
|
||||
result["before"].keys()
|
||||
) != ["metadata"]:
|
||||
return False, result
|
||||
|
||||
# If only metadata.generation and metadata.resourceVersion changed, ignore it
|
||||
ignored_keys = set(["generation", "resourceVersion"])
|
||||
|
||||
if not set(result["after"]["metadata"].keys()).issubset(ignored_keys):
|
||||
return False, result
|
||||
if not set(result["before"]["metadata"].keys()).issubset(ignored_keys):
|
||||
return False, result
|
||||
|
||||
return True, result
|
||||
238
plugins/module_utils/k8s/waiter.py
Normal file
238
plugins/module_utils/k8s/waiter.py
Normal file
@@ -0,0 +1,238 @@
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import NotFoundError
|
||||
from kubernetes.dynamic.resource import Resource, ResourceField, ResourceInstance
|
||||
except ImportError:
|
||||
# These are defined only for the sake of Ansible's checked import requirement
|
||||
Resource = Any # type: ignore
|
||||
ResourceInstance = Any # type: ignore
|
||||
pass
|
||||
|
||||
try:
|
||||
from urllib3.exceptions import HTTPError
|
||||
except ImportError:
|
||||
# Handled during module setup
|
||||
pass
|
||||
|
||||
|
||||
def deployment_ready(deployment: ResourceInstance) -> bool:
|
||||
# FIXME: frustratingly bool(deployment.status) is True even if status is empty
|
||||
# Furthermore deployment.status.availableReplicas == deployment.status.replicas == None if status is empty
|
||||
# deployment.status.replicas is None is perfectly ok if desired replicas == 0
|
||||
# Scaling up means that we also need to check that we're not in a
|
||||
# situation where status.replicas == status.availableReplicas
|
||||
# but spec.replicas != status.replicas
|
||||
return bool(
|
||||
deployment.status
|
||||
and deployment.spec.replicas == (deployment.status.replicas or 0)
|
||||
and deployment.status.availableReplicas == deployment.status.replicas
|
||||
and deployment.status.observedGeneration == deployment.metadata.generation
|
||||
and not deployment.status.unavailableReplicas
|
||||
)
|
||||
|
||||
|
||||
def pod_ready(pod: ResourceInstance) -> bool:
|
||||
return bool(
|
||||
pod.status
|
||||
and pod.status.containerStatuses is not None
|
||||
and all(container.ready for container in pod.status.containerStatuses)
|
||||
)
|
||||
|
||||
|
||||
def daemonset_ready(daemonset: ResourceInstance) -> bool:
|
||||
return bool(
|
||||
daemonset.status
|
||||
and daemonset.status.desiredNumberScheduled is not None
|
||||
and daemonset.status.updatedNumberScheduled
|
||||
== daemonset.status.desiredNumberScheduled
|
||||
and daemonset.status.numberReady == daemonset.status.desiredNumberScheduled
|
||||
and daemonset.status.observedGeneration == daemonset.metadata.generation
|
||||
and not daemonset.status.unavailableReplicas
|
||||
)
|
||||
|
||||
|
||||
def statefulset_ready(statefulset: ResourceInstance) -> bool:
|
||||
# These may be None
|
||||
updated_replicas = statefulset.status.updatedReplicas or 0
|
||||
ready_replicas = statefulset.status.readyReplicas or 0
|
||||
return bool(
|
||||
statefulset.status
|
||||
and statefulset.spec.updateStrategy.type == "RollingUpdate"
|
||||
and statefulset.status.observedGeneration
|
||||
== (statefulset.metadata.generation or 0)
|
||||
and statefulset.status.updateRevision == statefulset.status.currentRevision
|
||||
and updated_replicas == statefulset.spec.replicas
|
||||
and ready_replicas == statefulset.spec.replicas
|
||||
and statefulset.status.replicas == statefulset.spec.replicas
|
||||
)
|
||||
|
||||
|
||||
def custom_condition(condition: Dict, resource: ResourceInstance) -> bool:
|
||||
if not resource.status or not resource.status.conditions:
|
||||
return False
|
||||
matches = [x for x in resource.status.conditions if x.type == condition["type"]]
|
||||
if not matches:
|
||||
return False
|
||||
# There should never be more than one condition of a specific type
|
||||
match: ResourceField = matches[0]
|
||||
if match.status == "Unknown":
|
||||
if match.status == condition["status"]:
|
||||
if "reason" not in condition:
|
||||
return True
|
||||
if condition["reason"]:
|
||||
return match.reason == condition["reason"]
|
||||
return False
|
||||
status = True if match.status == "True" else False
|
||||
if status == boolean(condition["status"], strict=False):
|
||||
if condition.get("reason"):
|
||||
return match.reason == condition["reason"]
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def resource_absent(resource: ResourceInstance) -> bool:
|
||||
return not exists(resource)
|
||||
|
||||
|
||||
def exists(resource: Optional[ResourceInstance]) -> bool:
|
||||
"""Simple predicate to check for existence of a resource.
|
||||
|
||||
While a List type resource technically always exists, this will only return
|
||||
true if the List contains items."""
|
||||
return bool(resource) and not empty_list(resource)
|
||||
|
||||
|
||||
RESOURCE_PREDICATES = {
|
||||
"DaemonSet": daemonset_ready,
|
||||
"Deployment": deployment_ready,
|
||||
"Pod": pod_ready,
|
||||
"StatefulSet": statefulset_ready,
|
||||
}
|
||||
|
||||
|
||||
def empty_list(resource: ResourceInstance) -> bool:
|
||||
return resource["kind"].endswith("List") and not resource.get("items")
|
||||
|
||||
|
||||
def clock(total: int, interval: int) -> Iterator[int]:
|
||||
start = time.monotonic()
|
||||
yield 0
|
||||
while (time.monotonic() - start) < total:
|
||||
time.sleep(interval)
|
||||
yield int(time.monotonic() - start)
|
||||
|
||||
|
||||
class Waiter:
|
||||
def __init__(
|
||||
self, client, resource: Resource, predicate: Callable[[ResourceInstance], bool]
|
||||
):
|
||||
self.client = client
|
||||
self.resource = resource
|
||||
self.predicate = predicate
|
||||
|
||||
def wait(
|
||||
self,
|
||||
timeout: int,
|
||||
sleep: int,
|
||||
name: Optional[str] = None,
|
||||
namespace: Optional[str] = None,
|
||||
label_selectors: Optional[List[str]] = None,
|
||||
field_selectors: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, Dict, int]:
|
||||
params = {}
|
||||
|
||||
if name:
|
||||
params["name"] = name
|
||||
|
||||
if namespace:
|
||||
params["namespace"] = namespace
|
||||
|
||||
if label_selectors:
|
||||
params["label_selector"] = ",".join(label_selectors)
|
||||
|
||||
if field_selectors:
|
||||
params["field_selector"] = ",".join(field_selectors)
|
||||
|
||||
instance = {}
|
||||
response = None
|
||||
elapsed = 0
|
||||
for i in clock(timeout, sleep):
|
||||
exception = None
|
||||
elapsed = i
|
||||
try:
|
||||
response = self.client.get(self.resource, **params)
|
||||
except NotFoundError:
|
||||
response = None
|
||||
# Retry connection errors as it may be intermittent network issues
|
||||
except HTTPError as e:
|
||||
exception = e
|
||||
if self.predicate(response):
|
||||
break
|
||||
if exception:
|
||||
msg = (
|
||||
"Exception '{0}' raised while trying to get resource using {1}".format(
|
||||
exception, params
|
||||
)
|
||||
)
|
||||
raise CoreException(msg) from exception
|
||||
if response:
|
||||
instance = response.to_dict()
|
||||
return self.predicate(response), instance, elapsed
|
||||
|
||||
|
||||
class DummyWaiter:
|
||||
"""A no-op waiter that simply returns the item being waited on.
|
||||
|
||||
No API call will be made with this waiter; the function returns
|
||||
immediately. This waiter is useful for waiting on resource instances in
|
||||
check mode, for example.
|
||||
"""
|
||||
|
||||
def wait(
|
||||
self,
|
||||
definition: Dict,
|
||||
timeout: int,
|
||||
sleep: int,
|
||||
label_selectors: Optional[List[str]] = None,
|
||||
) -> Tuple[bool, Optional[Dict], int]:
|
||||
return True, definition, 0
|
||||
|
||||
|
||||
# The better solution would be typing.Protocol, but this is only in 3.8+
|
||||
SupportsWait = Union[Waiter, DummyWaiter]
|
||||
|
||||
|
||||
def get_waiter(
|
||||
client,
|
||||
resource: Resource,
|
||||
state: str = "present",
|
||||
condition: Optional[Dict] = None,
|
||||
check_mode: Optional[bool] = False,
|
||||
) -> SupportsWait:
|
||||
"""Create a Waiter object based on the specified resource.
|
||||
|
||||
This is a convenience method for creating a waiter from a resource.
|
||||
Based on the arguments and the kind of resource, an appropriate waiter
|
||||
will be returned. A waiter can also be created directly, of course.
|
||||
"""
|
||||
if check_mode:
|
||||
return DummyWaiter()
|
||||
if state == "present":
|
||||
if condition:
|
||||
predicate: Callable[[ResourceInstance], bool] = partial(
|
||||
custom_condition, condition
|
||||
)
|
||||
else:
|
||||
predicate = RESOURCE_PREDICATES.get(resource.kind, exists)
|
||||
else:
|
||||
predicate = resource_absent
|
||||
return Waiter(client, resource, predicate)
|
||||
@@ -14,26 +14,37 @@
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from kubernetes.dynamic import DynamicClient
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.apply import k8s_apply
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import ApplyException
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import (
|
||||
ApplyException,
|
||||
)
|
||||
|
||||
|
||||
class K8SDynamicClient(DynamicClient):
|
||||
def apply(self, resource, body=None, name=None, namespace=None):
|
||||
def apply(self, resource, body=None, name=None, namespace=None, **kwargs):
|
||||
body = super().serialize_body(body)
|
||||
body['metadata'] = body.get('metadata', dict())
|
||||
name = name or body['metadata'].get('name')
|
||||
body["metadata"] = body.get("metadata", dict())
|
||||
name = name or body["metadata"].get("name")
|
||||
if not name:
|
||||
raise ValueError("name is required to apply {0}.{1}".format(resource.group_version, resource.kind))
|
||||
raise ValueError(
|
||||
"name is required to apply {0}.{1}".format(
|
||||
resource.group_version, resource.kind
|
||||
)
|
||||
)
|
||||
if resource.namespaced:
|
||||
body['metadata']['namespace'] = super().ensure_namespace(resource, namespace, body)
|
||||
body["metadata"]["namespace"] = super().ensure_namespace(
|
||||
resource, namespace, body
|
||||
)
|
||||
try:
|
||||
return k8s_apply(resource, body)
|
||||
return k8s_apply(resource, body, **kwargs)
|
||||
except ApplyException as e:
|
||||
raise ValueError("Could not apply strategic merge to %s/%s: %s" %
|
||||
(body['kind'], body['metadata']['name'], e))
|
||||
raise ValueError(
|
||||
"Could not apply strategic merge to %s/%s: %s"
|
||||
% (body["kind"], body["metadata"]["name"], e)
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ import re
|
||||
|
||||
class Selector(object):
|
||||
|
||||
equality_based_operators = ('==', '!=', '=')
|
||||
equality_based_operators = ("==", "!=", "=")
|
||||
|
||||
def __init__(self, data):
|
||||
self._operator = None
|
||||
@@ -27,18 +27,23 @@ class Selector(object):
|
||||
for op in self.equality_based_operators:
|
||||
idx = no_whitespace_data.find(op)
|
||||
if idx != -1:
|
||||
self._operator = "in" if op == '==' or op == '=' else "notin"
|
||||
self._operator = "in" if op == "==" or op == "=" else "notin"
|
||||
self._key = no_whitespace_data[0:idx]
|
||||
# fmt: off
|
||||
self._data = [no_whitespace_data[idx + len(op):]]
|
||||
# fmt: on
|
||||
break
|
||||
|
||||
def parse_set_based_requirement(self, data):
|
||||
m = re.match(r'( *)([a-z0-9A-Z][a-z0-9A-Z\._-]*[a-z0-9A-Z])( +)(notin|in)( +)\((.*)\)( *)', data)
|
||||
m = re.match(
|
||||
r"( *)([a-z0-9A-Z][a-z0-9A-Z\._-]*[a-z0-9A-Z])( +)(notin|in)( +)\((.*)\)( *)",
|
||||
data,
|
||||
)
|
||||
if m:
|
||||
self._set_based_requirement = True
|
||||
self._key = m.group(2)
|
||||
self._operator = m.group(4)
|
||||
self._data = [x.replace(' ', '') for x in m.group(6).split(',') if x != '']
|
||||
self._data = [x.replace(" ", "") for x in m.group(6).split(",") if x != ""]
|
||||
return True
|
||||
elif all(x not in data for x in self.equality_based_operators):
|
||||
self._key = data.rstrip(" ").lstrip(" ")
|
||||
@@ -54,18 +59,21 @@ class Selector(object):
|
||||
elif self._operator == "notin":
|
||||
return self._key not in labels or labels.get(self._key) not in self._data
|
||||
else:
|
||||
return self._key not in labels if self._operator == "!" else self._key in labels
|
||||
return (
|
||||
self._key not in labels
|
||||
if self._operator == "!"
|
||||
else self._key in labels
|
||||
)
|
||||
|
||||
|
||||
class LabelSelectorFilter(object):
|
||||
|
||||
def __init__(self, label_selectors):
|
||||
self.selectors = [Selector(data) for data in label_selectors]
|
||||
|
||||
def isMatching(self, definition):
|
||||
if "metadata" not in definition or "labels" not in definition['metadata']:
|
||||
if "metadata" not in definition or "labels" not in definition["metadata"]:
|
||||
return False
|
||||
labels = definition['metadata']['labels']
|
||||
labels = definition["metadata"]["labels"]
|
||||
if not isinstance(labels, dict):
|
||||
return None
|
||||
return all(sel.isMatch(labels) for sel in self.selectors)
|
||||
|
||||
18
plugins/module_utils/version.py
Normal file
18
plugins/module_utils/version.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
"""Provide version object to compare version numbers."""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
|
||||
# remove the _version.py file, and replace the following import by
|
||||
#
|
||||
# from ansible.module_utils.compat.version import LooseVersion
|
||||
|
||||
from ._version import LooseVersion # noqa: F401
|
||||
@@ -4,10 +4,11 @@
|
||||
# Copyright: Ansible Project
|
||||
# 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'''
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: helm
|
||||
|
||||
@@ -26,6 +27,10 @@ requirements:
|
||||
description:
|
||||
- Install, upgrade, delete packages with the Helm package manager.
|
||||
|
||||
notes:
|
||||
- The default idempotency check can fail to report changes when C(release_state) is set to C(present)
|
||||
and C(chart_repo_url) is defined. Install helm diff >= 3.4.1 for better results.
|
||||
|
||||
options:
|
||||
chart_ref:
|
||||
description:
|
||||
@@ -46,6 +51,17 @@ options:
|
||||
- Chart version to install. If this is not specified, the latest version is installed.
|
||||
required: false
|
||||
type: str
|
||||
dependency_update:
|
||||
description:
|
||||
- Run standalone C(helm dependency update CHART) before the operation.
|
||||
- Run inline C(--dependency-update) with C(helm install) command. This feature is not supported yet with the C(helm upgrade) command.
|
||||
- So we should consider to use I(dependency_update) options with I(replace) option enabled when specifying I(chart_repo_url).
|
||||
- The I(dependency_update) option require the add of C(dependencies) block in C(Chart.yaml/requirements.yaml) file.
|
||||
- For more information please visit U(https://helm.sh/docs/helm/helm_dependency/)
|
||||
default: false
|
||||
type: bool
|
||||
aliases: [ dep_up ]
|
||||
version_added: "2.4.0"
|
||||
release_name:
|
||||
description:
|
||||
- Release name to manage.
|
||||
@@ -86,9 +102,34 @@ options:
|
||||
version_added: '1.1.0'
|
||||
update_repo_cache:
|
||||
description:
|
||||
- Run C(helm repo update) before the operation. Can be run as part of the package installation or as a separate step.
|
||||
- Run C(helm repo update) before the operation. Can be run as part of the package installation or as a separate step (see Examples).
|
||||
default: false
|
||||
type: bool
|
||||
set_values:
|
||||
description:
|
||||
- Values to pass to chart configuration
|
||||
required: false
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
value:
|
||||
description:
|
||||
- Value to pass to chart configuration (e.g phase=prod).
|
||||
type: str
|
||||
required: true
|
||||
value_type:
|
||||
description:
|
||||
- Use C(raw) set individual value.
|
||||
- Use C(string) to force a string for an individual value.
|
||||
- Use C(file) to set individual values from a file when the value itself is too long for the command line or is dynamically generated.
|
||||
- Use C(json) to set json values (scalars/objects/arrays). This feature requires helm>=3.10.0.
|
||||
default: raw
|
||||
choices:
|
||||
- raw
|
||||
- string
|
||||
- json
|
||||
- file
|
||||
version_added: '2.4.0'
|
||||
|
||||
#Helm options
|
||||
disable_hook:
|
||||
@@ -108,13 +149,24 @@ options:
|
||||
type: bool
|
||||
wait:
|
||||
description:
|
||||
- Wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful.
|
||||
- When I(release_state) is set to C(present), wait until all Pods, PVCs, Services,
|
||||
and minimum number of Pods of a Deployment are in a ready state before marking the release as successful.
|
||||
- When I(release_state) is set to C(absent), will wait until all the resources are deleted before returning.
|
||||
It will wait for as long as I(wait_timeout). This feature requires helm>=3.7.0. Added in version 2.3.0.
|
||||
default: False
|
||||
type: bool
|
||||
wait_timeout:
|
||||
description:
|
||||
- Timeout when wait option is enabled (helm2 is a number of seconds, helm3 is a duration).
|
||||
- The use of I(wait_timeout) to wait for kubernetes commands to complete has been deprecated and will be removed after 2022-12-01.
|
||||
type: str
|
||||
timeout:
|
||||
description:
|
||||
- A Go duration (described here I(https://pkg.go.dev/time#ParseDuration)) value to wait for Kubernetes commands to complete. This defaults to 5m0s.
|
||||
- similar to C(wait_timeout) but does not required C(wait) to be activated.
|
||||
- Mutually exclusive with C(wait_timeout).
|
||||
type: str
|
||||
version_added: "2.3.0"
|
||||
atomic:
|
||||
description:
|
||||
- If set, the installation process deletes the installation on failure.
|
||||
@@ -126,6 +178,11 @@ options:
|
||||
type: bool
|
||||
default: False
|
||||
version_added: "0.11.1"
|
||||
post_renderer:
|
||||
description:
|
||||
- Path to an executable to be used for post rendering.
|
||||
type: str
|
||||
version_added: "2.4.0"
|
||||
replace:
|
||||
description:
|
||||
- Reuse the given name, only if that name is a deleted release which remains in the history.
|
||||
@@ -148,9 +205,9 @@ options:
|
||||
version_added: "2.2.0"
|
||||
extends_documentation_fragment:
|
||||
- kubernetes.core.helm_common_options
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Deploy latest version of Prometheus chart inside monitoring namespace (and create it)
|
||||
kubernetes.core.helm:
|
||||
name: test
|
||||
@@ -193,6 +250,22 @@ EXAMPLES = r'''
|
||||
state: absent
|
||||
wait: true
|
||||
|
||||
- name: Separately update the repository cache
|
||||
kubernetes.core.helm:
|
||||
name: dummy
|
||||
namespace: kube-system
|
||||
state: absent
|
||||
update_repo_cache: true
|
||||
|
||||
- name: Deploy Grafana chart using set values on target
|
||||
kubernetes.core.helm:
|
||||
name: test
|
||||
chart_ref: stable/grafana
|
||||
release_namespace: monitoring
|
||||
set_values:
|
||||
- value: phase=prod
|
||||
value_type: string
|
||||
|
||||
# From git
|
||||
- name: Git clone stable repo on HEAD
|
||||
ansible.builtin.git:
|
||||
@@ -237,7 +310,7 @@ EXAMPLES = r'''
|
||||
enabled: True
|
||||
logging:
|
||||
enabled: True
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
status:
|
||||
@@ -294,22 +367,30 @@ command:
|
||||
sample: helm upgrade ...
|
||||
"""
|
||||
|
||||
import re
|
||||
import tempfile
|
||||
import traceback
|
||||
import copy
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
||||
IMP_YAML = True
|
||||
IMP_YAML_ERR = None
|
||||
except ImportError:
|
||||
IMP_YAML_ERR = traceback.format_exc()
|
||||
IMP_YAML = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
run_helm,
|
||||
get_values,
|
||||
get_helm_plugin_list,
|
||||
parse_helm_plugin_list
|
||||
AnsibleHelmModule,
|
||||
parse_helm_plugin_list,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import (
|
||||
HELM_AUTH_ARG_SPEC,
|
||||
)
|
||||
|
||||
|
||||
@@ -320,36 +401,46 @@ def get_release(state, release_name):
|
||||
|
||||
if state is not None:
|
||||
for release in state:
|
||||
if release['name'] == release_name:
|
||||
if release["name"] == release_name:
|
||||
return release
|
||||
return None
|
||||
|
||||
|
||||
def get_release_status(module, command, release_name):
|
||||
def get_release_status(module, release_name):
|
||||
"""
|
||||
Get Release state from deployed release
|
||||
"""
|
||||
|
||||
list_command = command + " list --output=yaml --filter " + release_name
|
||||
list_command = (
|
||||
module.get_helm_binary() + " list --output=yaml --filter " + release_name
|
||||
)
|
||||
|
||||
rc, out, err = run_helm(module, list_command)
|
||||
rc, out, err = module.run_helm_command(list_command)
|
||||
|
||||
release = get_release(yaml.safe_load(out), release_name)
|
||||
|
||||
if release is None: # not install
|
||||
return None
|
||||
|
||||
release['values'] = get_values(module, command, release_name)
|
||||
release["values"] = module.get_values(release_name)
|
||||
|
||||
return release
|
||||
|
||||
|
||||
def run_repo_update(module, command):
|
||||
def run_repo_update(module):
|
||||
"""
|
||||
Run Repo update
|
||||
"""
|
||||
repo_update_command = command + " repo update"
|
||||
rc, out, err = run_helm(module, repo_update_command)
|
||||
repo_update_command = module.get_helm_binary() + " repo update"
|
||||
rc, out, err = module.run_helm_command(repo_update_command)
|
||||
|
||||
|
||||
def run_dep_update(module, chart_ref):
|
||||
"""
|
||||
Run dependency update
|
||||
"""
|
||||
dep_update = module.get_helm_binary() + " dependency update " + chart_ref
|
||||
rc, out, err = module.run_helm_command(dep_update)
|
||||
|
||||
|
||||
def fetch_chart_info(module, command, chart_ref):
|
||||
@@ -358,20 +449,39 @@ def fetch_chart_info(module, command, chart_ref):
|
||||
"""
|
||||
inspect_command = command + " show chart " + chart_ref
|
||||
|
||||
rc, out, err = run_helm(module, inspect_command)
|
||||
rc, out, err = module.run_helm_command(inspect_command)
|
||||
|
||||
return yaml.safe_load(out)
|
||||
|
||||
|
||||
def deploy(command, release_name, release_values, chart_name, wait,
|
||||
wait_timeout, disable_hook, force, values_files, history_max, atomic=False,
|
||||
create_namespace=False, replace=False, skip_crds=False):
|
||||
def deploy(
|
||||
command,
|
||||
release_name,
|
||||
release_values,
|
||||
chart_name,
|
||||
wait,
|
||||
wait_timeout,
|
||||
disable_hook,
|
||||
force,
|
||||
values_files,
|
||||
history_max,
|
||||
atomic=False,
|
||||
create_namespace=False,
|
||||
replace=False,
|
||||
post_renderer=None,
|
||||
skip_crds=False,
|
||||
timeout=None,
|
||||
dependency_update=None,
|
||||
set_value_args=None,
|
||||
):
|
||||
"""
|
||||
Install/upgrade/rollback release chart
|
||||
"""
|
||||
if replace:
|
||||
# '--replace' is not supported by 'upgrade -i'
|
||||
deploy_command = command + " install"
|
||||
if dependency_update:
|
||||
deploy_command += " --dependency-update"
|
||||
else:
|
||||
deploy_command = command + " upgrade -i" # install/upgrade
|
||||
|
||||
@@ -386,6 +496,9 @@ def deploy(command, release_name, release_values, chart_name, wait,
|
||||
if atomic:
|
||||
deploy_command += " --atomic"
|
||||
|
||||
if timeout:
|
||||
deploy_command += " --timeout " + timeout
|
||||
|
||||
if force:
|
||||
deploy_command += " --force"
|
||||
|
||||
@@ -403,22 +516,28 @@ def deploy(command, release_name, release_values, chart_name, wait,
|
||||
deploy_command += " --values=" + value_file
|
||||
|
||||
if release_values != {}:
|
||||
fd, path = tempfile.mkstemp(suffix='.yml')
|
||||
with open(path, 'w') as yaml_file:
|
||||
fd, path = tempfile.mkstemp(suffix=".yml")
|
||||
with open(path, "w") as yaml_file:
|
||||
yaml.dump(release_values, yaml_file, default_flow_style=False)
|
||||
deploy_command += " -f=" + path
|
||||
|
||||
if post_renderer:
|
||||
deploy_command = " --post-renderer=" + post_renderer
|
||||
|
||||
if skip_crds:
|
||||
deploy_command += " --skip-crds"
|
||||
|
||||
if history_max is not None:
|
||||
deploy_command += " --history-max=%s" % str(history_max)
|
||||
|
||||
if set_value_args:
|
||||
deploy_command += " " + set_value_args
|
||||
|
||||
deploy_command += " " + release_name + " " + chart_name
|
||||
return deploy_command
|
||||
|
||||
|
||||
def delete(command, release_name, purge, disable_hook):
|
||||
def delete(command, release_name, purge, disable_hook, wait, wait_timeout):
|
||||
"""
|
||||
Delete release chart
|
||||
"""
|
||||
@@ -431,6 +550,12 @@ def delete(command, release_name, purge, disable_hook):
|
||||
if disable_hook:
|
||||
delete_command += " --no-hooks"
|
||||
|
||||
if wait:
|
||||
delete_command += " --wait"
|
||||
|
||||
if wait_timeout is not None:
|
||||
delete_command += " --timeout " + wait_timeout
|
||||
|
||||
delete_command += " " + release_name
|
||||
|
||||
return delete_command
|
||||
@@ -439,7 +564,7 @@ def delete(command, release_name, purge, disable_hook):
|
||||
def load_values_files(values_files):
|
||||
values = {}
|
||||
for values_file in values_files or []:
|
||||
with open(values_file, 'r') as fd:
|
||||
with open(values_file, "r") as fd:
|
||||
content = yaml.safe_load(fd)
|
||||
if not isinstance(content, dict):
|
||||
continue
|
||||
@@ -448,50 +573,60 @@ def load_values_files(values_files):
|
||||
return values
|
||||
|
||||
|
||||
def has_plugin(command, plugin):
|
||||
def get_plugin_version(plugin):
|
||||
"""
|
||||
Check if helm plugin is installed.
|
||||
Check if helm plugin is installed and return corresponding version
|
||||
"""
|
||||
|
||||
cmd = command + " plugin"
|
||||
rc, output, err = get_helm_plugin_list(module, helm_bin=cmd)
|
||||
out = parse_helm_plugin_list(module, output=output.splitlines())
|
||||
rc, output, err, command = module.get_helm_plugin_list()
|
||||
out = parse_helm_plugin_list(output=output.splitlines())
|
||||
|
||||
if not out:
|
||||
return False
|
||||
return None
|
||||
|
||||
for line in out:
|
||||
if line[0] == plugin:
|
||||
return True
|
||||
return False
|
||||
return line[1]
|
||||
return None
|
||||
|
||||
|
||||
def helmdiff_check(module, helm_cmd, release_name, chart_ref, release_values,
|
||||
values_files=None, chart_version=None, replace=False):
|
||||
def helmdiff_check(
|
||||
module,
|
||||
release_name,
|
||||
chart_ref,
|
||||
release_values,
|
||||
values_files=None,
|
||||
chart_version=None,
|
||||
replace=False,
|
||||
chart_repo_url=None,
|
||||
):
|
||||
"""
|
||||
Use helm diff to determine if a release would change by upgrading a chart.
|
||||
"""
|
||||
cmd = helm_cmd + " diff upgrade"
|
||||
cmd = module.get_helm_binary() + " diff upgrade"
|
||||
cmd += " " + release_name
|
||||
cmd += " " + chart_ref
|
||||
|
||||
if chart_repo_url is not None:
|
||||
cmd += " " + "--repo=" + chart_repo_url
|
||||
if chart_version is not None:
|
||||
cmd += " " + "--version=" + chart_version
|
||||
if not replace:
|
||||
cmd += " " + "--reset-values"
|
||||
|
||||
if release_values != {}:
|
||||
fd, path = tempfile.mkstemp(suffix='.yml')
|
||||
with open(path, 'w') as yaml_file:
|
||||
fd, path = tempfile.mkstemp(suffix=".yml")
|
||||
with open(path, "w") as yaml_file:
|
||||
yaml.dump(release_values, yaml_file, default_flow_style=False)
|
||||
cmd += " -f=" + path
|
||||
module.add_cleanup_file(path)
|
||||
|
||||
if values_files:
|
||||
for values_file in values_files:
|
||||
cmd += " -f=" + values_file
|
||||
|
||||
rc, out, err = run_helm(module, cmd)
|
||||
return len(out.strip()) > 0
|
||||
rc, out, err = module.run_helm_command(cmd)
|
||||
return (len(out.strip()) > 0, out.strip())
|
||||
|
||||
|
||||
def default_check(release_status, chart_info, values=None, values_files=None):
|
||||
@@ -499,64 +634,71 @@ def default_check(release_status, chart_info, values=None, values_files=None):
|
||||
Use default check to determine if release would change by upgrading a chart.
|
||||
"""
|
||||
# the 'appVersion' specification is optional in a chart
|
||||
chart_app_version = chart_info.get('appVersion', None)
|
||||
released_app_version = release_status.get('app_version', None)
|
||||
chart_app_version = chart_info.get("appVersion", None)
|
||||
released_app_version = release_status.get("app_version", None)
|
||||
|
||||
# when deployed without an 'appVersion' chart value the 'helm list' command will return the entry `app_version: ""`
|
||||
appversion_is_same = (chart_app_version == released_app_version) or (chart_app_version is None and released_app_version == "")
|
||||
appversion_is_same = (chart_app_version == released_app_version) or (
|
||||
chart_app_version is None and released_app_version == ""
|
||||
)
|
||||
|
||||
if values_files:
|
||||
values_match = release_status['values'] == load_values_files(values_files)
|
||||
values_match = release_status["values"] == load_values_files(values_files)
|
||||
else:
|
||||
values_match = release_status['values'] == values
|
||||
return not values_match \
|
||||
or (chart_info['name'] + '-' + chart_info['version']) != release_status["chart"] \
|
||||
values_match = release_status["values"] == values
|
||||
return (
|
||||
not values_match
|
||||
or (chart_info["name"] + "-" + chart_info["version"]) != release_status["chart"]
|
||||
or not appversion_is_same
|
||||
)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
|
||||
arg_spec.update(
|
||||
dict(
|
||||
chart_ref=dict(type="path"),
|
||||
chart_repo_url=dict(type="str"),
|
||||
chart_version=dict(type="str"),
|
||||
dependency_update=dict(type="bool", default=False, aliases=["dep_up"]),
|
||||
release_name=dict(type="str", required=True, aliases=["name"]),
|
||||
release_namespace=dict(type="str", required=True, aliases=["namespace"]),
|
||||
release_state=dict(
|
||||
default="present", choices=["present", "absent"], aliases=["state"]
|
||||
),
|
||||
release_values=dict(type="dict", default={}, aliases=["values"]),
|
||||
values_files=dict(type="list", default=[], elements="str"),
|
||||
update_repo_cache=dict(type="bool", default=False),
|
||||
disable_hook=dict(type="bool", default=False),
|
||||
force=dict(type="bool", default=False),
|
||||
purge=dict(type="bool", default=True),
|
||||
wait=dict(type="bool", default=False),
|
||||
wait_timeout=dict(type="str"),
|
||||
timeout=dict(type="str"),
|
||||
atomic=dict(type="bool", default=False),
|
||||
create_namespace=dict(type="bool", default=False),
|
||||
post_renderer=dict(type="str"),
|
||||
replace=dict(type="bool", default=False),
|
||||
skip_crds=dict(type="bool", default=False),
|
||||
history_max=dict(type="int"),
|
||||
set_values=dict(type="list", elements="dict"),
|
||||
)
|
||||
)
|
||||
return arg_spec
|
||||
|
||||
|
||||
def main():
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
binary_path=dict(type='path'),
|
||||
chart_ref=dict(type='path'),
|
||||
chart_repo_url=dict(type='str'),
|
||||
chart_version=dict(type='str'),
|
||||
release_name=dict(type='str', required=True, aliases=['name']),
|
||||
release_namespace=dict(type='str', required=True, aliases=['namespace']),
|
||||
release_state=dict(default='present', choices=['present', 'absent'], aliases=['state']),
|
||||
release_values=dict(type='dict', default={}, aliases=['values']),
|
||||
values_files=dict(type='list', default=[], elements='str'),
|
||||
update_repo_cache=dict(type='bool', default=False),
|
||||
|
||||
# Helm options
|
||||
disable_hook=dict(type='bool', default=False),
|
||||
force=dict(type='bool', default=False),
|
||||
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
|
||||
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
|
||||
purge=dict(type='bool', default=True),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='str'),
|
||||
atomic=dict(type='bool', default=False),
|
||||
create_namespace=dict(type='bool', default=False),
|
||||
replace=dict(type='bool', default=False),
|
||||
skip_crds=dict(type='bool', default=False),
|
||||
history_max=dict(type='int'),
|
||||
|
||||
# Generic auth key
|
||||
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])),
|
||||
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])),
|
||||
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY']))
|
||||
),
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=argument_spec(),
|
||||
required_if=[
|
||||
('release_state', 'present', ['release_name', 'chart_ref']),
|
||||
('release_state', 'absent', ['release_name'])
|
||||
("release_state", "present", ["release_name", "chart_ref"]),
|
||||
("release_state", "absent", ["release_name"]),
|
||||
],
|
||||
mutually_exclusive=[
|
||||
("context", "ca_cert"),
|
||||
("kubeconfig", "ca_cert"),
|
||||
("replace", "history_max"),
|
||||
("wait_timeout", "timeout"),
|
||||
],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
@@ -566,46 +708,55 @@ def main():
|
||||
|
||||
changed = False
|
||||
|
||||
bin_path = module.params.get('binary_path')
|
||||
chart_ref = module.params.get('chart_ref')
|
||||
chart_repo_url = module.params.get('chart_repo_url')
|
||||
chart_version = module.params.get('chart_version')
|
||||
release_name = module.params.get('release_name')
|
||||
release_state = module.params.get('release_state')
|
||||
release_values = module.params.get('release_values')
|
||||
values_files = module.params.get('values_files')
|
||||
update_repo_cache = module.params.get('update_repo_cache')
|
||||
chart_ref = module.params.get("chart_ref")
|
||||
chart_repo_url = module.params.get("chart_repo_url")
|
||||
chart_version = module.params.get("chart_version")
|
||||
dependency_update = module.params.get("dependency_update")
|
||||
release_name = module.params.get("release_name")
|
||||
release_state = module.params.get("release_state")
|
||||
release_values = module.params.get("release_values")
|
||||
values_files = module.params.get("values_files")
|
||||
update_repo_cache = module.params.get("update_repo_cache")
|
||||
|
||||
# Helm options
|
||||
disable_hook = module.params.get('disable_hook')
|
||||
force = module.params.get('force')
|
||||
purge = module.params.get('purge')
|
||||
wait = module.params.get('wait')
|
||||
wait_timeout = module.params.get('wait_timeout')
|
||||
atomic = module.params.get('atomic')
|
||||
create_namespace = module.params.get('create_namespace')
|
||||
replace = module.params.get('replace')
|
||||
skip_crds = module.params.get('skip_crds')
|
||||
history_max = module.params.get('history_max')
|
||||
|
||||
if bin_path is not None:
|
||||
helm_cmd_common = bin_path
|
||||
else:
|
||||
helm_cmd_common = module.get_bin_path('helm', required=True)
|
||||
disable_hook = module.params.get("disable_hook")
|
||||
force = module.params.get("force")
|
||||
purge = module.params.get("purge")
|
||||
wait = module.params.get("wait")
|
||||
wait_timeout = module.params.get("wait_timeout")
|
||||
atomic = module.params.get("atomic")
|
||||
create_namespace = module.params.get("create_namespace")
|
||||
post_renderer = module.params.get("post_renderer")
|
||||
replace = module.params.get("replace")
|
||||
skip_crds = module.params.get("skip_crds")
|
||||
history_max = module.params.get("history_max")
|
||||
timeout = module.params.get("timeout")
|
||||
set_values = module.params.get("set_values")
|
||||
|
||||
if update_repo_cache:
|
||||
run_repo_update(module, helm_cmd_common)
|
||||
run_repo_update(module)
|
||||
|
||||
# Get real/deployed release status
|
||||
release_status = get_release_status(module, helm_cmd_common, release_name)
|
||||
release_status = get_release_status(module, release_name)
|
||||
|
||||
# keep helm_cmd_common for get_release_status in module_exit_json
|
||||
helm_cmd = helm_cmd_common
|
||||
helm_cmd = module.get_helm_binary()
|
||||
opt_result = {}
|
||||
if release_state == "absent" and release_status is not None:
|
||||
if replace:
|
||||
module.fail_json(msg="replace is not applicable when state is absent")
|
||||
|
||||
helm_cmd = delete(helm_cmd, release_name, purge, disable_hook)
|
||||
if wait:
|
||||
helm_version = module.get_helm_version()
|
||||
if LooseVersion(helm_version) < LooseVersion("3.7.0"):
|
||||
opt_result["warnings"] = []
|
||||
opt_result["warnings"].append(
|
||||
"helm uninstall support option --wait for helm release >= 3.7.0"
|
||||
)
|
||||
wait = False
|
||||
|
||||
helm_cmd = delete(
|
||||
helm_cmd, release_name, purge, disable_hook, wait, wait_timeout
|
||||
)
|
||||
changed = True
|
||||
elif release_state == "present":
|
||||
|
||||
@@ -618,67 +769,156 @@ def main():
|
||||
# Fetch chart info to have real version and real name for chart_ref from archive, folder or url
|
||||
chart_info = fetch_chart_info(module, helm_cmd, chart_ref)
|
||||
|
||||
if dependency_update:
|
||||
if chart_info.get("dependencies"):
|
||||
# Can't use '--dependency-update' with 'helm upgrade' that is the
|
||||
# default chart install method, so if chart_repo_url is defined
|
||||
# we can't use the dependency update command. But, in the near future
|
||||
# we can get rid of this method and use only '--dependency-update'
|
||||
# option. Please see https://github.com/helm/helm/pull/8810
|
||||
if not chart_repo_url and not re.fullmatch(
|
||||
r"^http[s]*://[\w.:/?&=-]+$", chart_ref
|
||||
):
|
||||
run_dep_update(module, chart_ref)
|
||||
|
||||
# To not add --dependency-update option in the deploy function
|
||||
dependency_update = False
|
||||
else:
|
||||
module.warn(
|
||||
"This is a not stable feature with 'chart_repo_url'. Please consider to use dependency update with on-disk charts"
|
||||
)
|
||||
if not replace:
|
||||
msg_fail = (
|
||||
"'--dependency-update' hasn't been supported yet with 'helm upgrade'. "
|
||||
"Please use 'helm install' instead by adding 'replace' option"
|
||||
)
|
||||
module.fail_json(msg=msg_fail)
|
||||
else:
|
||||
module.warn(
|
||||
"There is no dependencies block defined in Chart.yaml. Dependency update will not be performed. "
|
||||
"Please consider add dependencies block or disable dependency_update to remove this warning."
|
||||
)
|
||||
|
||||
if release_status is None: # Not installed
|
||||
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout,
|
||||
disable_hook, False, values_files=values_files, atomic=atomic,
|
||||
create_namespace=create_namespace, replace=replace,
|
||||
skip_crds=skip_crds, history_max=history_max)
|
||||
set_value_args = None
|
||||
if set_values:
|
||||
set_value_args = module.get_helm_set_values_args(set_values)
|
||||
|
||||
helm_cmd = deploy(
|
||||
helm_cmd,
|
||||
release_name,
|
||||
release_values,
|
||||
chart_ref,
|
||||
wait,
|
||||
wait_timeout,
|
||||
disable_hook,
|
||||
False,
|
||||
values_files=values_files,
|
||||
atomic=atomic,
|
||||
create_namespace=create_namespace,
|
||||
post_renderer=post_renderer,
|
||||
replace=replace,
|
||||
dependency_update=dependency_update,
|
||||
skip_crds=skip_crds,
|
||||
history_max=history_max,
|
||||
timeout=timeout,
|
||||
set_value_args=set_value_args,
|
||||
)
|
||||
changed = True
|
||||
|
||||
else:
|
||||
|
||||
if has_plugin(helm_cmd_common, "diff") and not chart_repo_url:
|
||||
would_change = helmdiff_check(module, helm_cmd_common, release_name, chart_ref,
|
||||
release_values, values_files, chart_version, replace)
|
||||
helm_diff_version = get_plugin_version("diff")
|
||||
if helm_diff_version and (
|
||||
not chart_repo_url
|
||||
or (
|
||||
chart_repo_url
|
||||
and LooseVersion(helm_diff_version) >= LooseVersion("3.4.1")
|
||||
)
|
||||
):
|
||||
(would_change, prepared) = helmdiff_check(
|
||||
module,
|
||||
release_name,
|
||||
chart_ref,
|
||||
release_values,
|
||||
values_files,
|
||||
chart_version,
|
||||
replace,
|
||||
chart_repo_url,
|
||||
)
|
||||
if would_change and module._diff:
|
||||
opt_result["diff"] = {"prepared": prepared}
|
||||
else:
|
||||
module.warn("The default idempotency check can fail to report changes in certain cases. "
|
||||
"Install helm diff for better results.")
|
||||
would_change = default_check(release_status, chart_info, release_values, values_files)
|
||||
module.warn(
|
||||
"The default idempotency check can fail to report changes in certain cases. "
|
||||
"Install helm diff >= 3.4.1 for better results."
|
||||
)
|
||||
would_change = default_check(
|
||||
release_status, chart_info, release_values, values_files
|
||||
)
|
||||
|
||||
if force or would_change:
|
||||
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout,
|
||||
disable_hook, force, values_files=values_files, atomic=atomic,
|
||||
create_namespace=create_namespace, replace=replace,
|
||||
skip_crds=skip_crds, history_max=history_max)
|
||||
set_value_args = None
|
||||
if set_values:
|
||||
set_value_args = module.get_helm_set_values_args(set_values)
|
||||
|
||||
helm_cmd = deploy(
|
||||
helm_cmd,
|
||||
release_name,
|
||||
release_values,
|
||||
chart_ref,
|
||||
wait,
|
||||
wait_timeout,
|
||||
disable_hook,
|
||||
force,
|
||||
values_files=values_files,
|
||||
atomic=atomic,
|
||||
create_namespace=create_namespace,
|
||||
post_renderer=post_renderer,
|
||||
replace=replace,
|
||||
skip_crds=skip_crds,
|
||||
history_max=history_max,
|
||||
timeout=timeout,
|
||||
dependency_update=dependency_update,
|
||||
set_value_args=set_value_args,
|
||||
)
|
||||
changed = True
|
||||
|
||||
if module.check_mode:
|
||||
check_status = {
|
||||
'values': {
|
||||
"current": {},
|
||||
"declared": {},
|
||||
}
|
||||
}
|
||||
check_status = {"values": {"current": {}, "declared": {}}}
|
||||
if release_status:
|
||||
check_status['values']['current'] = release_status['values']
|
||||
check_status['values']['declared'] = release_status
|
||||
check_status["values"]["current"] = release_status["values"]
|
||||
check_status["values"]["declared"] = release_status
|
||||
|
||||
module.exit_json(
|
||||
changed=changed,
|
||||
command=helm_cmd,
|
||||
status=check_status,
|
||||
stdout='',
|
||||
stderr='',
|
||||
stdout="",
|
||||
stderr="",
|
||||
**opt_result,
|
||||
)
|
||||
elif not changed:
|
||||
module.exit_json(
|
||||
changed=False,
|
||||
status=release_status,
|
||||
stdout='',
|
||||
stderr='',
|
||||
stdout="",
|
||||
stderr="",
|
||||
command=helm_cmd,
|
||||
**opt_result,
|
||||
)
|
||||
|
||||
rc, out, err = run_helm(module, helm_cmd)
|
||||
rc, out, err = module.run_helm_command(helm_cmd)
|
||||
|
||||
module.exit_json(
|
||||
changed=changed,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
status=get_release_status(module, helm_cmd_common, release_name),
|
||||
status=get_release_status(module, release_name),
|
||||
command=helm_cmd,
|
||||
**opt_result,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
# Copyright: (c) 2020, Ansible Project
|
||||
# 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'''
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: helm_info
|
||||
|
||||
@@ -38,18 +39,48 @@ options:
|
||||
required: true
|
||||
type: str
|
||||
aliases: [ namespace ]
|
||||
release_state:
|
||||
description:
|
||||
- Show releases as per their states.
|
||||
- Default value is C(deployed) and C(failed).
|
||||
- If set to C(all), show all releases without any filter applied.
|
||||
- If set to C(deployed), show deployed releases.
|
||||
- If set to C(failed), show failed releases.
|
||||
- If set to C(pending), show pending releases.
|
||||
- If set to C(superseded), show superseded releases.
|
||||
- If set to C(uninstalled), show uninstalled releases, if C(helm uninstall --keep-history) was used.
|
||||
- If set to C(uninstalling), show releases that are currently being uninstalled.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
version_added: "2.3.0"
|
||||
get_all_values:
|
||||
description:
|
||||
- Set to C(True) if you want to get all (computed) values of the release.
|
||||
- When C(False) (default), only user supplied values are returned.
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
version_added: "2.4.0"
|
||||
extends_documentation_fragment:
|
||||
- kubernetes.core.helm_common_options
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Deploy latest version of Grafana chart inside monitoring namespace
|
||||
EXAMPLES = r"""
|
||||
- name: Gather information of Grafana chart inside monitoring namespace
|
||||
kubernetes.core.helm_info:
|
||||
name: test
|
||||
release_namespace: monitoring
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
- name: Gather information about test-chart with pending state
|
||||
kubernetes.core.helm_info:
|
||||
name: test-chart
|
||||
release_namespace: testenv
|
||||
release_state:
|
||||
- pending
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
status:
|
||||
type: complex
|
||||
description: A dictionary of status output
|
||||
@@ -87,40 +118,83 @@ status:
|
||||
type: str
|
||||
returned: always
|
||||
description: Dict of Values used to deploy
|
||||
'''
|
||||
hooks:
|
||||
type: list
|
||||
elements: dict
|
||||
description: Hooks of the release
|
||||
returned: always
|
||||
version_added: "2.4.0"
|
||||
notes:
|
||||
type: str
|
||||
description: Notes of the release
|
||||
returned: always
|
||||
version_added: "2.4.0"
|
||||
manifest:
|
||||
type: list
|
||||
elements: dict
|
||||
description: Manifest of the release
|
||||
returned: always
|
||||
version_added: "2.4.0"
|
||||
"""
|
||||
|
||||
import traceback
|
||||
import copy
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
||||
IMP_YAML = True
|
||||
IMP_YAML_ERR = None
|
||||
except ImportError:
|
||||
IMP_YAML_ERR = traceback.format_exc()
|
||||
IMP_YAML = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm, get_values
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
AnsibleHelmModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import (
|
||||
HELM_AUTH_ARG_SPEC,
|
||||
HELM_AUTH_MUTUALLY_EXCLUSIVE,
|
||||
)
|
||||
|
||||
|
||||
# Get Release from all deployed releases
|
||||
def get_release(state, release_name):
|
||||
if state is not None:
|
||||
for release in state:
|
||||
if release['name'] == release_name:
|
||||
if release["name"] == release_name:
|
||||
return release
|
||||
return None
|
||||
|
||||
|
||||
# Get Release state from deployed release
|
||||
def get_release_status(module, command, release_name):
|
||||
list_command = command + " list --output=yaml --filter " + release_name
|
||||
def get_release_status(module, release_name, release_state, get_all_values=False):
|
||||
list_command = module.get_helm_binary() + " list --output=yaml"
|
||||
|
||||
rc, out, err = run_helm(module, list_command)
|
||||
valid_release_states = [
|
||||
"all",
|
||||
"deployed",
|
||||
"failed",
|
||||
"pending",
|
||||
"superseded",
|
||||
"uninstalled",
|
||||
"uninstalling",
|
||||
]
|
||||
|
||||
for local_release_state in release_state:
|
||||
if local_release_state in valid_release_states:
|
||||
list_command += " --%s" % local_release_state
|
||||
|
||||
list_command += " --filter " + release_name
|
||||
rc, out, err = module.run_helm_command(list_command)
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
|
||||
command=list_command
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
|
||||
rc, out, err
|
||||
),
|
||||
command=list_command,
|
||||
)
|
||||
|
||||
release = get_release(yaml.safe_load(out), release_name)
|
||||
@@ -128,51 +202,46 @@ def get_release_status(module, command, release_name):
|
||||
if release is None: # not install
|
||||
return None
|
||||
|
||||
release['values'] = get_values(module, command, release_name)
|
||||
release["values"] = module.get_values(release_name, get_all_values)
|
||||
release["manifest"] = module.get_manifest(release_name)
|
||||
release["notes"] = module.get_notes(release_name)
|
||||
release["hooks"] = module.get_hooks(release_name)
|
||||
|
||||
return release
|
||||
|
||||
|
||||
def argument_spec():
|
||||
arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
|
||||
arg_spec.update(
|
||||
dict(
|
||||
release_name=dict(type="str", required=True, aliases=["name"]),
|
||||
release_namespace=dict(type="str", required=True, aliases=["namespace"]),
|
||||
release_state=dict(type="list", default=[], elements="str"),
|
||||
get_all_values=dict(type="bool", required=False, default=False),
|
||||
)
|
||||
)
|
||||
return arg_spec
|
||||
|
||||
|
||||
def main():
|
||||
global module
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
binary_path=dict(type='path'),
|
||||
release_name=dict(type='str', required=True, aliases=['name']),
|
||||
release_namespace=dict(type='str', required=True, aliases=['namespace']),
|
||||
|
||||
# Helm options
|
||||
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
|
||||
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
|
||||
|
||||
# Generic auth key
|
||||
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])),
|
||||
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])),
|
||||
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY']))
|
||||
),
|
||||
mutually_exclusive=[
|
||||
("context", "ca_cert"),
|
||||
("context", "validate_certs"),
|
||||
("kubeconfig", "ca_cert"),
|
||||
("kubeconfig", "validate_certs")
|
||||
],
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=argument_spec(),
|
||||
mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not IMP_YAML:
|
||||
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
|
||||
|
||||
bin_path = module.params.get('binary_path')
|
||||
release_name = module.params.get('release_name')
|
||||
release_name = module.params.get("release_name")
|
||||
release_state = module.params.get("release_state")
|
||||
get_all_values = module.params.get("get_all_values")
|
||||
|
||||
if bin_path is not None:
|
||||
helm_cmd_common = bin_path
|
||||
else:
|
||||
helm_cmd_common = module.get_bin_path('helm', required=True)
|
||||
|
||||
release_status = get_release_status(module, helm_cmd_common, release_name)
|
||||
release_status = get_release_status(
|
||||
module, release_name, release_state, get_all_values
|
||||
)
|
||||
|
||||
if release_status is not None:
|
||||
module.exit_json(changed=False, status=release_status)
|
||||
@@ -180,5 +249,5 @@ def main():
|
||||
module.exit_json(changed=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: helm_plugin
|
||||
short_description: Manage Helm plugins
|
||||
@@ -24,14 +24,15 @@ options:
|
||||
state:
|
||||
description:
|
||||
- If C(state=present) the Helm plugin will be installed.
|
||||
- If C(state=latest) the Helm plugin will be updated. Added in version 2.3.0.
|
||||
- If C(state=absent) the Helm plugin will be removed.
|
||||
choices: [ absent, present ]
|
||||
choices: [ absent, present, latest ]
|
||||
default: present
|
||||
type: str
|
||||
plugin_name:
|
||||
description:
|
||||
- Name of Helm plugin.
|
||||
- Required only if C(state=absent).
|
||||
- Required only if C(state=absent) or C(state=latest).
|
||||
type: str
|
||||
plugin_path:
|
||||
description:
|
||||
@@ -40,11 +41,18 @@ options:
|
||||
machine and not on Ansible controller.
|
||||
- Required only if C(state=present).
|
||||
type: str
|
||||
plugin_version:
|
||||
description:
|
||||
- Plugin version to install. If this is not specified, the latest version is installed.
|
||||
- Ignored when C(state=absent) or C(state=latest).
|
||||
required: false
|
||||
type: str
|
||||
version_added: "2.3.0"
|
||||
extends_documentation_fragment:
|
||||
- kubernetes.core.helm_common_options
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Install Helm env plugin
|
||||
kubernetes.core.helm_plugin:
|
||||
plugin_path: https://github.com/adamreese/helm-env
|
||||
@@ -59,9 +67,20 @@ EXAMPLES = r'''
|
||||
kubernetes.core.helm_plugin:
|
||||
plugin_name: env
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
- name: Install Helm plugin with a specific version
|
||||
kubernetes.core.helm_plugin:
|
||||
plugin_version: 2.0.1
|
||||
plugin_path: https://domain/path/to/plugin.tar.gz
|
||||
state: present
|
||||
|
||||
- name: Update Helm plugin
|
||||
kubernetes.core.helm_plugin:
|
||||
plugin_name: secrets
|
||||
state: latest
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
stdout:
|
||||
type: str
|
||||
description: Full `helm` command stdout, in case you want to display it or examine the event log
|
||||
@@ -87,67 +106,77 @@ rc:
|
||||
description: Helm plugin command return code
|
||||
returned: always
|
||||
sample: 1
|
||||
'''
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
import copy
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
run_helm,
|
||||
get_helm_plugin_list,
|
||||
parse_helm_plugin_list
|
||||
AnsibleHelmModule,
|
||||
parse_helm_plugin_list,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import (
|
||||
HELM_AUTH_ARG_SPEC,
|
||||
HELM_AUTH_MUTUALLY_EXCLUSIVE,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
binary_path=dict(type='path'),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
plugin_path=dict(type='str',),
|
||||
plugin_name=dict(type='str',),
|
||||
# Helm options
|
||||
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
|
||||
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
|
||||
def argument_spec():
|
||||
arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
|
||||
arg_spec.update(
|
||||
dict(
|
||||
plugin_path=dict(
|
||||
type="str",
|
||||
),
|
||||
plugin_name=dict(
|
||||
type="str",
|
||||
),
|
||||
plugin_version=dict(
|
||||
type="str",
|
||||
),
|
||||
state=dict(
|
||||
type="str",
|
||||
default="present",
|
||||
choices=["present", "absent", "latest"],
|
||||
),
|
||||
)
|
||||
)
|
||||
return arg_spec
|
||||
|
||||
# Generic auth key
|
||||
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])),
|
||||
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])),
|
||||
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY']))
|
||||
),
|
||||
|
||||
def mutually_exclusive():
|
||||
mutually_ex = copy.deepcopy(HELM_AUTH_MUTUALLY_EXCLUSIVE)
|
||||
mutually_ex.append(("plugin_name", "plugin_path"))
|
||||
return mutually_ex
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=argument_spec(),
|
||||
supports_check_mode=True,
|
||||
required_if=[
|
||||
("state", "present", ("plugin_path",)),
|
||||
("state", "absent", ("plugin_name",)),
|
||||
("state", "latest", ("plugin_name",)),
|
||||
],
|
||||
mutually_exclusive=[
|
||||
('plugin_name', 'plugin_path'),
|
||||
("context", "ca_cert"),
|
||||
("context", "validate_certs"),
|
||||
("kubeconfig", "ca_cert"),
|
||||
("kubeconfig", "validate_certs")
|
||||
],
|
||||
mutually_exclusive=mutually_exclusive(),
|
||||
)
|
||||
|
||||
bin_path = module.params.get('binary_path')
|
||||
state = module.params.get('state')
|
||||
state = module.params.get("state")
|
||||
|
||||
if bin_path is not None:
|
||||
helm_cmd_common = bin_path
|
||||
else:
|
||||
helm_cmd_common = 'helm'
|
||||
helm_cmd_common = module.get_helm_binary() + " plugin"
|
||||
|
||||
helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True)
|
||||
|
||||
helm_cmd_common += " plugin"
|
||||
|
||||
if state == 'present':
|
||||
helm_cmd_common += " install %s" % module.params.get('plugin_path')
|
||||
if state == "present":
|
||||
helm_cmd_common += " install %s" % module.params.get("plugin_path")
|
||||
plugin_version = module.params.get("plugin_version")
|
||||
if plugin_version is not None:
|
||||
helm_cmd_common += " --version=%s" % plugin_version
|
||||
if not module.check_mode:
|
||||
rc, out, err = run_helm(module, helm_cmd_common, fails_on_error=False)
|
||||
rc, out, err = module.run_helm_command(
|
||||
helm_cmd_common, fails_on_error=False
|
||||
)
|
||||
else:
|
||||
rc, out, err = (0, '', '')
|
||||
rc, out, err = (0, "", "")
|
||||
|
||||
if rc == 1 and 'plugin already exists' in err:
|
||||
if rc == 1 and "plugin already exists" in err:
|
||||
module.exit_json(
|
||||
failed=False,
|
||||
changed=False,
|
||||
@@ -155,7 +184,7 @@ def main():
|
||||
command=helm_cmd_common,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc
|
||||
rc=rc,
|
||||
)
|
||||
elif rc == 0:
|
||||
module.exit_json(
|
||||
@@ -175,20 +204,20 @@ def main():
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
elif state == 'absent':
|
||||
plugin_name = module.params.get('plugin_name')
|
||||
rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common)
|
||||
out = parse_helm_plugin_list(module, output=output.splitlines())
|
||||
elif state == "absent":
|
||||
plugin_name = module.params.get("plugin_name")
|
||||
rc, output, err, command = module.get_helm_plugin_list()
|
||||
out = parse_helm_plugin_list(output=output.splitlines())
|
||||
|
||||
if not out:
|
||||
module.exit_json(
|
||||
failed=False,
|
||||
changed=False,
|
||||
msg="Plugin not found or is already uninstalled",
|
||||
command=helm_cmd_common + " list",
|
||||
command=command,
|
||||
stdout=output,
|
||||
stderr=err,
|
||||
rc=rc
|
||||
rc=rc,
|
||||
)
|
||||
|
||||
found = False
|
||||
@@ -201,17 +230,19 @@ def main():
|
||||
failed=False,
|
||||
changed=False,
|
||||
msg="Plugin not found or is already uninstalled",
|
||||
command=helm_cmd_common + " list",
|
||||
command=command,
|
||||
stdout=output,
|
||||
stderr=err,
|
||||
rc=rc
|
||||
rc=rc,
|
||||
)
|
||||
|
||||
helm_uninstall_cmd = "%s uninstall %s" % (helm_cmd_common, plugin_name)
|
||||
if not module.check_mode:
|
||||
rc, out, err = run_helm(module, helm_uninstall_cmd, fails_on_error=False)
|
||||
rc, out, err = module.run_helm_command(
|
||||
helm_uninstall_cmd, fails_on_error=False
|
||||
)
|
||||
else:
|
||||
rc, out, err = (0, '', '')
|
||||
rc, out, err = (0, "", "")
|
||||
|
||||
if rc == 0:
|
||||
module.exit_json(
|
||||
@@ -220,7 +251,7 @@ def main():
|
||||
command=helm_uninstall_cmd,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc
|
||||
rc=rc,
|
||||
)
|
||||
module.fail_json(
|
||||
msg="Failed to get Helm plugin uninstall",
|
||||
@@ -229,7 +260,63 @@ def main():
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
elif state == "latest":
|
||||
plugin_name = module.params.get("plugin_name")
|
||||
rc, output, err, command = module.get_helm_plugin_list()
|
||||
out = parse_helm_plugin_list(output=output.splitlines())
|
||||
|
||||
if not out:
|
||||
module.exit_json(
|
||||
failed=False,
|
||||
changed=False,
|
||||
msg="Plugin not found",
|
||||
command=command,
|
||||
stdout=output,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
|
||||
found = False
|
||||
for line in out:
|
||||
if line[0] == plugin_name:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
module.exit_json(
|
||||
failed=False,
|
||||
changed=False,
|
||||
msg="Plugin not found",
|
||||
command=command,
|
||||
stdout=output,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
|
||||
helm_update_cmd = "%s update %s" % (helm_cmd_common, plugin_name)
|
||||
if not module.check_mode:
|
||||
rc, out, err = module.run_helm_command(
|
||||
helm_update_cmd, fails_on_error=False
|
||||
)
|
||||
else:
|
||||
rc, out, err = (0, "", "")
|
||||
|
||||
if rc == 0:
|
||||
module.exit_json(
|
||||
changed=True,
|
||||
msg="Plugin updated successfully",
|
||||
command=helm_update_cmd,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
module.fail_json(
|
||||
msg="Failed to get Helm plugin update",
|
||||
command=helm_update_cmd,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: helm_plugin_info
|
||||
short_description: Gather information about Helm plugins
|
||||
@@ -27,18 +27,18 @@ options:
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- kubernetes.core.helm_common_options
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Gather Helm plugin info
|
||||
kubernetes.core.helm_plugin_info:
|
||||
|
||||
- name: Gather Helm env plugin info
|
||||
kubernetes.core.helm_plugin_info:
|
||||
plugin_name: env
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
stdout:
|
||||
type: str
|
||||
description: Full `helm` command stdout, in case you want to display it or examine the event log
|
||||
@@ -68,82 +68,60 @@ rc:
|
||||
description: Helm plugin command return code
|
||||
returned: always
|
||||
sample: 1
|
||||
'''
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
import copy
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
get_helm_plugin_list,
|
||||
parse_helm_plugin_list,
|
||||
AnsibleHelmModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import (
|
||||
HELM_AUTH_ARG_SPEC,
|
||||
HELM_AUTH_MUTUALLY_EXCLUSIVE,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
binary_path=dict(type='path'),
|
||||
plugin_name=dict(type='str',),
|
||||
# Helm options
|
||||
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
|
||||
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
|
||||
|
||||
# Generic auth key
|
||||
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])),
|
||||
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])),
|
||||
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY']))
|
||||
),
|
||||
mutually_exclusive=[
|
||||
("context", "ca_cert"),
|
||||
("context", "validate_certs"),
|
||||
("kubeconfig", "ca_cert"),
|
||||
("kubeconfig", "validate_certs")
|
||||
],
|
||||
argument_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
|
||||
argument_spec.update(
|
||||
dict(
|
||||
plugin_name=dict(
|
||||
type="str",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
bin_path = module.params.get('binary_path')
|
||||
|
||||
if bin_path is not None:
|
||||
helm_cmd_common = bin_path
|
||||
else:
|
||||
helm_cmd_common = 'helm'
|
||||
|
||||
helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True)
|
||||
|
||||
helm_cmd_common += " plugin"
|
||||
|
||||
plugin_name = module.params.get('plugin_name')
|
||||
plugin_name = module.params.get("plugin_name")
|
||||
|
||||
plugin_list = []
|
||||
|
||||
rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common)
|
||||
rc, output, err, command = module.get_helm_plugin_list()
|
||||
|
||||
out = parse_helm_plugin_list(module, output=output.splitlines())
|
||||
out = parse_helm_plugin_list(output=output.splitlines())
|
||||
|
||||
for line in out:
|
||||
if plugin_name is None:
|
||||
plugin_list.append(
|
||||
{
|
||||
"name": line[0],
|
||||
"version": line[1],
|
||||
"description": line[2],
|
||||
}
|
||||
{"name": line[0], "version": line[1], "description": line[2]}
|
||||
)
|
||||
continue
|
||||
|
||||
if plugin_name == line[0]:
|
||||
plugin_list.append(
|
||||
{
|
||||
"name": line[0],
|
||||
"version": line[1],
|
||||
"description": line[2],
|
||||
}
|
||||
{"name": line[0], "version": line[1], "description": line[2]}
|
||||
)
|
||||
break
|
||||
|
||||
module.exit_json(
|
||||
changed=True,
|
||||
command=helm_cmd_common + " list",
|
||||
command=command,
|
||||
stdout=output,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
@@ -151,5 +129,5 @@ def main():
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
302
plugins/modules/helm_pull.py
Normal file
302
plugins/modules/helm_pull.py
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2022, Ansible Project
|
||||
# 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_pull
|
||||
short_description: download a chart from a repository and (optionally) unpack it in local directory.
|
||||
version_added: "2.4.0"
|
||||
author:
|
||||
- Aubin Bikouo (@abikouo)
|
||||
description:
|
||||
- Retrieve a package from a package repository, and download it locally.
|
||||
- It can also be used to perform cryptographic verification of a chart without installing the chart.
|
||||
- There are options for unpacking the chart after download.
|
||||
|
||||
requirements:
|
||||
- "helm >= 3.0 (https://github.com/helm/helm/releases)"
|
||||
|
||||
options:
|
||||
chart_ref:
|
||||
description:
|
||||
- chart name on chart repository.
|
||||
- absolute URL.
|
||||
required: true
|
||||
type: str
|
||||
chart_version:
|
||||
description:
|
||||
- Specify a version constraint for the chart version to use.
|
||||
- This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0).
|
||||
- Mutually exclusive with C(chart_devel).
|
||||
type: str
|
||||
verify_chart:
|
||||
description:
|
||||
- Verify the package before using it.
|
||||
default: False
|
||||
type: bool
|
||||
verify_chart_keyring:
|
||||
description:
|
||||
- location of public keys used for verification.
|
||||
type: path
|
||||
provenance:
|
||||
description:
|
||||
- Fetch the provenance file, but don't perform verification.
|
||||
type: bool
|
||||
default: False
|
||||
repo_url:
|
||||
description:
|
||||
- chart repository url where to locate the requested chart.
|
||||
type: str
|
||||
aliases: [ url, chart_repo_url ]
|
||||
repo_username:
|
||||
description:
|
||||
- Chart repository username where to locate the requested chart.
|
||||
- Required if C(repo_password) is specified.
|
||||
type: str
|
||||
aliases: [ username, chart_repo_username ]
|
||||
repo_password:
|
||||
description:
|
||||
- Chart repository password where to locate the requested chart.
|
||||
- Required if C(repo_username) is specified.
|
||||
type: str
|
||||
aliases: [ password, chart_repo_password ]
|
||||
pass_credentials:
|
||||
description:
|
||||
- Pass credentials to all domains.
|
||||
default: False
|
||||
type: bool
|
||||
skip_tls_certs_check:
|
||||
description:
|
||||
- Whether or not to check tls certificate for the chart download.
|
||||
- Requires helm >= 3.3.0.
|
||||
type: bool
|
||||
default: False
|
||||
chart_devel:
|
||||
description:
|
||||
- Use development versions, too. Equivalent to version '>0.0.0-0'.
|
||||
- Mutually exclusive with C(chart_version).
|
||||
type: bool
|
||||
untar_chart:
|
||||
description:
|
||||
- if set to true, will untar the chart after downloading it.
|
||||
type: bool
|
||||
default: False
|
||||
destination:
|
||||
description:
|
||||
- location to write the chart.
|
||||
type: path
|
||||
required: True
|
||||
chart_ca_cert:
|
||||
description:
|
||||
- Verify certificates of HTTPS-enabled servers using this CA bundle.
|
||||
- Requires helm >= 3.1.0.
|
||||
type: path
|
||||
chart_ssl_cert_file:
|
||||
description:
|
||||
- Identify HTTPS client using this SSL certificate file.
|
||||
- Requires helm >= 3.1.0.
|
||||
type: path
|
||||
chart_ssl_key_file:
|
||||
description:
|
||||
- Identify HTTPS client using this SSL key file
|
||||
- Requires helm >= 3.1.0.
|
||||
type: path
|
||||
binary_path:
|
||||
description:
|
||||
- The path of a helm binary to use.
|
||||
required: false
|
||||
type: path
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Download chart using chart url
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz
|
||||
destination: /path/to/chart
|
||||
|
||||
- name: Download Chart using chart_name and repo_url
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: redis
|
||||
repo_url: https://charts.bitnami.com/bitnami
|
||||
untar_chart: yes
|
||||
destination: /path/to/chart
|
||||
|
||||
- name: Download Chart (skip tls certificate check)
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: redis
|
||||
repo_url: https://charts.bitnami.com/bitnami
|
||||
untar_chart: yes
|
||||
destination: /path/to/chart
|
||||
skip_tls_certs_check: yes
|
||||
|
||||
- name: Download Chart using chart registry credentials
|
||||
kubernetes.core.helm_pull:
|
||||
chart_ref: redis
|
||||
repo_url: https://charts.bitnami.com/bitnami
|
||||
untar_chart: yes
|
||||
destination: /path/to/chart
|
||||
username: myuser
|
||||
password: mypassword123
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
stdout:
|
||||
type: str
|
||||
description: Full `helm pull` command stdout, in case you want to display it or examine the event log
|
||||
returned: always
|
||||
sample: ''
|
||||
stderr:
|
||||
type: str
|
||||
description: Full `helm pull` command stderr, in case you want to display it or examine the event log
|
||||
returned: always
|
||||
sample: ''
|
||||
command:
|
||||
type: str
|
||||
description: Full `helm pull` command built by this module, in case you want to re-run the command outside the module or debug a problem.
|
||||
returned: always
|
||||
sample: helm pull --repo test ...
|
||||
rc:
|
||||
type: int
|
||||
description: Helm pull command return code
|
||||
returned: always
|
||||
sample: 1
|
||||
"""
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
AnsibleHelmModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argspec = dict(
|
||||
chart_ref=dict(type="str", required=True),
|
||||
chart_version=dict(type="str"),
|
||||
verify_chart=dict(type="bool", default=False),
|
||||
verify_chart_keyring=dict(type="path"),
|
||||
provenance=dict(type="bool", default=False),
|
||||
repo_url=dict(type="str", aliases=["url", "chart_repo_url"]),
|
||||
repo_username=dict(type="str", aliases=["username", "chart_repo_username"]),
|
||||
repo_password=dict(
|
||||
type="str", no_log=True, aliases=["password", "chart_repo_password"]
|
||||
),
|
||||
pass_credentials=dict(type="bool", default=False),
|
||||
skip_tls_certs_check=dict(type="bool", default=False),
|
||||
chart_devel=dict(type="bool"),
|
||||
untar_chart=dict(type="bool", default=False),
|
||||
destination=dict(type="path", required=True),
|
||||
chart_ca_cert=dict(type="path"),
|
||||
chart_ssl_cert_file=dict(type="path"),
|
||||
chart_ssl_key_file=dict(type="path"),
|
||||
binary_path=dict(type="path"),
|
||||
)
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=argspec,
|
||||
supports_check_mode=True,
|
||||
required_by=dict(
|
||||
repo_username=("repo_password"),
|
||||
repo_password=("repo_username"),
|
||||
),
|
||||
mutually_exclusive=[("chart_version", "chart_devel")],
|
||||
)
|
||||
|
||||
helm_version = module.get_helm_version()
|
||||
if LooseVersion(helm_version) < LooseVersion("3.0.0"):
|
||||
module.fail_json(
|
||||
msg="This module requires helm >= 3.0.0, current version is {0}".format(
|
||||
helm_version
|
||||
)
|
||||
)
|
||||
|
||||
helm_pull_opt_versionning = dict(
|
||||
skip_tls_certs_check="3.3.0",
|
||||
chart_ca_cert="3.1.0",
|
||||
chart_ssl_cert_file="3.1.0",
|
||||
chart_ssl_key_file="3.1.0",
|
||||
)
|
||||
|
||||
def test_version_requirement(opt):
|
||||
req_version = helm_pull_opt_versionning.get(opt)
|
||||
if req_version and LooseVersion(helm_version) < LooseVersion(req_version):
|
||||
module.fail_json(
|
||||
msg="Parameter {0} requires helm >= {1}, current version is {2}".format(
|
||||
opt, req_version, helm_version
|
||||
)
|
||||
)
|
||||
|
||||
# Set `helm pull` arguments requiring values
|
||||
helm_pull_opts = []
|
||||
|
||||
helm_value_args = dict(
|
||||
chart_version="version",
|
||||
verify_chart_keyring="keyring",
|
||||
repo_url="repo",
|
||||
repo_username="username",
|
||||
repo_password="password",
|
||||
destination="destination",
|
||||
chart_ca_cert="ca-file",
|
||||
chart_ssl_cert_file="cert-file",
|
||||
chart_ssl_key_file="key-file",
|
||||
)
|
||||
|
||||
for opt, cmdkey in helm_value_args.items():
|
||||
if module.params.get(opt):
|
||||
test_version_requirement(opt)
|
||||
helm_pull_opts.append("--{0} {1}".format(cmdkey, module.params.get(opt)))
|
||||
|
||||
# Set `helm pull` arguments flags
|
||||
helm_flag_args = dict(
|
||||
verify_chart=dict(key="verify"),
|
||||
provenance=dict(key="prov"),
|
||||
pass_credentials=dict(key="pass-credentials"),
|
||||
skip_tls_certs_check=dict(key="insecure-skip-tls-verify"),
|
||||
chart_devel=dict(key="devel"),
|
||||
untar_chart=dict(key="untar"),
|
||||
)
|
||||
|
||||
for k, v in helm_flag_args.items():
|
||||
if module.params.get(k):
|
||||
test_version_requirement(k)
|
||||
helm_pull_opts.append("--{0}".format(v["key"]))
|
||||
|
||||
helm_cmd_common = "{0} pull {1} {2}".format(
|
||||
module.get_helm_binary(),
|
||||
module.params.get("chart_ref"),
|
||||
" ".join(helm_pull_opts),
|
||||
)
|
||||
if not module.check_mode:
|
||||
rc, out, err = module.run_helm_command(helm_cmd_common, fails_on_error=False)
|
||||
else:
|
||||
rc, out, err = (0, "", "")
|
||||
|
||||
if rc == 0:
|
||||
module.exit_json(
|
||||
failed=False,
|
||||
changed=True,
|
||||
command=helm_cmd_common,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
else:
|
||||
module.fail_json(
|
||||
msg="Failure when executing Helm command.",
|
||||
command=helm_cmd_common,
|
||||
changed=False,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,10 +4,11 @@
|
||||
# Copyright: (c) 2020, Ansible Project
|
||||
# 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'''
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: helm_repository
|
||||
|
||||
@@ -64,9 +65,63 @@ options:
|
||||
default: present
|
||||
aliases: [ state ]
|
||||
type: str
|
||||
'''
|
||||
pass_credentials:
|
||||
description:
|
||||
- Pass credentials to all domains.
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 2.3.0
|
||||
host:
|
||||
description:
|
||||
- Provide a URL for accessing the API. Can also be specified via C(K8S_AUTH_HOST) environment variable.
|
||||
type: str
|
||||
version_added: "2.3.0"
|
||||
api_key:
|
||||
description:
|
||||
- Token used to authenticate with the API. Can also be specified via C(K8S_AUTH_API_KEY) environment variable.
|
||||
type: str
|
||||
version_added: "2.3.0"
|
||||
validate_certs:
|
||||
description:
|
||||
- Whether or not to verify the API server's SSL certificates. Can also be specified via C(K8S_AUTH_VERIFY_SSL)
|
||||
environment variable.
|
||||
type: bool
|
||||
aliases: [ verify_ssl ]
|
||||
default: True
|
||||
version_added: "2.3.0"
|
||||
ca_cert:
|
||||
description:
|
||||
- Path to a CA certificate used to authenticate with the API. The full certificate chain must be provided to
|
||||
avoid certificate validation errors. Can also be specified via C(K8S_AUTH_SSL_CA_CERT) environment variable.
|
||||
type: path
|
||||
aliases: [ ssl_ca_cert ]
|
||||
version_added: "2.3.0"
|
||||
context:
|
||||
description:
|
||||
- Helm option to specify which kubeconfig context to use.
|
||||
- If the value is not specified in the task, the value of environment variable C(K8S_AUTH_CONTEXT) will be used instead.
|
||||
type: str
|
||||
aliases: [ kube_context ]
|
||||
version_added: "2.4.0"
|
||||
kubeconfig:
|
||||
description:
|
||||
- Helm option to specify kubeconfig path to use.
|
||||
- If the value is not specified in the task, the value of environment variable C(K8S_AUTH_KUBECONFIG) will be used instead.
|
||||
- The configuration can be provided as dictionary.
|
||||
type: raw
|
||||
aliases: [ kubeconfig_path ]
|
||||
version_added: "2.4.0"
|
||||
force_update:
|
||||
description:
|
||||
- Whether or not to replace (overwrite) the repo if it already exists.
|
||||
type: bool
|
||||
aliases: [ force ]
|
||||
default: False
|
||||
version_added: "2.4.0"
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Add a repository
|
||||
kubernetes.core.helm_repository:
|
||||
name: stable
|
||||
@@ -76,9 +131,9 @@ EXAMPLES = r'''
|
||||
kubernetes.core.helm_repository:
|
||||
name: redhat-charts
|
||||
repo_url: https://redhat-developer.github.com/redhat-helm-charts
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
stdout:
|
||||
type: str
|
||||
description: Full `helm` command stdout, in case you want to display it or examine the event log
|
||||
@@ -109,56 +164,81 @@ msg:
|
||||
description: Error message returned by `helm` command
|
||||
returned: on failure
|
||||
sample: 'Repository already have a repository named bitnami'
|
||||
'''
|
||||
"""
|
||||
|
||||
import traceback
|
||||
import copy
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
||||
IMP_YAML = True
|
||||
IMP_YAML_ERR = None
|
||||
except ImportError:
|
||||
IMP_YAML_ERR = traceback.format_exc()
|
||||
IMP_YAML = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
AnsibleHelmModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import (
|
||||
HELM_AUTH_ARG_SPEC,
|
||||
HELM_AUTH_MUTUALLY_EXCLUSIVE,
|
||||
)
|
||||
|
||||
|
||||
# Get repository from all repositories added
|
||||
def get_repository(state, repo_name):
|
||||
if state is not None:
|
||||
for repository in state:
|
||||
if repository['name'] == repo_name:
|
||||
if repository["name"] == repo_name:
|
||||
return repository
|
||||
return None
|
||||
|
||||
|
||||
# Get repository status
|
||||
def get_repository_status(module, command, repository_name):
|
||||
list_command = command + " repo list --output=yaml"
|
||||
def get_repository_status(module, repository_name):
|
||||
list_command = module.get_helm_binary() + " repo list --output=yaml"
|
||||
|
||||
rc, out, err = run_helm(module, list_command, fails_on_error=False)
|
||||
rc, out, err = module.run_helm_command(list_command, fails_on_error=False)
|
||||
|
||||
# no repo => rc=1 and 'no repositories to show' in output
|
||||
if rc == 1 and "no repositories to show" in err:
|
||||
return None
|
||||
elif rc != 0:
|
||||
module.fail_json(
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
|
||||
command=list_command
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
|
||||
rc, out, err
|
||||
),
|
||||
command=list_command,
|
||||
)
|
||||
|
||||
return get_repository(yaml.safe_load(out), repository_name)
|
||||
|
||||
|
||||
# Install repository
|
||||
def install_repository(command, repository_name, repository_url, repository_username, repository_password):
|
||||
def install_repository(
|
||||
command,
|
||||
repository_name,
|
||||
repository_url,
|
||||
repository_username,
|
||||
repository_password,
|
||||
pass_credentials,
|
||||
force_update,
|
||||
):
|
||||
install_command = command + " repo add " + repository_name + " " + repository_url
|
||||
|
||||
if repository_username is not None and repository_password is not None:
|
||||
install_command += " --username=" + repository_username
|
||||
install_command += " --password=" + repository_password
|
||||
|
||||
if pass_credentials:
|
||||
install_command += " --pass-credentials"
|
||||
|
||||
if force_update:
|
||||
install_command += " --force-update"
|
||||
|
||||
return install_command
|
||||
|
||||
|
||||
@@ -169,24 +249,32 @@ def delete_repository(command, repository_name):
|
||||
return remove_command
|
||||
|
||||
|
||||
def argument_spec():
|
||||
arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
|
||||
arg_spec.update(
|
||||
dict(
|
||||
repo_name=dict(type="str", aliases=["name"], required=True),
|
||||
repo_url=dict(type="str", aliases=["url"]),
|
||||
repo_username=dict(type="str", aliases=["username"]),
|
||||
repo_password=dict(type="str", aliases=["password"], no_log=True),
|
||||
repo_state=dict(
|
||||
default="present", choices=["present", "absent"], aliases=["state"]
|
||||
),
|
||||
pass_credentials=dict(type="bool", default=False, no_log=True),
|
||||
force_update=dict(type="bool", default=False, aliases=["force"]),
|
||||
)
|
||||
)
|
||||
return arg_spec
|
||||
|
||||
|
||||
def main():
|
||||
global module
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
binary_path=dict(type='path'),
|
||||
repo_name=dict(type='str', aliases=['name'], required=True),
|
||||
repo_url=dict(type='str', aliases=['url']),
|
||||
repo_username=dict(type='str', aliases=['username']),
|
||||
repo_password=dict(type='str', aliases=['password'], no_log=True),
|
||||
repo_state=dict(default='present', choices=['present', 'absent'], aliases=['state']),
|
||||
),
|
||||
required_together=[
|
||||
['repo_username', 'repo_password']
|
||||
],
|
||||
required_if=[
|
||||
('repo_state', 'present', ['repo_url']),
|
||||
],
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=argument_spec(),
|
||||
required_together=[["repo_username", "repo_password"]],
|
||||
required_if=[("repo_state", "present", ["repo_url"])],
|
||||
mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
@@ -195,48 +283,58 @@ def main():
|
||||
|
||||
changed = False
|
||||
|
||||
bin_path = module.params.get('binary_path')
|
||||
repo_name = module.params.get('repo_name')
|
||||
repo_url = module.params.get('repo_url')
|
||||
repo_username = module.params.get('repo_username')
|
||||
repo_password = module.params.get('repo_password')
|
||||
repo_state = module.params.get('repo_state')
|
||||
repo_name = module.params.get("repo_name")
|
||||
repo_url = module.params.get("repo_url")
|
||||
repo_username = module.params.get("repo_username")
|
||||
repo_password = module.params.get("repo_password")
|
||||
repo_state = module.params.get("repo_state")
|
||||
pass_credentials = module.params.get("pass_credentials")
|
||||
force_update = module.params.get("force_update")
|
||||
|
||||
if bin_path is not None:
|
||||
helm_cmd = bin_path
|
||||
else:
|
||||
helm_cmd = module.get_bin_path('helm', required=True)
|
||||
helm_cmd = module.get_helm_binary()
|
||||
|
||||
repository_status = get_repository_status(module, helm_cmd, repo_name)
|
||||
repository_status = get_repository_status(module, repo_name)
|
||||
|
||||
if repo_state == "absent" and repository_status is not None:
|
||||
helm_cmd = delete_repository(helm_cmd, repo_name)
|
||||
changed = True
|
||||
elif repo_state == "present":
|
||||
if repository_status is None:
|
||||
helm_cmd = install_repository(helm_cmd, repo_name, repo_url, repo_username, repo_password)
|
||||
if repository_status is None or force_update:
|
||||
helm_cmd = install_repository(
|
||||
helm_cmd,
|
||||
repo_name,
|
||||
repo_url,
|
||||
repo_username,
|
||||
repo_password,
|
||||
pass_credentials,
|
||||
force_update,
|
||||
)
|
||||
changed = True
|
||||
elif repository_status['url'] != repo_url:
|
||||
module.fail_json(msg="Repository already have a repository named {0}".format(repo_name))
|
||||
elif repository_status["url"] != repo_url:
|
||||
module.fail_json(
|
||||
msg="Repository already have a repository named {0}".format(repo_name)
|
||||
)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=changed)
|
||||
elif not changed:
|
||||
module.exit_json(changed=False, repo_name=repo_name, repo_url=repo_url)
|
||||
|
||||
rc, out, err = run_helm(module, helm_cmd)
|
||||
rc, out, err = module.run_helm_command(helm_cmd)
|
||||
|
||||
if repo_password is not None:
|
||||
helm_cmd = helm_cmd.replace(repo_password, '******')
|
||||
helm_cmd = helm_cmd.replace(repo_password, "******")
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err),
|
||||
command=helm_cmd
|
||||
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
|
||||
rc, out, err
|
||||
),
|
||||
command=helm_cmd,
|
||||
)
|
||||
|
||||
module.exit_json(changed=changed, stdout=out, stderr=err, command=helm_cmd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
|
||||
module: helm_template
|
||||
|
||||
@@ -45,6 +45,21 @@ options:
|
||||
- Chart version to use. If this is not specified, the latest version is installed.
|
||||
required: false
|
||||
type: str
|
||||
dependency_update:
|
||||
description:
|
||||
- Run helm dependency update before the operation.
|
||||
- The I(dependency_update) option require the add of C(dependencies) block in C(Chart.yaml/requirements.yaml) file.
|
||||
- For more information please visit U(https://helm.sh/docs/helm/helm_dependency/)
|
||||
default: false
|
||||
type: bool
|
||||
aliases: [ dep_up ]
|
||||
version_added: "2.4.0"
|
||||
disable_hook:
|
||||
description:
|
||||
- Prevent hooks from running during install.
|
||||
default: False
|
||||
type: bool
|
||||
version_added: 2.4.0
|
||||
include_crds:
|
||||
description:
|
||||
- Include custom resource descriptions in rendered templates.
|
||||
@@ -57,6 +72,19 @@ options:
|
||||
- If the directory already exists, it will be overwritten.
|
||||
required: false
|
||||
type: path
|
||||
release_name:
|
||||
description:
|
||||
- Release name to use in rendered templates.
|
||||
required: false
|
||||
aliases: [ name ]
|
||||
type: str
|
||||
version_added: 2.4.0
|
||||
release_namespace:
|
||||
description:
|
||||
- namespace scope for this request.
|
||||
required: false
|
||||
type: str
|
||||
version_added: 2.4.0
|
||||
release_values:
|
||||
description:
|
||||
- Values to pass to chart.
|
||||
@@ -64,6 +92,14 @@ options:
|
||||
default: {}
|
||||
aliases: [ values ]
|
||||
type: dict
|
||||
show_only:
|
||||
description:
|
||||
- Only show manifests rendered from the given templates.
|
||||
required: false
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
version_added: 2.4.0
|
||||
values_files:
|
||||
description:
|
||||
- Value files to pass to chart.
|
||||
@@ -79,9 +115,34 @@ options:
|
||||
- Run C(helm repo update) before the operation. Can be run as part of the template generation or as a separate step.
|
||||
default: false
|
||||
type: bool
|
||||
'''
|
||||
set_values:
|
||||
description:
|
||||
- Values to pass to chart configuration.
|
||||
required: false
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
value:
|
||||
description:
|
||||
- Value to pass to chart configuration (e.g phase=prod).
|
||||
type: str
|
||||
required: true
|
||||
value_type:
|
||||
description:
|
||||
- Use C(raw) set individual value.
|
||||
- Use C(string) to force a string for an individual value.
|
||||
- Use C(file) to set individual values from a file when the value itself is too long for the command line or is dynamically generated.
|
||||
- Use C(json) to set json values (scalars/objects/arrays). This feature requires helm>=3.10.0.
|
||||
default: raw
|
||||
choices:
|
||||
- raw
|
||||
- string
|
||||
- json
|
||||
- file
|
||||
version_added: '2.4.0'
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Render templates to specified directory
|
||||
kubernetes.core.helm_template:
|
||||
chart_ref: stable/prometheus
|
||||
@@ -96,9 +157,27 @@ EXAMPLES = r'''
|
||||
copy:
|
||||
dest: myfile.yaml
|
||||
content: "{{ result.stdout }}"
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
- name: Render MutatingWebhooksConfiguration for revision tag "canary", rev "1-13-0"
|
||||
kubernetes.core.helm_template:
|
||||
chart_ref: istio/istiod
|
||||
chart_version: "1.13.0"
|
||||
release_namespace: "istio-system"
|
||||
show_only:
|
||||
- "templates/revision-tags.yaml"
|
||||
release_values:
|
||||
revision: "1-13-0"
|
||||
revisionTags:
|
||||
- "canary"
|
||||
register: result
|
||||
|
||||
- name: Write templates to file
|
||||
copy:
|
||||
dest: myfile.yaml
|
||||
content: "{{ result.stdout }}"
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
stdout:
|
||||
type: str
|
||||
description: Full C(helm) command stdout. If no I(output_dir) has been provided this will contain the rendered templates as concatenated yaml documents.
|
||||
@@ -114,25 +193,51 @@ command:
|
||||
description: Full C(helm) command run by this module, in case you want to re-run the command outside the module or debug a problem.
|
||||
returned: always
|
||||
sample: helm template --output-dir mychart nginx-stable/nginx-ingress
|
||||
'''
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
||||
IMP_YAML = True
|
||||
IMP_YAML_ERR = None
|
||||
except ImportError:
|
||||
IMP_YAML_ERR = traceback.format_exc()
|
||||
IMP_YAML = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
|
||||
AnsibleHelmModule,
|
||||
)
|
||||
|
||||
|
||||
def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir=None,
|
||||
release_values=None, values_files=None, include_crds=False):
|
||||
cmd += " template " + chart_ref
|
||||
def template(
|
||||
cmd,
|
||||
chart_ref,
|
||||
chart_repo_url=None,
|
||||
chart_version=None,
|
||||
dependency_update=None,
|
||||
disable_hook=None,
|
||||
output_dir=None,
|
||||
show_only=None,
|
||||
release_name=None,
|
||||
release_namespace=None,
|
||||
release_values=None,
|
||||
values_files=None,
|
||||
include_crds=False,
|
||||
set_values=None,
|
||||
):
|
||||
cmd += " template "
|
||||
|
||||
if release_name:
|
||||
cmd += release_name + " "
|
||||
|
||||
cmd += chart_ref
|
||||
|
||||
if dependency_update:
|
||||
cmd += " --dependency-update"
|
||||
|
||||
if chart_repo_url:
|
||||
cmd += " --repo=" + chart_repo_url
|
||||
@@ -140,81 +245,116 @@ def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir
|
||||
if chart_version:
|
||||
cmd += " --version=" + chart_version
|
||||
|
||||
if disable_hook:
|
||||
cmd += " --no-hooks"
|
||||
|
||||
if output_dir:
|
||||
cmd += " --output-dir=" + output_dir
|
||||
|
||||
if release_values:
|
||||
fd, path = tempfile.mkstemp(suffix='.yml')
|
||||
with open(path, 'w') as yaml_file:
|
||||
yaml.dump(release_values, yaml_file, default_flow_style=False)
|
||||
cmd += " -f=" + path
|
||||
if show_only:
|
||||
for template in show_only:
|
||||
cmd += " -s " + template
|
||||
|
||||
if values_files:
|
||||
for values_file in values_files:
|
||||
cmd += " -f=" + values_file
|
||||
|
||||
if release_namespace:
|
||||
cmd += " -n " + release_namespace
|
||||
|
||||
if release_values:
|
||||
fd, path = tempfile.mkstemp(suffix=".yml")
|
||||
with open(path, "w") as yaml_file:
|
||||
yaml.dump(release_values, yaml_file, default_flow_style=False)
|
||||
cmd += " -f=" + path
|
||||
|
||||
if include_crds:
|
||||
cmd += " --include-crds"
|
||||
|
||||
if set_values:
|
||||
cmd += " " + set_values
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
module = AnsibleHelmModule(
|
||||
argument_spec=dict(
|
||||
binary_path=dict(type='path'),
|
||||
chart_ref=dict(type='path', required=True),
|
||||
chart_repo_url=dict(type='str'),
|
||||
chart_version=dict(type='str'),
|
||||
include_crds=dict(type='bool', default=False),
|
||||
output_dir=dict(type='path'),
|
||||
release_values=dict(type='dict', default={}, aliases=['values']),
|
||||
values_files=dict(type='list', default=[], elements='str'),
|
||||
update_repo_cache=dict(type='bool', default=False)
|
||||
binary_path=dict(type="path"),
|
||||
chart_ref=dict(type="path", required=True),
|
||||
chart_repo_url=dict(type="str"),
|
||||
chart_version=dict(type="str"),
|
||||
dependency_update=dict(type="bool", default=False, aliases=["dep_up"]),
|
||||
disable_hook=dict(type="bool", default=False),
|
||||
include_crds=dict(type="bool", default=False),
|
||||
release_name=dict(type="str", aliases=["name"]),
|
||||
output_dir=dict(type="path"),
|
||||
release_namespace=dict(type="str"),
|
||||
release_values=dict(type="dict", default={}, aliases=["values"]),
|
||||
show_only=dict(type="list", default=[], elements="str"),
|
||||
values_files=dict(type="list", default=[], elements="str"),
|
||||
update_repo_cache=dict(type="bool", default=False),
|
||||
set_values=dict(type="list", elements="dict"),
|
||||
),
|
||||
supports_check_mode=True
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
check_mode = module.check_mode
|
||||
bin_path = module.params.get('binary_path')
|
||||
chart_ref = module.params.get('chart_ref')
|
||||
chart_repo_url = module.params.get('chart_repo_url')
|
||||
chart_version = module.params.get('chart_version')
|
||||
include_crds = module.params.get('include_crds')
|
||||
output_dir = module.params.get('output_dir')
|
||||
release_values = module.params.get('release_values')
|
||||
values_files = module.params.get('values_files')
|
||||
update_repo_cache = module.params.get('update_repo_cache')
|
||||
chart_ref = module.params.get("chart_ref")
|
||||
chart_repo_url = module.params.get("chart_repo_url")
|
||||
chart_version = module.params.get("chart_version")
|
||||
dependency_update = module.params.get("dependency_update")
|
||||
disable_hook = module.params.get("disable_hook")
|
||||
include_crds = module.params.get("include_crds")
|
||||
release_name = module.params.get("release_name")
|
||||
output_dir = module.params.get("output_dir")
|
||||
show_only = module.params.get("show_only")
|
||||
release_namespace = module.params.get("release_namespace")
|
||||
release_values = module.params.get("release_values")
|
||||
values_files = module.params.get("values_files")
|
||||
update_repo_cache = module.params.get("update_repo_cache")
|
||||
set_values = module.params.get("set_values")
|
||||
|
||||
if not IMP_YAML:
|
||||
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
|
||||
|
||||
helm_cmd = bin_path or module.get_bin_path('helm', required=True)
|
||||
helm_cmd = module.get_helm_binary()
|
||||
|
||||
if update_repo_cache:
|
||||
update_cmd = helm_cmd + " repo update"
|
||||
run_helm(module, update_cmd)
|
||||
module.run_helm_command(update_cmd)
|
||||
|
||||
tmpl_cmd = template(helm_cmd, chart_ref, chart_repo_url=chart_repo_url,
|
||||
chart_version=chart_version, output_dir=output_dir,
|
||||
release_values=release_values, values_files=values_files,
|
||||
include_crds=include_crds)
|
||||
set_values_args = None
|
||||
if set_values:
|
||||
set_values_args = module.get_helm_set_values_args(set_values)
|
||||
|
||||
tmpl_cmd = template(
|
||||
helm_cmd,
|
||||
chart_ref,
|
||||
dependency_update=dependency_update,
|
||||
chart_repo_url=chart_repo_url,
|
||||
chart_version=chart_version,
|
||||
disable_hook=disable_hook,
|
||||
release_name=release_name,
|
||||
output_dir=output_dir,
|
||||
release_namespace=release_namespace,
|
||||
release_values=release_values,
|
||||
show_only=show_only,
|
||||
values_files=values_files,
|
||||
include_crds=include_crds,
|
||||
set_values=set_values_args,
|
||||
)
|
||||
|
||||
if not check_mode:
|
||||
rc, out, err = run_helm(module, tmpl_cmd)
|
||||
rc, out, err = module.run_helm_command(tmpl_cmd)
|
||||
else:
|
||||
out = err = ""
|
||||
rc = 0
|
||||
|
||||
module.exit_json(
|
||||
failed=False,
|
||||
changed=True,
|
||||
command=tmpl_cmd,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
rc=rc
|
||||
failed=False, changed=True, command=tmpl_cmd, stdout=out, stderr=err, rc=rc
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
|
||||
module: k8s
|
||||
|
||||
@@ -142,15 +142,45 @@ options:
|
||||
type: list
|
||||
elements: str
|
||||
version_added: 2.2.0
|
||||
generate_name:
|
||||
description:
|
||||
- Use to specify the basis of an object name and random characters will be added automatically on server to generate a unique name.
|
||||
- This option is ignored when I(state) is not set to C(present) or when I(apply) is set to C(yes).
|
||||
- If I(resource definition) is provided, the I(metadata.generateName) value from the I(resource_definition)
|
||||
will override this option.
|
||||
- If I(resource definition) is provided, and contains I(metadata.name), this option is ignored.
|
||||
- mutually exclusive with C(name).
|
||||
type: str
|
||||
version_added: 2.3.0
|
||||
server_side_apply:
|
||||
description:
|
||||
- When this option is set, apply runs in the server instead of the client.
|
||||
- Ignored if C(apply) is not set or is set to False.
|
||||
- This option requires "kubernetes >= 19.15.0".
|
||||
type: dict
|
||||
version_added: 2.3.0
|
||||
suboptions:
|
||||
field_manager:
|
||||
type: str
|
||||
description:
|
||||
- Name of the manager used to track field ownership.
|
||||
required: True
|
||||
force_conflicts:
|
||||
description:
|
||||
- A conflict is a special status error that occurs when an Server Side Apply operation tries to change a field,
|
||||
which another user also claims to manage.
|
||||
- When set to True, server-side apply will force the changes against conflicts.
|
||||
type: bool
|
||||
default: False
|
||||
|
||||
requirements:
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
- "jsonpatch"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Create a k8s namespace
|
||||
kubernetes.core.k8s:
|
||||
name: testing
|
||||
@@ -202,6 +232,14 @@ EXAMPLES = r'''
|
||||
state: present
|
||||
definition: "{{ lookup('file', '/testing/deployment.yml') | from_yaml }}"
|
||||
|
||||
- name: >-
|
||||
(Alternative) Read definition file from the Ansible controller file system.
|
||||
In this case, the definition file contains multiple YAML documents, separated by ---.
|
||||
If the definition file has been encrypted with Ansible Vault it will automatically be decrypted.
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('file', '/testing/deployment.yml') | from_yaml_all }}"
|
||||
|
||||
- name: Read definition template file from the Ansible controller file system
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
@@ -219,10 +257,10 @@ EXAMPLES = r'''
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
template:
|
||||
- path: '/testing/deployment_one.j2'
|
||||
- path: '/testing/deployment_two.j2'
|
||||
variable_start_string: '[['
|
||||
variable_end_string: ']]'
|
||||
- path: '/testing/deployment_one.j2'
|
||||
- path: '/testing/deployment_two.j2'
|
||||
variable_start_string: '[['
|
||||
variable_end_string: ']]'
|
||||
|
||||
- name: fail on validation errors
|
||||
kubernetes.core.k8s:
|
||||
@@ -278,9 +316,36 @@ EXAMPLES = r'''
|
||||
metadata:
|
||||
labels:
|
||||
support: patch
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
# Create object using generateName
|
||||
- name: create resource using name generated by the server
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
generate_name: pod-
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
spec:
|
||||
containers:
|
||||
- name: py
|
||||
image: python:3.7-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
# Server side apply
|
||||
- name: Create configmap using server side apply
|
||||
kubernetes.core.k8s:
|
||||
namespace: testing
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: my-configmap
|
||||
apply: yes
|
||||
server_side_apply:
|
||||
field_manager: ansible
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
result:
|
||||
description:
|
||||
- The created, patched, or otherwise present object. Will be empty in the case of a deletion.
|
||||
@@ -320,20 +385,43 @@ result:
|
||||
description: error while trying to create/delete the object.
|
||||
returned: error
|
||||
type: complex
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC, WAIT_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, DELETE_OPTS_ARG_SPEC)
|
||||
AUTH_ARG_SPEC,
|
||||
WAIT_ARG_SPEC,
|
||||
NAME_ARG_SPEC,
|
||||
RESOURCE_ARG_SPEC,
|
||||
DELETE_OPTS_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import (
|
||||
run_module,
|
||||
)
|
||||
|
||||
|
||||
def validate_spec():
|
||||
return dict(
|
||||
fail_on_error=dict(type='bool'),
|
||||
fail_on_error=dict(type="bool"),
|
||||
version=dict(),
|
||||
strict=dict(type='bool', default=True)
|
||||
strict=dict(type="bool", default=True),
|
||||
)
|
||||
|
||||
|
||||
def server_apply_spec():
|
||||
return dict(
|
||||
field_manager=dict(type="str", required=True),
|
||||
force_conflicts=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -342,56 +430,50 @@ def argspec():
|
||||
argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC))
|
||||
argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
|
||||
argument_spec.update(copy.deepcopy(WAIT_ARG_SPEC))
|
||||
argument_spec['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge'])
|
||||
argument_spec['validate'] = dict(type='dict', default=None, options=validate_spec())
|
||||
argument_spec['append_hash'] = dict(type='bool', default=False)
|
||||
argument_spec['apply'] = dict(type='bool', default=False)
|
||||
argument_spec['template'] = dict(type='raw', default=None)
|
||||
argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC))
|
||||
argument_spec['continue_on_error'] = dict(type='bool', default=False)
|
||||
argument_spec['state'] = dict(default='present', choices=['present', 'absent', 'patched'])
|
||||
argument_spec['force'] = dict(type='bool', default=False)
|
||||
argument_spec['label_selectors'] = dict(type='list', elements='str')
|
||||
argument_spec["merge_type"] = dict(
|
||||
type="list", elements="str", choices=["json", "merge", "strategic-merge"]
|
||||
)
|
||||
argument_spec["validate"] = dict(type="dict", default=None, options=validate_spec())
|
||||
argument_spec["append_hash"] = dict(type="bool", default=False)
|
||||
argument_spec["apply"] = dict(type="bool", default=False)
|
||||
argument_spec["template"] = dict(type="raw", default=None)
|
||||
argument_spec["delete_options"] = dict(
|
||||
type="dict", default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)
|
||||
)
|
||||
argument_spec["continue_on_error"] = dict(type="bool", default=False)
|
||||
argument_spec["state"] = dict(
|
||||
default="present", choices=["present", "absent", "patched"]
|
||||
)
|
||||
argument_spec["force"] = dict(type="bool", default=False)
|
||||
argument_spec["label_selectors"] = dict(type="list", elements="str")
|
||||
argument_spec["generate_name"] = dict()
|
||||
argument_spec["server_side_apply"] = dict(
|
||||
type="dict", default=None, options=server_apply_spec()
|
||||
)
|
||||
|
||||
return argument_spec
|
||||
|
||||
|
||||
def execute_module(module, k8s_ansible_mixin):
|
||||
k8s_ansible_mixin.module = module
|
||||
k8s_ansible_mixin.argspec = module.argument_spec
|
||||
k8s_ansible_mixin.check_mode = k8s_ansible_mixin.module.check_mode
|
||||
k8s_ansible_mixin.params = k8s_ansible_mixin.module.params
|
||||
k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json
|
||||
k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json
|
||||
k8s_ansible_mixin.exit_json = k8s_ansible_mixin.module.exit_json
|
||||
k8s_ansible_mixin.warn = k8s_ansible_mixin.module.warn
|
||||
k8s_ansible_mixin.warnings = []
|
||||
|
||||
k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get('kind')
|
||||
k8s_ansible_mixin.api_version = k8s_ansible_mixin.params.get('api_version')
|
||||
k8s_ansible_mixin.name = k8s_ansible_mixin.params.get('name')
|
||||
k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get('namespace')
|
||||
|
||||
k8s_ansible_mixin.check_library_version()
|
||||
k8s_ansible_mixin.set_resource_definitions(module)
|
||||
k8s_ansible_mixin.execute_module()
|
||||
|
||||
|
||||
def main():
|
||||
mutually_exclusive = [
|
||||
('resource_definition', 'src'),
|
||||
('merge_type', 'apply'),
|
||||
('template', 'resource_definition'),
|
||||
('template', 'src'),
|
||||
("resource_definition", "src"),
|
||||
("merge_type", "apply"),
|
||||
("template", "resource_definition"),
|
||||
("template", "src"),
|
||||
("name", "generate_name"),
|
||||
]
|
||||
module = AnsibleModule(argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
K8sAnsibleMixin, get_api_client)
|
||||
|
||||
k8s_ansible_mixin = K8sAnsibleMixin(module)
|
||||
k8s_ansible_mixin.client = get_api_client(module=module)
|
||||
execute_module(module, k8s_ansible_mixin)
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule,
|
||||
argument_spec=argspec(),
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
try:
|
||||
run_module(module)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
# 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'''
|
||||
DOCUMENTATION = r"""
|
||||
module: k8s_cluster_info
|
||||
|
||||
version_added: "0.11.1"
|
||||
@@ -36,9 +37,9 @@ requirements:
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Get Cluster information
|
||||
kubernetes.core.k8s_cluster_info:
|
||||
register: api_status
|
||||
@@ -47,9 +48,9 @@ EXAMPLES = r'''
|
||||
kubernetes.core.k8s_cluster_info:
|
||||
invalidate_cache: False
|
||||
register: api_status
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
connection:
|
||||
description:
|
||||
- Connection information
|
||||
@@ -136,78 +137,96 @@ apis:
|
||||
description: Resource singular name
|
||||
returned: success
|
||||
type: str
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
import copy
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
|
||||
HAS_K8S = False
|
||||
try:
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import ResourceList
|
||||
HAS_K8S = True
|
||||
except ImportError as e:
|
||||
K8S_IMP_ERR = e
|
||||
K8S_IMP_EXC = traceback.format_exc()
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import (
|
||||
ResourceList,
|
||||
)
|
||||
except ImportError:
|
||||
# Handled during module setup
|
||||
pass
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_SPEC)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC,
|
||||
)
|
||||
|
||||
|
||||
def execute_module(module, client):
|
||||
invalidate_cache = boolean(module.params.get('invalidate_cache', True), strict=False)
|
||||
if invalidate_cache:
|
||||
if module.params.get("invalidate_cache"):
|
||||
client.resources.invalidate_cache()
|
||||
results = defaultdict(dict)
|
||||
for resource in list(client.resources):
|
||||
resource = resource[0]
|
||||
if isinstance(resource, ResourceList):
|
||||
continue
|
||||
key = resource.group_version if resource.group == '' else '/'.join([resource.group, resource.group_version.split('/')[-1]])
|
||||
key = (
|
||||
resource.group_version
|
||||
if resource.group == ""
|
||||
else "/".join([resource.group, resource.group_version.split("/")[-1]])
|
||||
)
|
||||
results[key][resource.kind] = {
|
||||
'categories': resource.categories if resource.categories else [],
|
||||
'name': resource.name,
|
||||
'namespaced': resource.namespaced,
|
||||
'preferred': resource.preferred,
|
||||
'short_names': resource.short_names if resource.short_names else [],
|
||||
'singular_name': resource.singular_name,
|
||||
"categories": resource.categories if resource.categories else [],
|
||||
"name": resource.name,
|
||||
"namespaced": resource.namespaced,
|
||||
"preferred": resource.preferred,
|
||||
"short_names": resource.short_names if resource.short_names else [],
|
||||
"singular_name": resource.singular_name,
|
||||
}
|
||||
configuration = client.configuration
|
||||
connection = {
|
||||
'cert_file': configuration.cert_file,
|
||||
'host': configuration.host,
|
||||
'password': configuration.password,
|
||||
'proxy': configuration.proxy,
|
||||
'ssl_ca_cert': configuration.ssl_ca_cert,
|
||||
'username': configuration.username,
|
||||
'verify_ssl': configuration.verify_ssl,
|
||||
"cert_file": configuration.cert_file,
|
||||
"host": configuration.host,
|
||||
"password": configuration.password,
|
||||
"proxy": configuration.proxy,
|
||||
"ssl_ca_cert": configuration.ssl_ca_cert,
|
||||
"username": configuration.username,
|
||||
"verify_ssl": configuration.verify_ssl,
|
||||
}
|
||||
from kubernetes import __version__ as version
|
||||
|
||||
version_info = {
|
||||
'client': version,
|
||||
'server': client.version,
|
||||
"client": version,
|
||||
"server": client.client.version,
|
||||
}
|
||||
module.exit_json(changed=False, apis=results, connection=connection, version=version_info)
|
||||
module.exit_json(
|
||||
changed=False, apis=results, connection=connection, version=version_info
|
||||
)
|
||||
|
||||
|
||||
def argspec():
|
||||
spec = copy.deepcopy(AUTH_ARG_SPEC)
|
||||
spec['invalidate_cache'] = dict(type='bool', default=True)
|
||||
spec["invalidate_cache"] = dict(type="bool", default=True)
|
||||
return spec
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
|
||||
if not HAS_K8S:
|
||||
module.fail_json(msg=missing_required_lib('kubernetes'), exception=K8S_IMP_EXC,
|
||||
error=to_native(K8S_IMP_ERR))
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import get_api_client
|
||||
execute_module(module, client=get_api_client(module=module))
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
|
||||
try:
|
||||
execute_module(module, client=get_api_client(module=module))
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
# 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'''
|
||||
DOCUMENTATION = r"""
|
||||
|
||||
module: k8s_cp
|
||||
|
||||
@@ -78,9 +79,9 @@ options:
|
||||
|
||||
notes:
|
||||
- the tar binary is required on the container when copying from local filesystem to pod.
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
# kubectl cp /tmp/foo some-namespace/some-pod:/tmp/bar
|
||||
- name: Copy /tmp/foo local file to /tmp/bar in a remote pod
|
||||
kubernetes.core.k8s_cp:
|
||||
@@ -118,376 +119,107 @@ EXAMPLES = r'''
|
||||
state: from_pod
|
||||
|
||||
# copy content into a file in the remote pod
|
||||
- name: Copy /tmp/foo from a remote pod to /tmp/bar locally
|
||||
- name: Copy content into a file in the remote pod
|
||||
kubernetes.core.k8s_cp:
|
||||
state: to_pod
|
||||
namespace: some-namespace
|
||||
pod: some-pod
|
||||
remote_path: /tmp/foo.txt
|
||||
content: "This content will be copied into remote file"
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
result:
|
||||
description:
|
||||
- message describing the copy operation successfully done.
|
||||
returned: success
|
||||
type: str
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
import os
|
||||
from tempfile import TemporaryFile, NamedTemporaryFile
|
||||
from select import select
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import tarfile
|
||||
|
||||
# from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
K8sService,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.copy import (
|
||||
K8SCopyFromPod,
|
||||
K8SCopyToPod,
|
||||
check_pod,
|
||||
)
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import K8sAnsibleMixin, get_api_client
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC
|
||||
|
||||
try:
|
||||
from kubernetes.client.api import core_v1_api
|
||||
from kubernetes.stream import stream
|
||||
from kubernetes.stream.ws_client import STDOUT_CHANNEL, STDERR_CHANNEL, ERROR_CHANNEL, ABNF
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
# ImportError are managed by the common module already.
|
||||
pass
|
||||
|
||||
|
||||
class K8SCopy(metaclass=ABCMeta):
|
||||
|
||||
def __init__(self, module, client):
|
||||
self.client = client
|
||||
self.module = module
|
||||
self.api_instance = core_v1_api.CoreV1Api(client.client)
|
||||
|
||||
self.local_path = module.params.get('local_path')
|
||||
self.name = module.params.get('pod')
|
||||
self.namespace = module.params.get('namespace')
|
||||
self.remote_path = module.params.get('remote_path')
|
||||
self.content = module.params.get('content')
|
||||
|
||||
self.no_preserve = module.params.get('no_preserve')
|
||||
self.container_arg = {}
|
||||
if module.params.get('container'):
|
||||
self.container_arg['container'] = module.params.get('container')
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
|
||||
class K8SCopyFromPod(K8SCopy):
|
||||
"""
|
||||
Copy files/directory from Pod into local filesystem
|
||||
"""
|
||||
def __init__(self, module, client):
|
||||
super(K8SCopyFromPod, self).__init__(module, client)
|
||||
self.is_remote_path_dir = None
|
||||
self.files_to_copy = list()
|
||||
|
||||
def list_remote_files(self):
|
||||
"""
|
||||
This method will check if the remote path is a dir or file
|
||||
if it is a directory the file list will be updated accordingly
|
||||
"""
|
||||
try:
|
||||
find_cmd = ['find', self.remote_path, '-type', 'f', '-name', '*']
|
||||
response = stream(self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=find_cmd,
|
||||
stdout=True, stderr=True,
|
||||
stdin=False, tty=False,
|
||||
_preload_content=False, **self.container_arg)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to execute on pod {0}/{1} due to : {2}".format(self.namespace, self.name, to_native(e)))
|
||||
stderr = []
|
||||
while response.is_open():
|
||||
response.update(timeout=1)
|
||||
if response.peek_stdout():
|
||||
self.files_to_copy.extend(response.read_stdout().rstrip('\n').split('\n'))
|
||||
if response.peek_stderr():
|
||||
err = response.read_stderr()
|
||||
if "No such file or directory" in err:
|
||||
self.module.fail_json(msg="{0} does not exist in remote pod filesystem".format(self.remote_path))
|
||||
stderr.append(err)
|
||||
error = response.read_channel(ERROR_CHANNEL)
|
||||
response.close()
|
||||
error = yaml.safe_load(error)
|
||||
if error['status'] != 'Success':
|
||||
self.module.fail_json(msg="Failed to execute on Pod due to: {0}".format(error))
|
||||
|
||||
def read(self):
|
||||
self.stdout = None
|
||||
self.stderr = None
|
||||
|
||||
if self.response.is_open():
|
||||
if not self.response.sock.connected:
|
||||
self.response._connected = False
|
||||
else:
|
||||
ret, out, err = select((self.response.sock.sock, ), (), (), 0)
|
||||
if ret:
|
||||
code, frame = self.response.sock.recv_data_frame(True)
|
||||
if code == ABNF.OPCODE_CLOSE:
|
||||
self.response._connected = False
|
||||
elif code in (ABNF.OPCODE_BINARY, ABNF.OPCODE_TEXT) and len(frame.data) > 1:
|
||||
channel = frame.data[0]
|
||||
content = frame.data[1:]
|
||||
if content:
|
||||
if channel == STDOUT_CHANNEL:
|
||||
self.stdout = content
|
||||
elif channel == STDERR_CHANNEL:
|
||||
self.stderr = content.decode("utf-8", "replace")
|
||||
|
||||
def copy(self):
|
||||
is_remote_path_dir = len(self.files_to_copy) > 1 or self.files_to_copy[0] != self.remote_path
|
||||
relpath_start = self.remote_path
|
||||
if is_remote_path_dir and os.path.isdir(self.local_path):
|
||||
relpath_start = os.path.dirname(self.remote_path)
|
||||
|
||||
for remote_file in self.files_to_copy:
|
||||
dest_file = self.local_path
|
||||
if is_remote_path_dir:
|
||||
dest_file = os.path.join(self.local_path, os.path.relpath(remote_file, start=relpath_start))
|
||||
# create directory to copy file in
|
||||
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
|
||||
|
||||
pod_command = ['cat', remote_file]
|
||||
self.response = stream(self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=pod_command,
|
||||
stderr=True, stdin=True,
|
||||
stdout=True, tty=False,
|
||||
_preload_content=False, **self.container_arg)
|
||||
errors = []
|
||||
with open(dest_file, 'wb') as fh:
|
||||
while self.response._connected:
|
||||
self.read()
|
||||
if self.stdout:
|
||||
fh.write(self.stdout)
|
||||
if self.stderr:
|
||||
errors.append(self.stderr)
|
||||
if errors:
|
||||
self.module.fail_json(msg="Failed to copy file from Pod: {0}".format(''.join(errors)))
|
||||
self.module.exit_json(changed=True, result="{0} successfully copied locally into {1}".format(self.remote_path, self.local_path))
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.list_remote_files()
|
||||
if self.files_to_copy == []:
|
||||
self.module.exit_json(changed=False, warning="No file found from directory '{0}' into remote Pod.".format(self.remote_path))
|
||||
self.copy()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to copy file/directory from Pod due to: {0}".format(to_native(e)))
|
||||
|
||||
|
||||
class K8SCopyToPod(K8SCopy):
|
||||
"""
|
||||
Copy files/directory from local filesystem into remote Pod
|
||||
"""
|
||||
def __init__(self, module, client):
|
||||
super(K8SCopyToPod, self).__init__(module, client)
|
||||
self.files_to_copy = list()
|
||||
|
||||
def run_from_pod(self, command):
|
||||
response = stream(self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=command,
|
||||
stderr=True, stdin=False,
|
||||
stdout=True, tty=False,
|
||||
_preload_content=False, **self.container_arg)
|
||||
errors = []
|
||||
while response.is_open():
|
||||
response.update(timeout=1)
|
||||
if response.peek_stderr():
|
||||
errors.append(response.read_stderr())
|
||||
response.close()
|
||||
err = response.read_channel(ERROR_CHANNEL)
|
||||
err = yaml.safe_load(err)
|
||||
response.close()
|
||||
if err['status'] != 'Success':
|
||||
self.module.fail_json(msg="Failed to run {0} on Pod.".format(command), errors=errors)
|
||||
|
||||
def is_remote_path_dir(self):
|
||||
pod_command = ['test', '-d', self.remote_path]
|
||||
response = stream(self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=pod_command,
|
||||
stdout=True, stderr=True,
|
||||
stdin=False, tty=False,
|
||||
_preload_content=False, **self.container_arg)
|
||||
while response.is_open():
|
||||
response.update(timeout=1)
|
||||
err = response.read_channel(ERROR_CHANNEL)
|
||||
err = yaml.safe_load(err)
|
||||
response.close()
|
||||
if err['status'] == 'Success':
|
||||
return True
|
||||
return False
|
||||
|
||||
def close_temp_file(self):
|
||||
if self.named_temp_file:
|
||||
self.named_temp_file.close()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# remove trailing slash from destination path
|
||||
dest_file = self.remote_path.rstrip("/")
|
||||
src_file = self.local_path
|
||||
self.named_temp_file = None
|
||||
if self.content:
|
||||
self.named_temp_file = NamedTemporaryFile(mode="w")
|
||||
self.named_temp_file.write(self.content)
|
||||
self.named_temp_file.flush()
|
||||
src_file = self.named_temp_file.name
|
||||
else:
|
||||
if not os.path.exists(self.local_path):
|
||||
self.module.fail_json(msg="{0} does not exist in local filesystem".format(self.local_path))
|
||||
if not os.access(self.local_path, os.R_OK):
|
||||
self.module.fail_json(msg="{0} not readable".format(self.local_path))
|
||||
|
||||
if self.is_remote_path_dir():
|
||||
if self.content:
|
||||
self.module.fail_json(msg="When content is specified, remote path should not be an existing directory")
|
||||
else:
|
||||
dest_file = os.path.join(dest_file, os.path.basename(src_file))
|
||||
|
||||
if self.no_preserve:
|
||||
tar_command = ['tar', '--no-same-permissions', '--no-same-owner', '-xmf', '-']
|
||||
else:
|
||||
tar_command = ['tar', '-xmf', '-']
|
||||
|
||||
if dest_file.startswith("/"):
|
||||
tar_command.extend(['-C', '/'])
|
||||
|
||||
response = stream(self.api_instance.connect_get_namespaced_pod_exec,
|
||||
self.name,
|
||||
self.namespace,
|
||||
command=tar_command,
|
||||
stderr=True, stdin=True,
|
||||
stdout=True, tty=False,
|
||||
_preload_content=False, **self.container_arg)
|
||||
with TemporaryFile() as tar_buffer:
|
||||
with tarfile.open(fileobj=tar_buffer, mode='w') as tar:
|
||||
tar.add(src_file, dest_file)
|
||||
tar_buffer.seek(0)
|
||||
commands = []
|
||||
# push command in chunk mode
|
||||
size = 1024 * 1024
|
||||
while True:
|
||||
data = tar_buffer.read(size)
|
||||
if not data:
|
||||
break
|
||||
commands.append(data)
|
||||
|
||||
stderr, stdout = [], []
|
||||
while response.is_open():
|
||||
if response.peek_stdout():
|
||||
stdout.append(response.read_stdout().rstrip("\n"))
|
||||
if response.peek_stderr():
|
||||
stderr.append(response.read_stderr().rstrip("\n"))
|
||||
if commands:
|
||||
cmd = commands.pop(0)
|
||||
response.write_stdin(cmd)
|
||||
else:
|
||||
break
|
||||
response.close()
|
||||
if stderr:
|
||||
self.close_temp_file()
|
||||
self.module.fail_json(command=tar_command, msg="Failed to copy local file/directory into Pod due to: {0}".format(''.join(stderr)))
|
||||
self.close_temp_file()
|
||||
if self.content:
|
||||
self.module.exit_json(changed=True, result="Content successfully copied into {0} on remote Pod".format(self.remote_path))
|
||||
self.module.exit_json(changed=True, result="{0} successfully copied into remote Pod into {1}".format(self.local_path, self.remote_path))
|
||||
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to copy local file/directory into Pod due to: {0}".format(to_native(e)))
|
||||
|
||||
|
||||
def check_pod(k8s_ansible_mixin, module):
|
||||
resource = k8s_ansible_mixin.find_resource("Pod", None, True)
|
||||
namespace = module.params.get('namespace')
|
||||
name = module.params.get('pod')
|
||||
container = module.params.get('container')
|
||||
|
||||
def _fail(exc):
|
||||
arg = {}
|
||||
if hasattr(exc, 'body'):
|
||||
msg = "Namespace={0} Kind=Pod Name={1}: Failed requested object: {2}".format(namespace, name, exc.body)
|
||||
else:
|
||||
msg = to_native(exc)
|
||||
for attr in ['status', 'reason']:
|
||||
if hasattr(exc, attr):
|
||||
arg[attr] = getattr(exc, attr)
|
||||
module.fail_json(msg=msg, **arg)
|
||||
|
||||
try:
|
||||
result = resource.get(name=name, namespace=namespace)
|
||||
containers = [c['name'] for c in result.to_dict()['status']['containerStatuses']]
|
||||
if container and container not in containers:
|
||||
module.fail_json(msg="Pod has no container {0}".format(container))
|
||||
return containers
|
||||
except Exception as exc:
|
||||
_fail(exc)
|
||||
def argspec():
|
||||
argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
|
||||
argument_spec["namespace"] = {"type": "str", "required": True}
|
||||
argument_spec["pod"] = {"type": "str", "required": True}
|
||||
argument_spec["container"] = {}
|
||||
argument_spec["remote_path"] = {"type": "path", "required": True}
|
||||
argument_spec["local_path"] = {"type": "path"}
|
||||
argument_spec["content"] = {"type": "str"}
|
||||
argument_spec["state"] = {
|
||||
"type": "str",
|
||||
"default": "to_pod",
|
||||
"choices": ["to_pod", "from_pod"],
|
||||
}
|
||||
argument_spec["no_preserve"] = {"type": "bool", "default": False}
|
||||
return argument_spec
|
||||
|
||||
|
||||
def execute_module(module):
|
||||
client = get_api_client(module=module)
|
||||
svc = K8sService(client, module)
|
||||
containers = check_pod(svc)
|
||||
if len(containers) > 1 and module.params.get("container") is None:
|
||||
module.fail_json(
|
||||
msg="Pod contains more than 1 container, option 'container' should be set"
|
||||
)
|
||||
|
||||
k8s_ansible_mixin = K8sAnsibleMixin(module, pyyaml_required=False)
|
||||
k8s_ansible_mixin.check_library_version()
|
||||
|
||||
k8s_ansible_mixin.module = module
|
||||
k8s_ansible_mixin.argspec = module.argument_spec
|
||||
k8s_ansible_mixin.params = k8s_ansible_mixin.module.params
|
||||
k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json
|
||||
k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json
|
||||
|
||||
k8s_ansible_mixin.client = get_api_client(module=module)
|
||||
containers = check_pod(k8s_ansible_mixin, module)
|
||||
if len(containers) > 1 and module.params.get('container') is None:
|
||||
module.fail_json(msg="Pod contains more than 1 container, option 'container' should be set")
|
||||
state = module.params.get("state")
|
||||
if state == "to_pod":
|
||||
k8s_copy = K8SCopyToPod(module, client.client)
|
||||
else:
|
||||
k8s_copy = K8SCopyFromPod(module, client.client)
|
||||
|
||||
try:
|
||||
load_class = {'to_pod': K8SCopyToPod, 'from_pod': K8SCopyFromPod}
|
||||
state = module.params.get('state')
|
||||
k8s_copy = load_class.get(state)(module, k8s_ansible_mixin.client)
|
||||
k8s_copy.run()
|
||||
except Exception as e:
|
||||
module.fail_json("Failed to copy object due to: {0}".format(to_native(e)))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
|
||||
argument_spec['namespace'] = {'type': 'str', 'required': True}
|
||||
argument_spec['pod'] = {'type': 'str', 'required': True}
|
||||
argument_spec['container'] = {}
|
||||
argument_spec['remote_path'] = {'type': 'path', 'required': True}
|
||||
argument_spec['local_path'] = {'type': 'path'}
|
||||
argument_spec['content'] = {'type': 'str'}
|
||||
argument_spec['state'] = {'type': 'str', 'default': 'to_pod', 'choices': ['to_pod', 'from_pod']}
|
||||
argument_spec['no_preserve'] = {'type': 'bool', 'default': False}
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule,
|
||||
argument_spec=argspec(),
|
||||
check_pyyaml=False,
|
||||
mutually_exclusive=[("local_path", "content")],
|
||||
required_if=[("state", "from_pod", ["local_path"])],
|
||||
required_one_of=[["local_path", "content"]],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
mutually_exclusive=[('local_path', 'content')],
|
||||
required_if=[('state', 'from_pod', ['local_path'])],
|
||||
required_one_of=[['local_path', 'content']],
|
||||
supports_check_mode=True)
|
||||
|
||||
execute_module(module)
|
||||
try:
|
||||
execute_module(module)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
|
||||
module: k8s_drain
|
||||
|
||||
@@ -46,6 +46,7 @@ options:
|
||||
description:
|
||||
- Specify options to delete pods.
|
||||
- This option has effect only when C(state) is set to I(drain).
|
||||
default: {}
|
||||
suboptions:
|
||||
terminate_grace_period:
|
||||
description:
|
||||
@@ -64,6 +65,12 @@ options:
|
||||
- Ignore DaemonSet-managed pods.
|
||||
type: bool
|
||||
default: False
|
||||
delete_emptydir_data:
|
||||
description:
|
||||
- Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).
|
||||
type: bool
|
||||
default: False
|
||||
version_added: 2.3.0
|
||||
disable_eviction:
|
||||
description:
|
||||
- Forces drain to use delete rather than evict.
|
||||
@@ -83,9 +90,9 @@ options:
|
||||
requirements:
|
||||
- python >= 3.6
|
||||
- kubernetes >= 12.0.0
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Drain node "foo", even if there are pods not managed by a ReplicationController, Job, or DaemonSet on it.
|
||||
kubernetes.core.k8s_drain:
|
||||
state: drain
|
||||
@@ -97,7 +104,7 @@ EXAMPLES = r'''
|
||||
state: drain
|
||||
name: foo
|
||||
delete_options:
|
||||
terminate_grace_period: 900
|
||||
terminate_grace_period: 900
|
||||
|
||||
- name: Mark node "foo" as schedulable.
|
||||
kubernetes.core.k8s_drain:
|
||||
@@ -108,33 +115,63 @@ EXAMPLES = r'''
|
||||
kubernetes.core.k8s_drain:
|
||||
state: cordon
|
||||
name: foo
|
||||
"""
|
||||
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
result:
|
||||
description:
|
||||
- The node status and the number of pods deleted.
|
||||
returned: success
|
||||
type: str
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
from datetime import datetime
|
||||
import time
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC
|
||||
import traceback
|
||||
|
||||
from datetime import datetime
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
from kubernetes.client.api import core_v1_api
|
||||
from kubernetes.client.models import V1beta1Eviction, V1DeleteOptions
|
||||
from kubernetes.client.models import V1DeleteOptions, V1ObjectMeta
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
except ImportError:
|
||||
# ImportError are managed by the common module already.
|
||||
pass
|
||||
|
||||
HAS_EVICTION_API = True
|
||||
k8s_import_exception = None
|
||||
K8S_IMP_ERR = None
|
||||
|
||||
def filter_pods(pods, force, ignore_daemonset):
|
||||
try:
|
||||
from kubernetes.client.models import V1beta1Eviction as v1_eviction
|
||||
except ImportError:
|
||||
try:
|
||||
from kubernetes.client.models import V1Eviction as v1_eviction
|
||||
except ImportError as e:
|
||||
k8s_import_exception = e
|
||||
K8S_IMP_ERR = traceback.format_exc()
|
||||
HAS_EVICTION_API = False
|
||||
|
||||
|
||||
def filter_pods(pods, force, ignore_daemonset, delete_emptydir_data):
|
||||
k8s_kind_mirror = "kubernetes.io/config.mirror"
|
||||
daemonSet, unmanaged, mirror, localStorage, to_delete = [], [], [], [], []
|
||||
for pod in pods:
|
||||
@@ -144,12 +181,11 @@ def filter_pods(pods, force, ignore_daemonset):
|
||||
continue
|
||||
|
||||
# Any finished pod can be deleted
|
||||
if pod.status.phase in ('Succeeded', 'Failed'):
|
||||
if pod.status.phase in ("Succeeded", "Failed"):
|
||||
to_delete.append((pod.metadata.namespace, pod.metadata.name))
|
||||
continue
|
||||
|
||||
# Pod with local storage cannot be deleted
|
||||
# TODO: support new option delete-emptydatadir in order to allow deletion of such pod
|
||||
if pod.spec.volumes and any(vol.empty_dir for vol in pod.spec.volumes):
|
||||
localStorage.append((pod.metadata.namespace, pod.metadata.name))
|
||||
continue
|
||||
@@ -167,65 +203,68 @@ def filter_pods(pods, force, ignore_daemonset):
|
||||
|
||||
warnings, errors = [], []
|
||||
if unmanaged:
|
||||
pod_names = ','.join([pod[0] + "/" + pod[1] for pod in unmanaged])
|
||||
pod_names = ",".join([pod[0] + "/" + pod[1] for pod in unmanaged])
|
||||
if not force:
|
||||
errors.append("cannot delete Pods not managed by ReplicationController, ReplicaSet, Job,"
|
||||
" DaemonSet or StatefulSet (use option force set to yes): {0}.".format(pod_names))
|
||||
errors.append(
|
||||
"cannot delete Pods not managed by ReplicationController, ReplicaSet, Job,"
|
||||
" DaemonSet or StatefulSet (use option force set to yes): {0}.".format(
|
||||
pod_names
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Pod not managed will be deleted as 'force' is true
|
||||
warnings.append("Deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: {0}.".format(pod_names))
|
||||
warnings.append(
|
||||
"Deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: {0}.".format(
|
||||
pod_names
|
||||
)
|
||||
)
|
||||
to_delete += unmanaged
|
||||
|
||||
# mirror pods warning
|
||||
if mirror:
|
||||
pod_names = ','.join([pod[0] + "/" + pod[1] for pod in mirror])
|
||||
warnings.append("cannot delete mirror Pods using API server: {0}.".format(pod_names))
|
||||
pod_names = ",".join([pod[0] + "/" + pod[1] for pod in mirror])
|
||||
warnings.append(
|
||||
"cannot delete mirror Pods using API server: {0}.".format(pod_names)
|
||||
)
|
||||
|
||||
# local storage
|
||||
if localStorage:
|
||||
errors.append("cannot delete Pods with local storage: {0}.".format(pod_names))
|
||||
pod_names = ",".join([pod[0] + "/" + pod[1] for pod in localStorage])
|
||||
if not delete_emptydir_data:
|
||||
errors.append(
|
||||
"cannot delete Pods with local storage: {0}.".format(pod_names)
|
||||
)
|
||||
else:
|
||||
warnings.append("Deleting Pods with local storage: {0}.".format(pod_names))
|
||||
for pod in localStorage:
|
||||
to_delete.append((pod[0], pod[1]))
|
||||
|
||||
# DaemonSet managed Pods
|
||||
if daemonSet:
|
||||
pod_names = ','.join([pod[0] + "/" + pod[1] for pod in daemonSet])
|
||||
pod_names = ",".join([pod[0] + "/" + pod[1] for pod in daemonSet])
|
||||
if not ignore_daemonset:
|
||||
errors.append("cannot delete DaemonSet-managed Pods (use option ignore_daemonset set to yes): {0}.".format(pod_names))
|
||||
errors.append(
|
||||
"cannot delete DaemonSet-managed Pods (use option ignore_daemonset set to yes): {0}.".format(
|
||||
pod_names
|
||||
)
|
||||
)
|
||||
else:
|
||||
warnings.append("Ignoring DaemonSet-managed Pods: {0}.".format(pod_names))
|
||||
return to_delete, warnings, errors
|
||||
|
||||
|
||||
class K8sDrainAnsible(object):
|
||||
|
||||
def __init__(self, module):
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
K8sAnsibleMixin, get_api_client)
|
||||
|
||||
def __init__(self, module, client):
|
||||
self._module = module
|
||||
self._k8s_ansible_mixin = K8sAnsibleMixin(module)
|
||||
self._k8s_ansible_mixin.client = get_api_client(module=self._module)
|
||||
|
||||
self._k8s_ansible_mixin.module = self._module
|
||||
self._k8s_ansible_mixin.argspec = self._module.argument_spec
|
||||
self._k8s_ansible_mixin.check_mode = self._module.check_mode
|
||||
self._k8s_ansible_mixin.params = self._module.params
|
||||
self._k8s_ansible_mixin.fail_json = self._module.fail_json
|
||||
self._k8s_ansible_mixin.fail = self._module.fail_json
|
||||
self._k8s_ansible_mixin.exit_json = self._module.exit_json
|
||||
self._k8s_ansible_mixin.warn = self._module.warn
|
||||
self._k8s_ansible_mixin.warnings = []
|
||||
|
||||
self._api_instance = core_v1_api.CoreV1Api(self._k8s_ansible_mixin.client.client)
|
||||
self._k8s_ansible_mixin.check_library_version()
|
||||
self._api_instance = core_v1_api.CoreV1Api(client.client)
|
||||
|
||||
# delete options
|
||||
self._drain_options = module.params.get('delete_options', {})
|
||||
self._drain_options = module.params.get("delete_options", {})
|
||||
self._delete_options = None
|
||||
if self._drain_options.get('terminate_grace_period'):
|
||||
self._delete_options = {}
|
||||
self._delete_options.update({'apiVersion': 'v1'})
|
||||
self._delete_options.update({'kind': 'DeleteOptions'})
|
||||
self._delete_options.update({'gracePeriodSeconds': self._drain_options.get('terminate_grace_period')})
|
||||
if self._drain_options.get("terminate_grace_period"):
|
||||
self._delete_options = V1DeleteOptions(
|
||||
grace_period_seconds=self._drain_options.get("terminate_grace_period")
|
||||
)
|
||||
|
||||
self._changed = False
|
||||
|
||||
@@ -241,13 +280,17 @@ class K8sDrainAnsible(object):
|
||||
if not pod:
|
||||
pod = pods.pop()
|
||||
try:
|
||||
response = self._api_instance.read_namespaced_pod(namespace=pod[0], name=pod[1])
|
||||
response = self._api_instance.read_namespaced_pod(
|
||||
namespace=pod[0], name=pod[1]
|
||||
)
|
||||
if not response:
|
||||
pod = None
|
||||
time.sleep(wait_sleep)
|
||||
except ApiException as exc:
|
||||
if exc.reason != "Not Found":
|
||||
self._module.fail_json(msg="Exception raised: {0}".format(exc.reason))
|
||||
self._module.fail_json(
|
||||
msg="Exception raised: {0}".format(exc.reason)
|
||||
)
|
||||
pod = None
|
||||
except Exception as e:
|
||||
self._module.fail_json(msg="Exception raised: {0}".format(to_native(e)))
|
||||
@@ -257,37 +300,49 @@ class K8sDrainAnsible(object):
|
||||
|
||||
def evict_pods(self, pods):
|
||||
for namespace, name in pods:
|
||||
definition = {
|
||||
'metadata': {
|
||||
'name': name,
|
||||
'namespace': namespace
|
||||
}
|
||||
}
|
||||
if self._delete_options:
|
||||
definition.update({'delete_options': self._delete_options})
|
||||
try:
|
||||
if self._drain_options.get('disable_eviction'):
|
||||
body = V1DeleteOptions(**definition)
|
||||
self._api_instance.delete_namespaced_pod(name=name, namespace=namespace, body=body)
|
||||
if self._drain_options.get("disable_eviction"):
|
||||
self._api_instance.delete_namespaced_pod(
|
||||
name=name, namespace=namespace, body=self._delete_options
|
||||
)
|
||||
else:
|
||||
body = V1beta1Eviction(**definition)
|
||||
self._api_instance.create_namespaced_pod_eviction(name=name, namespace=namespace, body=body)
|
||||
body = v1_eviction(
|
||||
delete_options=self._delete_options,
|
||||
metadata=V1ObjectMeta(name=name, namespace=namespace),
|
||||
)
|
||||
self._api_instance.create_namespaced_pod_eviction(
|
||||
name=name, namespace=namespace, body=body
|
||||
)
|
||||
self._changed = True
|
||||
except ApiException as exc:
|
||||
if exc.reason != "Not Found":
|
||||
self._module.fail_json(msg="Failed to delete pod {0}/{1} due to: {2}".format(namespace, name, exc.reason))
|
||||
self._module.fail_json(
|
||||
msg="Failed to delete pod {0}/{1} due to: {2}".format(
|
||||
namespace, name, exc.reason
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
self._module.fail_json(msg="Failed to delete pod {0}/{1} due to: {2}".format(namespace, name, to_native(exc)))
|
||||
self._module.fail_json(
|
||||
msg="Failed to delete pod {0}/{1} due to: {2}".format(
|
||||
namespace, name, to_native(exc)
|
||||
)
|
||||
)
|
||||
|
||||
def delete_or_evict_pods(self, node_unschedulable):
|
||||
# Mark node as unschedulable
|
||||
result = []
|
||||
if not node_unschedulable:
|
||||
self.patch_node(unschedulable=True)
|
||||
result.append("node {0} marked unschedulable.".format(self._module.params.get('name')))
|
||||
result.append(
|
||||
"node {0} marked unschedulable.".format(self._module.params.get("name"))
|
||||
)
|
||||
self._changed = True
|
||||
else:
|
||||
result.append("node {0} already marked unschedulable.".format(self._module.params.get('name')))
|
||||
result.append(
|
||||
"node {0} already marked unschedulable.".format(
|
||||
self._module.params.get("name")
|
||||
)
|
||||
)
|
||||
|
||||
def _revert_node_patch():
|
||||
if self._changed:
|
||||
@@ -295,77 +350,112 @@ class K8sDrainAnsible(object):
|
||||
self.patch_node(unschedulable=False)
|
||||
|
||||
try:
|
||||
field_selector = "spec.nodeName={name}".format(name=self._module.params.get('name'))
|
||||
pod_list = self._api_instance.list_pod_for_all_namespaces(field_selector=field_selector)
|
||||
field_selector = "spec.nodeName={name}".format(
|
||||
name=self._module.params.get("name")
|
||||
)
|
||||
pod_list = self._api_instance.list_pod_for_all_namespaces(
|
||||
field_selector=field_selector
|
||||
)
|
||||
# Filter pods
|
||||
force = self._drain_options.get('force', False)
|
||||
ignore_daemonset = self._drain_options.get('ignore_daemonsets', False)
|
||||
pods, warnings, errors = filter_pods(pod_list.items, force, ignore_daemonset)
|
||||
force = self._drain_options.get("force", False)
|
||||
ignore_daemonset = self._drain_options.get("ignore_daemonsets", False)
|
||||
delete_emptydir_data = self._drain_options.get(
|
||||
"delete_emptydir_data", False
|
||||
)
|
||||
pods, warnings, errors = filter_pods(
|
||||
pod_list.items, force, ignore_daemonset, delete_emptydir_data
|
||||
)
|
||||
if errors:
|
||||
_revert_node_patch()
|
||||
self._module.fail_json(msg="Pod deletion errors: {0}".format(" ".join(errors)))
|
||||
self._module.fail_json(
|
||||
msg="Pod deletion errors: {0}".format(" ".join(errors))
|
||||
)
|
||||
except ApiException as exc:
|
||||
if exc.reason != "Not Found":
|
||||
_revert_node_patch()
|
||||
self._module.fail_json(msg="Failed to list pod from node {name} due to: {reason}".format(
|
||||
name=self._module.params.get('name'), reason=exc.reason), status=exc.status)
|
||||
self._module.fail_json(
|
||||
msg="Failed to list pod from node {name} due to: {reason}".format(
|
||||
name=self._module.params.get("name"), reason=exc.reason
|
||||
),
|
||||
status=exc.status,
|
||||
)
|
||||
pods = []
|
||||
except Exception as exc:
|
||||
_revert_node_patch()
|
||||
self._module.fail_json(msg="Failed to list pod from node {name} due to: {error}".format(
|
||||
name=self._module.params.get('name'), error=to_native(exc)))
|
||||
self._module.fail_json(
|
||||
msg="Failed to list pod from node {name} due to: {error}".format(
|
||||
name=self._module.params.get("name"), error=to_native(exc)
|
||||
)
|
||||
)
|
||||
|
||||
# Delete Pods
|
||||
if pods:
|
||||
self.evict_pods(pods)
|
||||
number_pod = len(pods)
|
||||
if self._drain_options.get('wait_timeout') is not None:
|
||||
warn = self.wait_for_pod_deletion(pods,
|
||||
self._drain_options.get('wait_timeout'),
|
||||
self._drain_options.get('wait_sleep'))
|
||||
if self._drain_options.get("wait_timeout") is not None:
|
||||
warn = self.wait_for_pod_deletion(
|
||||
pods,
|
||||
self._drain_options.get("wait_timeout"),
|
||||
self._drain_options.get("wait_sleep"),
|
||||
)
|
||||
if warn:
|
||||
warnings.append(warn)
|
||||
result.append("{0} Pod(s) deleted from node.".format(number_pod))
|
||||
if warnings:
|
||||
return dict(result=' '.join(result), warnings=warnings)
|
||||
return dict(result=' '.join(result))
|
||||
return dict(result=" ".join(result), warnings=warnings)
|
||||
return dict(result=" ".join(result))
|
||||
|
||||
def patch_node(self, unschedulable):
|
||||
|
||||
body = {
|
||||
'spec': {'unschedulable': unschedulable}
|
||||
}
|
||||
body = {"spec": {"unschedulable": unschedulable}}
|
||||
try:
|
||||
self._api_instance.patch_node(name=self._module.params.get('name'), body=body)
|
||||
self._api_instance.patch_node(
|
||||
name=self._module.params.get("name"), body=body
|
||||
)
|
||||
except Exception as exc:
|
||||
self._module.fail_json(msg="Failed to patch node due to: {0}".format(to_native(exc)))
|
||||
self._module.fail_json(
|
||||
msg="Failed to patch node due to: {0}".format(to_native(exc))
|
||||
)
|
||||
|
||||
def execute_module(self):
|
||||
|
||||
state = self._module.params.get('state')
|
||||
name = self._module.params.get('name')
|
||||
state = self._module.params.get("state")
|
||||
name = self._module.params.get("name")
|
||||
try:
|
||||
node = self._api_instance.read_node(name=name)
|
||||
except ApiException as exc:
|
||||
if exc.reason == "Not Found":
|
||||
self._module.fail_json(msg="Node {0} not found.".format(name))
|
||||
self._module.fail_json(msg="Failed to retrieve node '{0}' due to: {1}".format(name, exc.reason), status=exc.status)
|
||||
self._module.fail_json(
|
||||
msg="Failed to retrieve node '{0}' due to: {1}".format(
|
||||
name, exc.reason
|
||||
),
|
||||
status=exc.status,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._module.fail_json(msg="Failed to retrieve node '{0}' due to: {1}".format(name, to_native(exc)))
|
||||
self._module.fail_json(
|
||||
msg="Failed to retrieve node '{0}' due to: {1}".format(
|
||||
name, to_native(exc)
|
||||
)
|
||||
)
|
||||
|
||||
result = {}
|
||||
if state == "cordon":
|
||||
if node.spec.unschedulable:
|
||||
self._module.exit_json(result="node {0} already marked unschedulable.".format(name))
|
||||
self._module.exit_json(
|
||||
result="node {0} already marked unschedulable.".format(name)
|
||||
)
|
||||
self.patch_node(unschedulable=True)
|
||||
result['result'] = "node {0} marked unschedulable.".format(name)
|
||||
result["result"] = "node {0} marked unschedulable.".format(name)
|
||||
self._changed = True
|
||||
|
||||
elif state == "uncordon":
|
||||
if not node.spec.unschedulable:
|
||||
self._module.exit_json(result="node {0} already marked schedulable.".format(name))
|
||||
self._module.exit_json(
|
||||
result="node {0} already marked schedulable.".format(name)
|
||||
)
|
||||
self.patch_node(unschedulable=False)
|
||||
result['result'] = "node {0} marked schedulable.".format(name)
|
||||
result["result"] = "node {0} marked schedulable.".format(name)
|
||||
self._changed = True
|
||||
|
||||
else:
|
||||
@@ -384,16 +474,17 @@ def argspec():
|
||||
state=dict(default="drain", choices=["cordon", "drain", "uncordon"]),
|
||||
name=dict(required=True),
|
||||
delete_options=dict(
|
||||
type='dict',
|
||||
type="dict",
|
||||
default={},
|
||||
options=dict(
|
||||
terminate_grace_period=dict(type='int'),
|
||||
force=dict(type='bool', default=False),
|
||||
ignore_daemonsets=dict(type='bool', default=False),
|
||||
disable_eviction=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int'),
|
||||
wait_sleep=dict(type='int', default=5),
|
||||
)
|
||||
terminate_grace_period=dict(type="int"),
|
||||
force=dict(type="bool", default=False),
|
||||
ignore_daemonsets=dict(type="bool", default=False),
|
||||
delete_emptydir_data=dict(type="bool", default=False),
|
||||
disable_eviction=dict(type="bool", default=False),
|
||||
wait_timeout=dict(type="int"),
|
||||
wait_sleep=dict(type="int", default=5),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -401,11 +492,22 @@ def argspec():
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(argument_spec=argspec())
|
||||
module = AnsibleK8SModule(module_class=AnsibleModule, argument_spec=argspec())
|
||||
|
||||
k8s_drain = K8sDrainAnsible(module)
|
||||
k8s_drain.execute_module()
|
||||
if not HAS_EVICTION_API:
|
||||
module.fail_json(
|
||||
msg="The kubernetes Python library missing with V1Eviction API",
|
||||
exception=K8S_IMP_ERR,
|
||||
error=to_native(k8s_import_exception),
|
||||
)
|
||||
|
||||
try:
|
||||
client = get_api_client(module=module)
|
||||
k8s_drain = K8sDrainAnsible(module, client.client)
|
||||
k8s_drain.execute_module()
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
|
||||
module: k8s_exec
|
||||
|
||||
@@ -40,32 +40,33 @@ options:
|
||||
description:
|
||||
- The URL of an HTTP proxy to use for the connection.
|
||||
- Can also be specified via I(K8S_AUTH_PROXY) environment variable.
|
||||
- Please note that this module does not pick up typical proxy settings from the environment (e.g. HTTP_PROXY).
|
||||
- Please note that this module does not pick up typical proxy settings from the environment (for example, HTTP_PROXY).
|
||||
type: str
|
||||
namespace:
|
||||
description:
|
||||
- The pod namespace name
|
||||
- The pod namespace name.
|
||||
type: str
|
||||
required: yes
|
||||
pod:
|
||||
description:
|
||||
- The pod name
|
||||
- The pod name.
|
||||
type: str
|
||||
required: yes
|
||||
container:
|
||||
description:
|
||||
- The name of the container in the pod to connect to.
|
||||
- Defaults to only container if there is only one container in the pod.
|
||||
- If not specified, will choose the first container from the given pod as kubectl cmdline does.
|
||||
type: str
|
||||
required: no
|
||||
command:
|
||||
description:
|
||||
- The command to execute
|
||||
- The command to execute.
|
||||
type: str
|
||||
required: yes
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Execute a command
|
||||
kubernetes.core.k8s_exec:
|
||||
namespace: myproject
|
||||
@@ -84,9 +85,16 @@ EXAMPLES = r'''
|
||||
debug:
|
||||
msg: "cmd failed"
|
||||
when: command_status.rc != 0
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
- name: Specify a container name to execute the command on
|
||||
kubernetes.core.k8s_exec:
|
||||
namespace: myproject
|
||||
pod: busybox-test
|
||||
container: manager
|
||||
command: echo "hello"
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
result:
|
||||
description:
|
||||
- The command object
|
||||
@@ -112,7 +120,7 @@ result:
|
||||
return_code:
|
||||
description: The command status code. This attribute is deprecated and will be removed in a future release. Please use rc instead.
|
||||
type: int
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
import shlex
|
||||
@@ -123,15 +131,27 @@ except ImportError:
|
||||
# ImportError are managed by the common module already.
|
||||
pass
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
AUTH_ARG_SPEC
|
||||
AUTH_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
|
||||
try:
|
||||
from kubernetes.client.apis import core_v1_api
|
||||
from kubernetes.stream import stream
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
except ImportError:
|
||||
# ImportError are managed by the common module already.
|
||||
pass
|
||||
@@ -139,22 +159,34 @@ except ImportError:
|
||||
|
||||
def argspec():
|
||||
spec = copy.deepcopy(AUTH_ARG_SPEC)
|
||||
spec['namespace'] = dict(type='str', required=True)
|
||||
spec['pod'] = dict(type='str', required=True)
|
||||
spec['container'] = dict(type='str')
|
||||
spec['command'] = dict(type='str', required=True)
|
||||
spec["namespace"] = dict(type="str", required=True)
|
||||
spec["pod"] = dict(type="str", required=True)
|
||||
spec["container"] = dict(type="str")
|
||||
spec["command"] = dict(type="str", required=True)
|
||||
return spec
|
||||
|
||||
|
||||
def execute_module(module, k8s_ansible_mixin):
|
||||
|
||||
def execute_module(module, client):
|
||||
# Load kubernetes.client.Configuration
|
||||
api = core_v1_api.CoreV1Api(k8s_ansible_mixin.client.client)
|
||||
api = core_v1_api.CoreV1Api(client.client)
|
||||
|
||||
# hack because passing the container as None breaks things
|
||||
optional_kwargs = {}
|
||||
if module.params.get('container'):
|
||||
optional_kwargs['container'] = module.params['container']
|
||||
if module.params.get("container"):
|
||||
optional_kwargs["container"] = module.params["container"]
|
||||
else:
|
||||
# default to the first container available on pod
|
||||
resp = None
|
||||
try:
|
||||
resp = api.read_namespaced_pod(
|
||||
name=module.params["pod"], namespace=module.params["namespace"]
|
||||
)
|
||||
except ApiException:
|
||||
pass
|
||||
|
||||
if resp and len(resp.spec.containers) >= 1:
|
||||
optional_kwargs["container"] = resp.spec.containers[0].name
|
||||
|
||||
try:
|
||||
resp = stream(
|
||||
api.connect_get_namespaced_pod_exec,
|
||||
@@ -165,10 +197,14 @@ def execute_module(module, k8s_ansible_mixin):
|
||||
stderr=True,
|
||||
stdin=False,
|
||||
tty=False,
|
||||
_preload_content=False, **optional_kwargs)
|
||||
_preload_content=False,
|
||||
**optional_kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to execute on pod %s"
|
||||
" due to : %s" % (module.params.get('pod'), to_native(e)))
|
||||
module.fail_json(
|
||||
msg="Failed to execute on pod %s"
|
||||
" due to : %s" % (module.params.get("pod"), to_native(e))
|
||||
)
|
||||
stdout, stderr, rc = [], [], 0
|
||||
while resp.is_open():
|
||||
resp.update(timeout=1)
|
||||
@@ -178,34 +214,41 @@ def execute_module(module, k8s_ansible_mixin):
|
||||
stderr.append(resp.read_stderr())
|
||||
err = resp.read_channel(3)
|
||||
err = yaml.safe_load(err)
|
||||
if err['status'] == 'Success':
|
||||
if err["status"] == "Success":
|
||||
rc = 0
|
||||
else:
|
||||
rc = int(err['details']['causes'][0]['message'])
|
||||
rc = int(err["details"]["causes"][0]["message"])
|
||||
|
||||
module.deprecate("The 'return_code' return key is deprecated. Please use 'rc' instead.", version="4.0.0", collection_name="kubernetes.core")
|
||||
module.deprecate(
|
||||
"The 'return_code' return key is being renamed to 'rc'. "
|
||||
"Both keys are being returned for now to allow users to migrate their automation.",
|
||||
version="4.0.0",
|
||||
collection_name="kubernetes.core",
|
||||
)
|
||||
module.exit_json(
|
||||
# Some command might change environment, but ultimately failing at end
|
||||
changed=True,
|
||||
stdout="".join(stdout),
|
||||
stderr="".join(stderr),
|
||||
rc=rc,
|
||||
return_code=rc
|
||||
return_code=rc,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule,
|
||||
check_pyyaml=False,
|
||||
argument_spec=argspec(),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
K8sAnsibleMixin, get_api_client)
|
||||
|
||||
k8s_ansible_mixin = K8sAnsibleMixin(module)
|
||||
k8s_ansible_mixin.client = get_api_client(module=module)
|
||||
execute_module(module, k8s_ansible_mixin)
|
||||
try:
|
||||
client = get_api_client(module)
|
||||
execute_module(module, client.client)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
module: k8s_info
|
||||
|
||||
short_description: Describe Kubernetes (K8s) objects
|
||||
@@ -38,10 +38,12 @@ options:
|
||||
description: List of label selectors to use to filter results
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
field_selectors:
|
||||
description: List of field selectors to use to filter results
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
|
||||
extends_documentation_fragment:
|
||||
- kubernetes.core.k8s_auth_options
|
||||
@@ -52,9 +54,9 @@ requirements:
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Get an existing Service object
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
@@ -109,9 +111,9 @@ EXAMPLES = r'''
|
||||
namespace: default
|
||||
wait_sleep: 10
|
||||
wait_timeout: 360
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
api_found:
|
||||
description:
|
||||
- Whether the specified api_version and kind were successfully mapped to an existing API on the targeted cluster.
|
||||
@@ -144,16 +146,33 @@ resources:
|
||||
description: Current status details for the object.
|
||||
returned: success
|
||||
type: dict
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_SPEC, WAIT_ARG_SPEC)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC,
|
||||
WAIT_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
K8sService,
|
||||
)
|
||||
|
||||
|
||||
def execute_module(module, k8s_ansible_mixin):
|
||||
facts = k8s_ansible_mixin.kubernetes_facts(
|
||||
def execute_module(module, svc):
|
||||
facts = svc.find(
|
||||
module.params["kind"],
|
||||
module.params["api_version"],
|
||||
name=module.params["name"],
|
||||
@@ -174,25 +193,27 @@ def argspec():
|
||||
args.update(
|
||||
dict(
|
||||
kind=dict(required=True),
|
||||
api_version=dict(default='v1', aliases=['api', 'version']),
|
||||
api_version=dict(default="v1", aliases=["api", "version"]),
|
||||
name=dict(),
|
||||
namespace=dict(),
|
||||
label_selectors=dict(type='list', elements='str', default=[]),
|
||||
field_selectors=dict(type='list', elements='str', default=[]),
|
||||
label_selectors=dict(type="list", elements="str", default=[]),
|
||||
field_selectors=dict(type="list", elements="str", default=[]),
|
||||
)
|
||||
)
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
K8sAnsibleMixin, get_api_client)
|
||||
|
||||
k8s_ansible_mixin = K8sAnsibleMixin(module)
|
||||
k8s_ansible_mixin.client = get_api_client(module=module)
|
||||
execute_module(module, k8s_ansible_mixin)
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True
|
||||
)
|
||||
try:
|
||||
client = get_api_client(module)
|
||||
svc = K8sService(client, module)
|
||||
execute_module(module, svc)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
module: k8s_json_patch
|
||||
|
||||
short_description: Apply JSON patch operations to existing objects
|
||||
@@ -66,9 +66,9 @@ requirements:
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
- "jsonpatch"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Apply multiple patch operations to an existing Pod
|
||||
kubernetes.core.k8s_json_patch:
|
||||
kind: Pod
|
||||
@@ -79,11 +79,11 @@ EXAMPLES = r'''
|
||||
path: /metadata/labels/app
|
||||
value: myapp
|
||||
- op: replace
|
||||
patch: /spec/containers/0/image
|
||||
path: /spec/containers/0/image
|
||||
value: nginx
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
result:
|
||||
description: The modified object.
|
||||
returned: success
|
||||
@@ -122,17 +122,36 @@ error:
|
||||
"msg": "Failed to import the required Python library (jsonpatch) ...",
|
||||
"exception": "Traceback (most recent call last): ..."
|
||||
}
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, WAIT_ARG_SPEC
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
get_api_client, K8sAnsibleMixin)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC,
|
||||
WAIT_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
diff_objects,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import (
|
||||
get_waiter,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import DynamicApiError
|
||||
@@ -143,6 +162,7 @@ except ImportError:
|
||||
JSON_PATCH_IMPORT_ERR = None
|
||||
try:
|
||||
import jsonpatch
|
||||
|
||||
HAS_JSON_PATCH = True
|
||||
except ImportError:
|
||||
HAS_JSON_PATCH = False
|
||||
@@ -150,33 +170,18 @@ except ImportError:
|
||||
|
||||
|
||||
JSON_PATCH_ARGS = {
|
||||
'api_version': {
|
||||
'default': 'v1',
|
||||
'aliases': ['api', 'version'],
|
||||
},
|
||||
"kind": {
|
||||
"type": "str",
|
||||
"required": True,
|
||||
},
|
||||
"namespace": {
|
||||
"type": "str",
|
||||
},
|
||||
"name": {
|
||||
"type": "str",
|
||||
"required": True,
|
||||
},
|
||||
"patch": {
|
||||
"type": "list",
|
||||
"required": True,
|
||||
"elements": "dict",
|
||||
},
|
||||
"api_version": {"default": "v1", "aliases": ["api", "version"]},
|
||||
"kind": {"type": "str", "required": True},
|
||||
"namespace": {"type": "str"},
|
||||
"name": {"type": "str", "required": True},
|
||||
"patch": {"type": "list", "required": True, "elements": "dict"},
|
||||
}
|
||||
|
||||
|
||||
def json_patch(existing, patch):
|
||||
if not HAS_JSON_PATCH:
|
||||
error = {
|
||||
"msg": missing_required_lib('jsonpatch'),
|
||||
"msg": missing_required_lib("jsonpatch"),
|
||||
"exception": JSON_PATCH_IMPORT_ERR,
|
||||
}
|
||||
return None, error
|
||||
@@ -185,20 +190,14 @@ def json_patch(existing, patch):
|
||||
patched = patch.apply(existing)
|
||||
return patched, None
|
||||
except jsonpatch.InvalidJsonPatch as e:
|
||||
error = {
|
||||
"msg": "Invalid JSON patch",
|
||||
"exception": e
|
||||
}
|
||||
error = {"msg": "Invalid JSON patch", "exception": e}
|
||||
return None, error
|
||||
except jsonpatch.JsonPatchConflict as e:
|
||||
error = {
|
||||
"msg": "Patch could not be applied due to a conflict",
|
||||
"exception": e
|
||||
}
|
||||
error = {"msg": "Patch could not be applied due to a conflict", "exception": e}
|
||||
return None, error
|
||||
|
||||
|
||||
def execute_module(k8s_module, module):
|
||||
def execute_module(module, client):
|
||||
kind = module.params.get("kind")
|
||||
api_version = module.params.get("api_version")
|
||||
name = module.params.get("name")
|
||||
@@ -209,50 +208,66 @@ def execute_module(k8s_module, module):
|
||||
wait_sleep = module.params.get("wait_sleep")
|
||||
wait_timeout = module.params.get("wait_timeout")
|
||||
wait_condition = None
|
||||
if module.params.get("wait_condition") and module.params.get("wait_condition").get("type"):
|
||||
wait_condition = module.params['wait_condition']
|
||||
# definition is needed for wait
|
||||
definition = {
|
||||
"kind": kind,
|
||||
"metadata": {
|
||||
"name": name,
|
||||
"namespace": namespace,
|
||||
}
|
||||
}
|
||||
if module.params.get("wait_condition") and module.params.get("wait_condition").get(
|
||||
"type"
|
||||
):
|
||||
wait_condition = module.params["wait_condition"]
|
||||
|
||||
def build_error_msg(kind, name, msg):
|
||||
return "%s %s: %s" % (kind, name, msg)
|
||||
|
||||
resource = k8s_module.find_resource(kind, api_version, fail=True)
|
||||
resource = client.resource(kind, api_version)
|
||||
|
||||
try:
|
||||
existing = resource.get(name=name, namespace=namespace)
|
||||
existing = client.get(resource, name=name, namespace=namespace)
|
||||
except DynamicApiError as exc:
|
||||
msg = 'Failed to retrieve requested object: {0}'.format(exc.body)
|
||||
module.fail_json(msg=build_error_msg(kind, name, msg), error=exc.status, status=exc.status, reason=exc.reason)
|
||||
msg = "Failed to retrieve requested object: {0}".format(exc.body)
|
||||
module.fail_json(
|
||||
msg=build_error_msg(kind, name, msg),
|
||||
error=exc.status,
|
||||
status=exc.status,
|
||||
reason=exc.reason,
|
||||
)
|
||||
except ValueError as exc:
|
||||
msg = 'Failed to retrieve requested object: {0}'.format(to_native(exc))
|
||||
module.fail_json(msg=build_error_msg(kind, name, msg), error='', status='', reason='')
|
||||
msg = "Failed to retrieve requested object: {0}".format(to_native(exc))
|
||||
module.fail_json(
|
||||
msg=build_error_msg(kind, name, msg), error="", status="", reason=""
|
||||
)
|
||||
|
||||
if module.check_mode:
|
||||
if module.check_mode and not client.dry_run:
|
||||
obj, error = json_patch(existing.to_dict(), patch)
|
||||
if error:
|
||||
module.fail_json(**error)
|
||||
else:
|
||||
params = {}
|
||||
if module.check_mode:
|
||||
params["dry_run"] = "All"
|
||||
try:
|
||||
obj = resource.patch(patch, name=name, namespace=namespace, content_type="application/json-patch+json").to_dict()
|
||||
obj = client.patch(
|
||||
resource,
|
||||
patch,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
content_type="application/json-patch+json",
|
||||
**params
|
||||
).to_dict()
|
||||
except DynamicApiError as exc:
|
||||
msg = "Failed to patch existing object: {0}".format(exc.body)
|
||||
module.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)
|
||||
module.fail_json(
|
||||
msg=msg, error=exc.status, status=exc.status, reason=exc.reason
|
||||
)
|
||||
except Exception as exc:
|
||||
msg = "Failed to patch existing object: {0}".format(exc)
|
||||
module.fail_json(msg=msg, error=to_native(exc), status='', reason='')
|
||||
module.fail_json(msg=msg, error=to_native(exc), status="", reason="")
|
||||
|
||||
success = True
|
||||
result = {"result": obj}
|
||||
if wait and not module.check_mode:
|
||||
success, result['result'], result['duration'] = k8s_module.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition)
|
||||
match, diffs = k8s_module.diff_objects(existing.to_dict(), obj)
|
||||
waiter = get_waiter(client, resource, condition=wait_condition)
|
||||
success, result["result"], result["duration"] = waiter.wait(
|
||||
wait_timeout, wait_sleep, name, namespace
|
||||
)
|
||||
match, diffs = diff_objects(existing.to_dict(), obj)
|
||||
result["changed"] = not match
|
||||
if module._diff:
|
||||
result["diff"] = diffs
|
||||
@@ -268,13 +283,14 @@ def main():
|
||||
args = copy.deepcopy(AUTH_ARG_SPEC)
|
||||
args.update(copy.deepcopy(WAIT_ARG_SPEC))
|
||||
args.update(JSON_PATCH_ARGS)
|
||||
module = AnsibleModule(argument_spec=args, supports_check_mode=True)
|
||||
k8s_module = K8sAnsibleMixin(module)
|
||||
k8s_module.params = module.params
|
||||
k8s_module.check_library_version()
|
||||
client = get_api_client(module)
|
||||
k8s_module.client = client
|
||||
execute_module(k8s_module, module)
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule, argument_spec=args, supports_check_mode=True
|
||||
)
|
||||
try:
|
||||
client = get_api_client(module)
|
||||
execute_module(module, client)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
module: k8s_log
|
||||
|
||||
short_description: Fetch logs from Kubernetes resources
|
||||
@@ -47,11 +47,13 @@ options:
|
||||
- Only one of I(name) or I(label_selectors) may be provided.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
container:
|
||||
description:
|
||||
- Use to specify the container within a pod to grab the log from.
|
||||
- If there is only one container, this will default to that container.
|
||||
- If there is more than one container, this option is required.
|
||||
- If there is more than one container, this option is required or set I(all_containers) to C(true).
|
||||
- mutually exclusive with C(all_containers).
|
||||
required: no
|
||||
type: str
|
||||
since_seconds:
|
||||
@@ -60,14 +62,33 @@ options:
|
||||
required: no
|
||||
type: str
|
||||
version_added: '2.2.0'
|
||||
previous:
|
||||
description:
|
||||
- If C(true), print the logs for the previous instance of the container in a pod if it exists.
|
||||
required: no
|
||||
type: bool
|
||||
default: False
|
||||
version_added: '2.4.0'
|
||||
tail_lines:
|
||||
description:
|
||||
- A number of lines from the end of the logs to retrieve.
|
||||
required: no
|
||||
type: int
|
||||
version_added: '2.4.0'
|
||||
all_containers:
|
||||
description:
|
||||
- If set to C(true), retrieve all containers' logs in the pod(s).
|
||||
- mutually exclusive with C(container).
|
||||
type: bool
|
||||
version_added: '2.4.0'
|
||||
|
||||
requirements:
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Get a log from a Pod
|
||||
kubernetes.core.k8s_log:
|
||||
name: example-1
|
||||
@@ -99,10 +120,18 @@ EXAMPLES = r'''
|
||||
kind: DeploymentConfig
|
||||
namespace: testing
|
||||
name: example
|
||||
tail_lines: 100
|
||||
register: log
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
# This will get the logs from all containers in Pod
|
||||
- name: Get the logs from all containers in pod
|
||||
kubernetes.core.k8s_log:
|
||||
namespace: testing
|
||||
name: some-pod
|
||||
all_containers: true
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
log:
|
||||
type: str
|
||||
description:
|
||||
@@ -113,15 +142,37 @@ log_lines:
|
||||
description:
|
||||
- The log of the object, split on newlines
|
||||
returned: success
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible.module_utils.six import PY2
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC,
|
||||
NAME_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
K8sService,
|
||||
)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_SPEC, NAME_ARG_SPEC)
|
||||
try:
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
except ImportError:
|
||||
# ImportError are managed by the common module already.
|
||||
pass
|
||||
|
||||
|
||||
def argspec():
|
||||
@@ -129,110 +180,184 @@ def argspec():
|
||||
args.update(NAME_ARG_SPEC)
|
||||
args.update(
|
||||
dict(
|
||||
kind=dict(type='str', default='Pod'),
|
||||
kind=dict(type="str", default="Pod"),
|
||||
container=dict(),
|
||||
since_seconds=dict(),
|
||||
label_selectors=dict(type='list', elements='str', default=[]),
|
||||
label_selectors=dict(type="list", elements="str", default=[]),
|
||||
previous=dict(type="bool", default=False),
|
||||
tail_lines=dict(type="int"),
|
||||
all_containers=dict(type="bool"),
|
||||
)
|
||||
)
|
||||
return args
|
||||
|
||||
|
||||
def execute_module(module, k8s_ansible_mixin):
|
||||
name = module.params.get('name')
|
||||
namespace = module.params.get('namespace')
|
||||
label_selector = ','.join(module.params.get('label_selectors', {}))
|
||||
def get_exception_message(exc):
|
||||
try:
|
||||
d = json.loads(exc.body.decode("utf8"))
|
||||
return d["message"]
|
||||
except Exception:
|
||||
return exc
|
||||
|
||||
|
||||
def list_containers_in_pod(svc, resource, namespace, name):
|
||||
try:
|
||||
result = svc.client.get(resource, name=name, namespace=namespace)
|
||||
containers = [
|
||||
c["name"] for c in result.to_dict()["status"]["containerStatuses"]
|
||||
]
|
||||
return containers
|
||||
except Exception as exc:
|
||||
raise CoreException(
|
||||
"Unable to retrieve log from Pod due to: {0}".format(
|
||||
get_exception_message(exc)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def execute_module(svc, params):
|
||||
name = params.get("name")
|
||||
namespace = params.get("namespace")
|
||||
label_selector = ",".join(params.get("label_selectors", {}))
|
||||
if name and label_selector:
|
||||
module.fail(msg='Only one of name or label_selectors can be provided')
|
||||
raise CoreException("Only one of name or label_selectors can be provided")
|
||||
|
||||
resource = k8s_ansible_mixin.find_resource(module.params['kind'], module.params['api_version'], fail=True)
|
||||
v1_pods = k8s_ansible_mixin.find_resource('Pod', 'v1', fail=True)
|
||||
resource = svc.find_resource(params["kind"], params["api_version"], fail=True)
|
||||
v1_pods = svc.find_resource("Pod", "v1", fail=True)
|
||||
|
||||
if 'log' not in resource.subresources:
|
||||
if "log" not in resource.subresources:
|
||||
if not name:
|
||||
module.fail(msg='name must be provided for resources that do not support the log subresource')
|
||||
raise CoreException(
|
||||
"name must be provided for resources that do not support the log subresource"
|
||||
)
|
||||
instance = resource.get(name=name, namespace=namespace)
|
||||
label_selector = ','.join(extract_selectors(module, instance))
|
||||
label_selector = ",".join(extract_selectors(instance))
|
||||
resource = v1_pods
|
||||
|
||||
if label_selector:
|
||||
instances = v1_pods.get(namespace=namespace, label_selector=label_selector)
|
||||
if not instances.items:
|
||||
module.fail(msg='No pods in namespace {0} matched selector {1}'.format(namespace, label_selector))
|
||||
raise CoreException(
|
||||
"No pods in namespace {0} matched selector {1}".format(
|
||||
namespace, label_selector
|
||||
)
|
||||
)
|
||||
# This matches the behavior of kubectl when logging pods via a selector
|
||||
name = instances.items[0].metadata.name
|
||||
resource = v1_pods
|
||||
|
||||
if "base" not in resource.log.urls and not name:
|
||||
raise CoreException(
|
||||
"name must be provided for resources that do not support namespaced base url"
|
||||
)
|
||||
|
||||
kwargs = {}
|
||||
if module.params.get('container'):
|
||||
kwargs['query_params'] = dict(container=module.params['container'])
|
||||
if params.get("container"):
|
||||
kwargs["query_params"] = {"container": params["container"]}
|
||||
|
||||
if module.params.get('since_seconds'):
|
||||
kwargs.setdefault('query_params', {}).update({'sinceSeconds': module.params['since_seconds']})
|
||||
if params.get("since_seconds"):
|
||||
kwargs.setdefault("query_params", {}).update(
|
||||
{"sinceSeconds": params["since_seconds"]}
|
||||
)
|
||||
|
||||
log = serialize_log(resource.log.get(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
serialize=False,
|
||||
**kwargs
|
||||
))
|
||||
if params.get("previous"):
|
||||
kwargs.setdefault("query_params", {}).update({"previous": params["previous"]})
|
||||
|
||||
module.exit_json(changed=False, log=log, log_lines=log.split('\n'))
|
||||
if params.get("tail_lines"):
|
||||
kwargs.setdefault("query_params", {}).update(
|
||||
{"tailLines": params["tail_lines"]}
|
||||
)
|
||||
|
||||
pod_containers = [None]
|
||||
if params.get("all_containers"):
|
||||
pod_containers = list_containers_in_pod(svc, resource, namespace, name)
|
||||
|
||||
log = ""
|
||||
try:
|
||||
for container in pod_containers:
|
||||
if container is not None:
|
||||
kwargs.setdefault("query_params", {}).update({"container": container})
|
||||
response = resource.log.get(
|
||||
name=name, namespace=namespace, serialize=False, **kwargs
|
||||
)
|
||||
log += response.data.decode("utf8")
|
||||
except ApiException as exc:
|
||||
if exc.reason == "Not Found":
|
||||
raise CoreException("Pod {0}/{1} not found.".format(namespace, name))
|
||||
raise CoreException(
|
||||
"Unable to retrieve log from Pod due to: {0}".format(
|
||||
get_exception_message(exc)
|
||||
)
|
||||
)
|
||||
|
||||
return {"changed": False, "log": log, "log_lines": log.split("\n")}
|
||||
|
||||
|
||||
def extract_selectors(module, instance):
|
||||
def extract_selectors(instance):
|
||||
# Parses selectors on an object based on the specifications documented here:
|
||||
# https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
|
||||
selectors = []
|
||||
if not instance.spec.selector:
|
||||
module.fail(msg='{0} {1} does not support the log subresource directly, and no Pod selector was found on the object'.format(
|
||||
'/'.join(instance.group, instance.apiVersion), instance.kind))
|
||||
raise CoreException(
|
||||
"{0} {1} does not support the log subresource directly, and no Pod selector was found on the object".format(
|
||||
"/".join(instance.group, instance.apiVersion), instance.kind
|
||||
)
|
||||
)
|
||||
|
||||
if not (instance.spec.selector.matchLabels or instance.spec.selector.matchExpressions):
|
||||
if not (
|
||||
instance.spec.selector.matchLabels or instance.spec.selector.matchExpressions
|
||||
):
|
||||
# A few resources (like DeploymentConfigs) just use a simple key:value style instead of supporting expressions
|
||||
for k, v in dict(instance.spec.selector).items():
|
||||
selectors.append('{0}={1}'.format(k, v))
|
||||
selectors.append("{0}={1}".format(k, v))
|
||||
return selectors
|
||||
|
||||
if instance.spec.selector.matchLabels:
|
||||
for k, v in dict(instance.spec.selector.matchLabels).items():
|
||||
selectors.append('{0}={1}'.format(k, v))
|
||||
selectors.append("{0}={1}".format(k, v))
|
||||
|
||||
if instance.spec.selector.matchExpressions:
|
||||
for expression in instance.spec.selector.matchExpressions:
|
||||
operator = expression.operator
|
||||
|
||||
if operator == 'Exists':
|
||||
if operator == "Exists":
|
||||
selectors.append(expression.key)
|
||||
elif operator == 'DoesNotExist':
|
||||
selectors.append('!{0}'.format(expression.key))
|
||||
elif operator in ['In', 'NotIn']:
|
||||
selectors.append('{key} {operator} {values}'.format(
|
||||
key=expression.key,
|
||||
operator=operator.lower(),
|
||||
values='({0})'.format(', '.join(expression.values))
|
||||
))
|
||||
elif operator == "DoesNotExist":
|
||||
selectors.append("!{0}".format(expression.key))
|
||||
elif operator in ["In", "NotIn"]:
|
||||
selectors.append(
|
||||
"{key} {operator} {values}".format(
|
||||
key=expression.key,
|
||||
operator=operator.lower(),
|
||||
values="({0})".format(", ".join(expression.values)),
|
||||
)
|
||||
)
|
||||
else:
|
||||
module.fail(msg='The k8s_log module does not support the {0} matchExpression operator'.format(operator.lower()))
|
||||
raise CoreException(
|
||||
"The k8s_log module does not support the {0} matchExpression operator".format(
|
||||
operator.lower()
|
||||
)
|
||||
)
|
||||
|
||||
return selectors
|
||||
|
||||
|
||||
def serialize_log(response):
|
||||
if PY2:
|
||||
return response.data
|
||||
return response.data.decode('utf8')
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
K8sAnsibleMixin, get_api_client)
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule,
|
||||
argument_spec=argspec(),
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[("container", "all_containers")],
|
||||
)
|
||||
|
||||
k8s_ansible_mixin = K8sAnsibleMixin(module)
|
||||
k8s_ansible_mixin.client = get_api_client(module=module)
|
||||
execute_module(module, k8s_ansible_mixin)
|
||||
try:
|
||||
client = get_api_client(module=module)
|
||||
svc = K8sService(client, module)
|
||||
result = execute_module(svc, module.params)
|
||||
module.exit_json(**result)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
# 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'''
|
||||
DOCUMENTATION = r"""
|
||||
module: k8s_rollback
|
||||
short_description: Rollback Kubernetes (K8S) Deployments and DaemonSets
|
||||
version_added: "1.0.0"
|
||||
@@ -23,10 +24,12 @@ options:
|
||||
description: List of label selectors to use to filter results.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
field_selectors:
|
||||
description: List of field selectors to use to filter results.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
extends_documentation_fragment:
|
||||
- kubernetes.core.k8s_auth_options
|
||||
- kubernetes.core.k8s_name_options
|
||||
@@ -34,18 +37,18 @@ requirements:
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Rollback a failed deployment
|
||||
kubernetes.core.k8s_rollback:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
name: web
|
||||
namespace: testing
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
rollback_info:
|
||||
description:
|
||||
- The object that was rolled back.
|
||||
@@ -74,108 +77,148 @@ rollback_info:
|
||||
description: Current status details for the object.
|
||||
returned: success
|
||||
type: dict
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC, NAME_ARG_SPEC)
|
||||
AUTH_ARG_SPEC,
|
||||
NAME_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
K8sService,
|
||||
)
|
||||
|
||||
|
||||
def get_managed_resource(module):
|
||||
def get_managed_resource(kind):
|
||||
managed_resource = {}
|
||||
|
||||
kind = module.params['kind']
|
||||
if kind == "DaemonSet":
|
||||
managed_resource['kind'] = "ControllerRevision"
|
||||
managed_resource['api_version'] = "apps/v1"
|
||||
managed_resource["kind"] = "ControllerRevision"
|
||||
managed_resource["api_version"] = "apps/v1"
|
||||
elif kind == "Deployment":
|
||||
managed_resource['kind'] = "ReplicaSet"
|
||||
managed_resource['api_version'] = "apps/v1"
|
||||
managed_resource["kind"] = "ReplicaSet"
|
||||
managed_resource["api_version"] = "apps/v1"
|
||||
else:
|
||||
module.fail(msg="Cannot perform rollback on resource of kind {0}".format(kind))
|
||||
raise CoreException(
|
||||
"Cannot perform rollback on resource of kind {0}".format(kind)
|
||||
)
|
||||
return managed_resource
|
||||
|
||||
|
||||
def execute_module(module, k8s_ansible_mixin):
|
||||
def execute_module(svc):
|
||||
results = []
|
||||
module = svc.module
|
||||
|
||||
resources = k8s_ansible_mixin.kubernetes_facts(
|
||||
module.params['kind'],
|
||||
module.params['api_version'],
|
||||
module.params['name'],
|
||||
module.params['namespace'],
|
||||
module.params['label_selectors'],
|
||||
module.params['field_selectors'])
|
||||
resources = svc.find(
|
||||
module.params["kind"],
|
||||
module.params["api_version"],
|
||||
module.params["name"],
|
||||
module.params["namespace"],
|
||||
module.params["label_selectors"],
|
||||
module.params["field_selectors"],
|
||||
)
|
||||
|
||||
for resource in resources['resources']:
|
||||
result = perform_action(module, k8s_ansible_mixin, resource)
|
||||
changed = False
|
||||
for resource in resources["resources"]:
|
||||
result = perform_action(svc, resource)
|
||||
changed = result["changed"] or changed
|
||||
results.append(result)
|
||||
|
||||
module.exit_json(**{
|
||||
'changed': True,
|
||||
'rollback_info': results
|
||||
})
|
||||
module.exit_json(**{"changed": changed, "rollback_info": results})
|
||||
|
||||
|
||||
def perform_action(module, k8s_ansible_mixin, resource):
|
||||
if module.params['kind'] == "DaemonSet":
|
||||
current_revision = resource['metadata']['generation']
|
||||
elif module.params['kind'] == "Deployment":
|
||||
current_revision = resource['metadata']['annotations']['deployment.kubernetes.io/revision']
|
||||
def perform_action(svc, resource):
|
||||
module = svc.module
|
||||
|
||||
managed_resource = get_managed_resource(module)
|
||||
managed_resources = k8s_ansible_mixin.kubernetes_facts(
|
||||
managed_resource['kind'],
|
||||
managed_resource['api_version'],
|
||||
'',
|
||||
module.params['namespace'],
|
||||
resource['spec']
|
||||
['selector']
|
||||
['matchLabels'],
|
||||
'')
|
||||
if module.params["kind"] == "DaemonSet":
|
||||
current_revision = resource["metadata"]["generation"]
|
||||
elif module.params["kind"] == "Deployment":
|
||||
current_revision = resource["metadata"]["annotations"][
|
||||
"deployment.kubernetes.io/revision"
|
||||
]
|
||||
|
||||
prev_managed_resource = get_previous_revision(managed_resources['resources'],
|
||||
current_revision)
|
||||
managed_resource = get_managed_resource(module.params["kind"])
|
||||
managed_resources = svc.find(
|
||||
managed_resource["kind"],
|
||||
managed_resource["api_version"],
|
||||
"",
|
||||
module.params["namespace"],
|
||||
resource["spec"]["selector"]["matchLabels"],
|
||||
"",
|
||||
)
|
||||
|
||||
if module.params['kind'] == "Deployment":
|
||||
del prev_managed_resource['spec']['template']['metadata']['labels']['pod-template-hash']
|
||||
prev_managed_resource = get_previous_revision(
|
||||
managed_resources["resources"], current_revision
|
||||
)
|
||||
if not prev_managed_resource:
|
||||
warn = "No rollout history found for resource %s/%s" % (
|
||||
module.params["kind"],
|
||||
resource["metadata"]["name"],
|
||||
)
|
||||
result = {"changed": False, "warnings": [warn]}
|
||||
return result
|
||||
|
||||
resource_patch = [{
|
||||
"op": "replace",
|
||||
"path": "/spec/template",
|
||||
"value": prev_managed_resource['spec']['template']
|
||||
}, {
|
||||
"op": "replace",
|
||||
"path": "/metadata/annotations",
|
||||
"value": {
|
||||
"deployment.kubernetes.io/revision": prev_managed_resource['metadata']['annotations']['deployment.kubernetes.io/revision']
|
||||
}
|
||||
}]
|
||||
if module.params["kind"] == "Deployment":
|
||||
del prev_managed_resource["spec"]["template"]["metadata"]["labels"][
|
||||
"pod-template-hash"
|
||||
]
|
||||
|
||||
api_target = 'deployments'
|
||||
content_type = 'application/json-patch+json'
|
||||
elif module.params['kind'] == "DaemonSet":
|
||||
resource_patch = [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/spec/template",
|
||||
"value": prev_managed_resource["spec"]["template"],
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/metadata/annotations",
|
||||
"value": {
|
||||
"deployment.kubernetes.io/revision": prev_managed_resource[
|
||||
"metadata"
|
||||
]["annotations"]["deployment.kubernetes.io/revision"]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
api_target = "deployments"
|
||||
content_type = "application/json-patch+json"
|
||||
elif module.params["kind"] == "DaemonSet":
|
||||
resource_patch = prev_managed_resource["data"]
|
||||
|
||||
api_target = 'daemonsets'
|
||||
content_type = 'application/strategic-merge-patch+json'
|
||||
api_target = "daemonsets"
|
||||
content_type = "application/strategic-merge-patch+json"
|
||||
|
||||
rollback = k8s_ansible_mixin.client.request(
|
||||
"PATCH",
|
||||
"/apis/{0}/namespaces/{1}/{2}/{3}"
|
||||
.format(module.params['api_version'],
|
||||
module.params['namespace'],
|
||||
rollback = resource
|
||||
if not module.check_mode:
|
||||
rollback = svc.client.client.request(
|
||||
"PATCH",
|
||||
"/apis/{0}/namespaces/{1}/{2}/{3}".format(
|
||||
module.params["api_version"],
|
||||
module.params["namespace"],
|
||||
api_target,
|
||||
module.params['name']),
|
||||
body=resource_patch,
|
||||
content_type=content_type)
|
||||
module.params["name"],
|
||||
),
|
||||
body=resource_patch,
|
||||
content_type=content_type,
|
||||
).to_dict()
|
||||
|
||||
result = {'changed': True}
|
||||
result['method'] = 'patch'
|
||||
result['body'] = resource_patch
|
||||
result['resources'] = rollback.to_dict()
|
||||
result = {"changed": True}
|
||||
result["method"] = "patch"
|
||||
result["body"] = resource_patch
|
||||
result["resources"] = rollback
|
||||
return result
|
||||
|
||||
|
||||
@@ -184,8 +227,8 @@ def argspec():
|
||||
args.update(NAME_ARG_SPEC)
|
||||
args.update(
|
||||
dict(
|
||||
label_selectors=dict(type='list', elements='str', default=[]),
|
||||
field_selectors=dict(type='list', elements='str', default=[]),
|
||||
label_selectors=dict(type="list", elements="str", default=[]),
|
||||
field_selectors=dict(type="list", elements="str", default=[]),
|
||||
)
|
||||
)
|
||||
return args
|
||||
@@ -193,27 +236,41 @@ def argspec():
|
||||
|
||||
def get_previous_revision(all_resources, current_revision):
|
||||
for resource in all_resources:
|
||||
if resource['kind'] == 'ReplicaSet':
|
||||
if int(resource['metadata']
|
||||
['annotations']
|
||||
['deployment.kubernetes.io/revision']) == int(current_revision) - 1:
|
||||
if resource["kind"] == "ReplicaSet":
|
||||
if (
|
||||
int(
|
||||
resource["metadata"]["annotations"][
|
||||
"deployment.kubernetes.io/revision"
|
||||
]
|
||||
)
|
||||
== int(current_revision) - 1
|
||||
):
|
||||
return resource
|
||||
elif resource['kind'] == 'ControllerRevision':
|
||||
if int(resource['metadata']
|
||||
['annotations']
|
||||
['deprecated.daemonset.template.generation']) == int(current_revision) - 1:
|
||||
elif resource["kind"] == "ControllerRevision":
|
||||
if (
|
||||
int(
|
||||
resource["metadata"]["annotations"][
|
||||
"deprecated.daemonset.template.generation"
|
||||
]
|
||||
)
|
||||
== int(current_revision) - 1
|
||||
):
|
||||
return resource
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (K8sAnsibleMixin, get_api_client)
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True
|
||||
)
|
||||
|
||||
k8s_ansible_mixin = K8sAnsibleMixin(module)
|
||||
k8s_ansible_mixin.client = get_api_client(module=module)
|
||||
execute_module(module, k8s_ansible_mixin)
|
||||
try:
|
||||
client = get_api_client(module=module)
|
||||
svc = K8sService(client, module)
|
||||
execute_module(svc)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
DOCUMENTATION = r"""
|
||||
|
||||
module: k8s_scale
|
||||
|
||||
@@ -36,6 +36,7 @@ options:
|
||||
description: List of label selectors to use to filter results.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
version_added: 2.0.0
|
||||
continue_on_error:
|
||||
description:
|
||||
@@ -48,9 +49,9 @@ requirements:
|
||||
- "python >= 3.6"
|
||||
- "kubernetes >= 12.0.0"
|
||||
- "PyYAML >= 3.11"
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
EXAMPLES = r"""
|
||||
- name: Scale deployment up, and extend timeout
|
||||
kubernetes.core.k8s_scale:
|
||||
api_version: v1
|
||||
@@ -105,9 +106,9 @@ EXAMPLES = r'''
|
||||
label_selectors:
|
||||
- app=test
|
||||
continue_on_error: true
|
||||
'''
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
RETURN = r"""
|
||||
result:
|
||||
description:
|
||||
- If a change was made, will return the patched object, otherwise returns the existing object.
|
||||
@@ -139,133 +140,188 @@ result:
|
||||
returned: when C(wait) is true
|
||||
type: int
|
||||
sample: 48
|
||||
'''
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC)
|
||||
try:
|
||||
from kubernetes.dynamic.exceptions import NotFoundError
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
||||
AnsibleModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
||||
AUTH_ARG_SPEC,
|
||||
RESOURCE_ARG_SPEC,
|
||||
NAME_ARG_SPEC,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
||||
get_api_client,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
||||
AnsibleK8SModule,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
||||
CoreException,
|
||||
ResourceTimeout,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
||||
diff_objects,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import (
|
||||
get_waiter,
|
||||
)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
||||
create_definitions,
|
||||
)
|
||||
|
||||
SCALE_ARG_SPEC = {
|
||||
'replicas': {'type': 'int', 'required': True},
|
||||
'current_replicas': {'type': 'int'},
|
||||
'resource_version': {},
|
||||
'wait': {'type': 'bool', 'default': True},
|
||||
'wait_timeout': {'type': 'int', 'default': 20},
|
||||
'wait_sleep': {'type': 'int', 'default': 5},
|
||||
"replicas": {"type": "int", "required": True},
|
||||
"current_replicas": {"type": "int"},
|
||||
"resource_version": {},
|
||||
"wait": {"type": "bool", "default": True},
|
||||
"wait_timeout": {"type": "int", "default": 20},
|
||||
"wait_sleep": {"type": "int", "default": 5},
|
||||
}
|
||||
|
||||
|
||||
def execute_module(module, k8s_ansible_mixin,):
|
||||
k8s_ansible_mixin.set_resource_definitions(module)
|
||||
|
||||
definition = k8s_ansible_mixin.resource_definitions[0]
|
||||
|
||||
name = definition['metadata']['name']
|
||||
namespace = definition['metadata'].get('namespace')
|
||||
api_version = definition['apiVersion']
|
||||
kind = definition['kind']
|
||||
current_replicas = module.params.get('current_replicas')
|
||||
replicas = module.params.get('replicas')
|
||||
resource_version = module.params.get('resource_version')
|
||||
|
||||
label_selectors = module.params.get('label_selectors')
|
||||
def execute_module(client, module):
|
||||
current_replicas = module.params.get("current_replicas")
|
||||
replicas = module.params.get("replicas")
|
||||
resource_version = module.params.get("resource_version")
|
||||
definitions = create_definitions(module.params)
|
||||
definition = definitions[0]
|
||||
name = definition["metadata"].get("name")
|
||||
namespace = definition["metadata"].get("namespace")
|
||||
api_version = definition["apiVersion"]
|
||||
kind = definition["kind"]
|
||||
label_selectors = module.params.get("label_selectors")
|
||||
if not label_selectors:
|
||||
label_selectors = []
|
||||
continue_on_error = module.params.get('continue_on_error')
|
||||
|
||||
wait = module.params.get('wait')
|
||||
wait_time = module.params.get('wait_timeout')
|
||||
wait_sleep = module.params.get('wait_sleep')
|
||||
continue_on_error = module.params.get("continue_on_error")
|
||||
wait = module.params.get("wait")
|
||||
wait_time = module.params.get("wait_timeout")
|
||||
wait_sleep = module.params.get("wait_sleep")
|
||||
existing = None
|
||||
existing_count = None
|
||||
return_attributes = dict(result=dict())
|
||||
if module._diff:
|
||||
return_attributes['diff'] = dict()
|
||||
return_attributes["diff"] = dict()
|
||||
if wait:
|
||||
return_attributes['duration'] = 0
|
||||
|
||||
resource = k8s_ansible_mixin.find_resource(kind, api_version, fail=True)
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import NotFoundError
|
||||
return_attributes["duration"] = 0
|
||||
|
||||
resource = client.resource(kind, api_version)
|
||||
multiple_scale = False
|
||||
try:
|
||||
existing = resource.get(name=name, namespace=namespace, label_selector=','.join(label_selectors))
|
||||
if existing.kind.endswith('List'):
|
||||
existing = resource.get(
|
||||
name=name, namespace=namespace, label_selector=",".join(label_selectors)
|
||||
)
|
||||
if existing.kind.endswith("List"):
|
||||
existing_items = existing.items
|
||||
multiple_scale = len(existing_items) > 1
|
||||
else:
|
||||
existing_items = [existing]
|
||||
except NotFoundError as exc:
|
||||
module.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc),
|
||||
error=exc.value.get('status'))
|
||||
except NotFoundError as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Failed to retrieve requested object: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
if multiple_scale:
|
||||
# when scaling multiple resource, the 'result' is changed to 'results' and is a list
|
||||
return_attributes = {'results': []}
|
||||
return_attributes = {"results": []}
|
||||
changed = False
|
||||
|
||||
def _continue_or_fail(error):
|
||||
if multiple_scale and continue_on_error:
|
||||
if "errors" not in return_attributes:
|
||||
return_attributes['errors'] = []
|
||||
return_attributes['errors'].append({'error': error, 'failed': True})
|
||||
return_attributes["errors"] = []
|
||||
return_attributes["errors"].append({"error": error, "failed": True})
|
||||
else:
|
||||
module.fail_json(msg=error, **return_attributes)
|
||||
|
||||
def _continue_or_exit(warn):
|
||||
if multiple_scale:
|
||||
return_attributes['results'].append({'warning': warn, 'changed': False})
|
||||
return_attributes["results"].append({"warning": warn, "changed": False})
|
||||
else:
|
||||
module.exit_json(warning=warn, **return_attributes)
|
||||
|
||||
for existing in existing_items:
|
||||
if module.params['kind'] == 'job':
|
||||
if kind.lower() == "job":
|
||||
existing_count = existing.spec.parallelism
|
||||
elif hasattr(existing.spec, 'replicas'):
|
||||
elif hasattr(existing.spec, "replicas"):
|
||||
existing_count = existing.spec.replicas
|
||||
|
||||
if existing_count is None:
|
||||
error = 'Failed to retrieve the available count for object kind={0} name={1} namespace={2}.'.format(
|
||||
existing.kind, existing.metadata.name, existing.metadata.namespace)
|
||||
error = "Failed to retrieve the available count for object kind={0} name={1} namespace={2}.".format(
|
||||
existing.kind, existing.metadata.name, existing.metadata.namespace
|
||||
)
|
||||
_continue_or_fail(error)
|
||||
continue
|
||||
|
||||
if resource_version and resource_version != existing.metadata.resourceVersion:
|
||||
warn = 'expected resource version {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.'.format(
|
||||
resource_version, existing.metadata.resourceVersion, existing.kind, existing.metadata.name, existing.metadata.namespace)
|
||||
warn = "expected resource version {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.".format(
|
||||
resource_version,
|
||||
existing.metadata.resourceVersion,
|
||||
existing.kind,
|
||||
existing.metadata.name,
|
||||
existing.metadata.namespace,
|
||||
)
|
||||
_continue_or_exit(warn)
|
||||
continue
|
||||
|
||||
if current_replicas is not None and existing_count != current_replicas:
|
||||
warn = 'current replicas {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.'.format(
|
||||
current_replicas, existing_count, existing.kind, existing.metadata.name, existing.metadata.namespace)
|
||||
warn = "current replicas {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.".format(
|
||||
current_replicas,
|
||||
existing_count,
|
||||
existing.kind,
|
||||
existing.metadata.name,
|
||||
existing.metadata.namespace,
|
||||
)
|
||||
_continue_or_exit(warn)
|
||||
continue
|
||||
|
||||
if existing_count != replicas:
|
||||
if not module.check_mode:
|
||||
if module.params['kind'] == 'job':
|
||||
existing.spec.parallelism = replicas
|
||||
result = resource.patch(existing.to_dict()).to_dict()
|
||||
if kind.lower() == "job":
|
||||
existing.spec.parallelism = replicas
|
||||
result = {"changed": True}
|
||||
if module.check_mode:
|
||||
result["result"] = existing.to_dict()
|
||||
else:
|
||||
result = scale(module, k8s_ansible_mixin, resource, existing, replicas, wait, wait_time, wait_sleep)
|
||||
changed = changed or result['changed']
|
||||
result["result"] = client.patch(
|
||||
resource, existing.to_dict()
|
||||
).to_dict()
|
||||
else:
|
||||
try:
|
||||
result = scale(
|
||||
client,
|
||||
module,
|
||||
resource,
|
||||
existing,
|
||||
replicas,
|
||||
wait,
|
||||
wait_time,
|
||||
wait_sleep,
|
||||
)
|
||||
except CoreException as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
changed = changed or result["changed"]
|
||||
else:
|
||||
name = existing.metadata.name
|
||||
namespace = existing.metadata.namespace
|
||||
existing = resource.get(name=name, namespace=namespace)
|
||||
result = {'changed': False, 'result': existing.to_dict()}
|
||||
existing = client.get(resource, name=name, namespace=namespace)
|
||||
result = {"changed": False, "result": existing.to_dict()}
|
||||
if module._diff:
|
||||
result['diff'] = {}
|
||||
result["diff"] = {}
|
||||
if wait:
|
||||
result['duration'] = 0
|
||||
result["duration"] = 0
|
||||
# append result to the return attribute
|
||||
if multiple_scale:
|
||||
return_attributes['results'].append(result)
|
||||
return_attributes["results"].append(result)
|
||||
else:
|
||||
module.exit_json(**result)
|
||||
|
||||
@@ -277,56 +333,91 @@ def argspec():
|
||||
args.update(RESOURCE_ARG_SPEC)
|
||||
args.update(NAME_ARG_SPEC)
|
||||
args.update(AUTH_ARG_SPEC)
|
||||
args.update({'label_selectors': {'type': 'list', 'elements': 'str', 'default': []}})
|
||||
args.update(({'continue_on_error': {'type': 'bool', 'default': False}}))
|
||||
args.update({"label_selectors": {"type": "list", "elements": "str", "default": []}})
|
||||
args.update(({"continue_on_error": {"type": "bool", "default": False}}))
|
||||
return args
|
||||
|
||||
|
||||
def scale(module, k8s_ansible_mixin, resource, existing_object, replicas, wait, wait_time, wait_sleep):
|
||||
def scale(
|
||||
client,
|
||||
module,
|
||||
resource,
|
||||
existing_object,
|
||||
replicas,
|
||||
wait,
|
||||
wait_time,
|
||||
wait_sleep,
|
||||
):
|
||||
name = existing_object.metadata.name
|
||||
namespace = existing_object.metadata.namespace
|
||||
kind = existing_object.kind
|
||||
|
||||
if not hasattr(resource, 'scale'):
|
||||
module.fail_json(
|
||||
msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
|
||||
if not hasattr(resource, "scale"):
|
||||
raise CoreException(
|
||||
"Cannot perform scale on resource of kind {0}".format(resource.kind)
|
||||
)
|
||||
|
||||
scale_obj = {'kind': kind, 'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
|
||||
scale_obj = {
|
||||
"kind": kind,
|
||||
"metadata": {"name": name, "namespace": namespace},
|
||||
"spec": {"replicas": replicas},
|
||||
}
|
||||
|
||||
existing = resource.get(name=name, namespace=namespace)
|
||||
existing = client.get(resource, name=name, namespace=namespace)
|
||||
|
||||
try:
|
||||
resource.scale.patch(body=scale_obj)
|
||||
except Exception as exc:
|
||||
module.fail_json(msg="Scale request failed: {0}".format(exc))
|
||||
|
||||
k8s_obj = resource.get(name=name, namespace=namespace).to_dict()
|
||||
match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj)
|
||||
result = dict()
|
||||
result['result'] = k8s_obj
|
||||
result['changed'] = not match
|
||||
if module._diff:
|
||||
result['diff'] = diffs
|
||||
if module.check_mode:
|
||||
k8s_obj = copy.deepcopy(existing.to_dict())
|
||||
k8s_obj["spec"]["replicas"] = replicas
|
||||
if wait:
|
||||
result["duration"] = 0
|
||||
result["result"] = k8s_obj
|
||||
else:
|
||||
try:
|
||||
resource.scale.patch(body=scale_obj)
|
||||
except Exception as e:
|
||||
reason = e.body if hasattr(e, "body") else e
|
||||
msg = "Scale request failed: {0}".format(reason)
|
||||
raise CoreException(msg) from e
|
||||
|
||||
k8s_obj = client.get(resource, name=name, namespace=namespace).to_dict()
|
||||
result["result"] = k8s_obj
|
||||
if wait:
|
||||
waiter = get_waiter(client, resource)
|
||||
success, result["result"], result["duration"] = waiter.wait(
|
||||
timeout=wait_time,
|
||||
sleep=wait_sleep,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
)
|
||||
if not success:
|
||||
raise ResourceTimeout("Resource scaling timed out", **result)
|
||||
|
||||
match, diffs = diff_objects(existing.to_dict(), result["result"])
|
||||
result["changed"] = not match
|
||||
if module._diff:
|
||||
result["diff"] = diffs
|
||||
|
||||
if wait:
|
||||
success, result['result'], result['duration'] = k8s_ansible_mixin.wait(resource, scale_obj, wait_sleep, wait_time)
|
||||
if not success:
|
||||
module.fail_json(msg="Resource scaling timed out", **result)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
mutually_exclusive = [
|
||||
('resource_definition', 'src'),
|
||||
("resource_definition", "src"),
|
||||
]
|
||||
module = AnsibleModule(argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
||||
K8sAnsibleMixin, get_api_client)
|
||||
k8s_ansible_mixin = K8sAnsibleMixin(module)
|
||||
k8s_ansible_mixin.client = get_api_client(module=module)
|
||||
execute_module(module, k8s_ansible_mixin)
|
||||
module = AnsibleK8SModule(
|
||||
module_class=AnsibleModule,
|
||||
argument_spec=argspec(),
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
try:
|
||||
client = get_api_client(module=module)
|
||||
execute_module(client, module)
|
||||
except CoreException as e:
|
||||
module.fail_from_exception(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user