Compare commits

...

69 Commits

Author SHA1 Message Date
Mike Graves
69304d1c3b Release version 2.3.2 (#475)
Release version 2.3.2

SUMMARY

Release version 2.3.2

ISSUE TYPE

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Alina Buzachis <None>
2022-06-10 12:29:55 +00:00
Mike Graves
fdddb6f78f helm_repository: Silence false no_log warning (#423) (#473)
[backport/2.3] helm_repository: Silence false no_log warning (#423)

Depends-on: ansible/ansible-zuul-jobs#1563
helm_repository: Silence false no_log warning
Depends-On: #424
SUMMARY
Apply no_log=True to pass_credentials to silence
false positive warning.
Fixes: #412
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
changelogs/fragments/412_pass_creds.yml
plugins/modules/helm_repository.py
Reviewed-by: Mike Graves mgraves@redhat.com
(cherry picked from commit d311ac7)
SUMMARY


ISSUE TYPE


Bugfix Pull Request
Docs Pull Request
Feature Pull Request
New Module Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION
2022-06-08 17:20:12 +00:00
Mike Graves
e8cf1ef517 Add missing PSF license (#463) (#465)
[backport/2.3] Add missing PSF license (#463)

Add missing PSF license
SUMMARY
Add missing PSF license
Fixes #462
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
ADDITIONAL INFORMATION
Reviewed-by: Abhijeet Kasurde 
Reviewed-by: Felix Fontein felix@fontein.de
(cherry picked from commit 3729b8b)
2022-05-16 23:34:34 +00:00
Mike Graves
6dbc0d5b6d Remove distutils from connection plugin (#456) (#464)
[backport/2.3] Remove distutils from connection plugin (#456)

Remove distutils from connection plugin
SUMMARY
distutils.spawn.find_executable is deprecated and shutils.which is a
suitable replacement.
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
ADDITIONAL INFORMATION
Reviewed-by: Gonéri Le Bouder goneri@lebouder.net
Reviewed-by: Joseph Torcasso 
(cherry picked from commit 531a9fe)
2022-05-16 16:19:36 +00:00
Mike Graves
0a72c87d2c Release 2.3.1 (#448)
Release 2.3.1

SUMMARY


ISSUE TYPE


Bugfix Pull Request
Docs Pull Request
Feature Pull Request
New Module Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Joseph Torcasso <None>
Reviewed-by: Jill R <None>
Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net>
2022-05-02 20:13:12 +00:00
Mike Graves
c149394556 k8s_cp - fix issue when using local_path (#422) (#442)
[backport/2.3] k8s_cp - fix issue when using local_path (#422)

Depends-On: #446
k8s_cp - fix issue when using local_path
SUMMARY
When copying from local path to pod, the file is found on the controller node instead of the managed node.
This PR aims to resolve this issue.
Fixes #421
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
k8s_cp
Reviewed-by: Abhijeet Kasurde 
Reviewed-by: Mike Graves mgraves@redhat.com
(cherry picked from commit 1d05cf5)
2022-05-02 17:41:21 +00:00
Mike Graves
e2dec91460 k8s - fix issue when try to delete resources using label_selectors option (#434) (#444)
[backport/2.3] k8s - fix issue when try to delete resources using label_selectors op…

Depends-On: #446
k8s - fix issue when try to delete resources using label_selectors
SUMMARY
The kubernetes dynamic client has label_selector parameter for the delete method, however based on the documentation of REST API we cannot delete resources using labelSelector option, this fix update the way the resources are deleted. The list of resources are deleted one after another like in the kubectl go client.
Fixes #428
ISSUE TYPE
Bugfix Pull Request
Reviewed-by: Abhijeet Kasurde 
(cherry picked from commit f2f4b66)
2022-05-02 15:43:19 +00:00
Mike Graves
edf104d687 [backport/2.3] continue waiting when an exception is raised (#408) (#441)
[backport/2.3] continue waiting when an exception is raised (#408)

Depends-On: #446
Continue waiting when an exception is raised
SUMMARY
When an exception is raised and the wait_timeout is not reached, we should continue waiting as this may occurs due to temporary issue on cluster
Fixes #407
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
ADDITIONAL INFORMATION
Reviewed-by: Mike Graves mgraves@redhat.com
Reviewed-by: Abhijeet Kasurde 
(cherry picked from commit f418353)
2022-05-02 13:37:13 +00:00
Mike Graves
7b09c01d98 Change line in doc fragment yaml (#439) (#445)
[backport/2.3] Change line in doc fragment yaml (#439)

Depends-On: #446
Change line in doc fragment yaml
SUMMARY
For whatever reason, the one line in this doc fragment leads to sanity
failures in the redhat.openshift collection, which uses this fragment.
The downstream build process for that collection creates yaml that
appears to be valid, but that fails to lint. I'm not sure exactly which
tool the problem is in, but the easiest solution is to just remove the
single quotes here.
ISSUE TYPE
Docs Pull Request
COMPONENT NAME
ADDITIONAL INFORMATION
Reviewed-by: Abhijeet Kasurde 
(cherry picked from commit b5cfc85)
2022-05-02 13:03:32 +00:00
Mike Graves
7409eaf993 Remove omit from template resource (#432) (#443)
[backport/2.3] Remove omit from template resource (#432)

Depends-On: #446
Remove omit value from template args
SUMMARY
While defining resource using template parameter, the code does not remove the omit value if any.
This fix adds a post process to remove any omit value from the resource definition.
fixes #431
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
k8s*
Reviewed-by: Mike Graves mgraves@redhat.com
Reviewed-by: Abhijeet Kasurde 
(cherry picked from commit 882e672)
2022-05-02 12:59:08 +00:00
Mike Graves
321b6dcdd8 fix issue when using k8s_drain with disable_eviction set to yes (#418) (#440)
[backport/2.3] fix issue when using k8s_drain with disable_eviction set to yes (#418)

Depends-On: #446
fix issue when using k8s_drain with disable_eviction set to yes
SUMMARY
fixes #416
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
k8s_drain
ADDITIONAL INFORMATION
Reviewed-by: Abhijeet Kasurde 
(cherry picked from commit 074f0a6)
2022-04-29 20:45:47 +00:00
Mike Graves
68d45af767 Upgrade black version (#424) (#446)
[backport/2.3] Upgrade black version (#424)

Depends-On: ansible/ansible-zuul-jobs#1515
Upgrade black version
SUMMARY
Move off of beta version of black and pin to current calendar year
version.
The only manual changes here are to tox.ini. Everything else is from running the new version of black.
ISSUE TYPE
COMPONENT NAME
ADDITIONAL INFORMATION
Reviewed-by: Abhijeet Kasurde 
(cherry picked from commit 7c71436)
2022-04-29 17:47:08 +00:00
Mike Graves
346808ec4b Update documentation (#409)
Update documentation

SUMMARY


ISSUE TYPE


Docs Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: None <None>
Reviewed-by: Abhijeet Kasurde <None>
2022-03-16 14:54:27 +00:00
Mike Graves
767aa10b1d Release 2.3.0 (#404) 2022-03-16 08:10:08 -04:00
Mike Graves
d68dec3b90 Fix waiting on StatefulSet scale down (#391)
Fix waiting on StatefulSet scale down

SUMMARY

When scaling a StatefulSet down to 0 replicas the wait will fail
because some properties of the status (readyReplicas, updatedReplicas)
will not exist. These are probably defined as omitempty in the API and
since the value is zero are not present in the response.

Fixes #203
ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

k8s_scale
ADDITIONAL INFORMATION

Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net>
2022-03-11 17:32:12 +00:00
Mike Graves
30e84faa24 Fix validation errors in plugin documentation (#399)
Fix validation errors in plugin documentation

Depends-On: ansible/ansible-zuul-jobs#1385
SUMMARY

This fixes validation errors in plugin documentation now that ansible
test for 2.13 is running validate-modules on all plugins. The kubectl
connection plugin validation is ignored because there seems to be a
requirement for the author field to have a github username, which we do
not have.

ISSUE TYPE


Docs Pull Request

COMPONENT NAME

plugins/connection/kubectl
plugins/inventory/k8s
plugins/lookup/k8s
plugins/lookup/kustomize
ADDITIONAL INFORMATION

Reviewed-by: None <None>
2022-03-11 14:37:01 +00:00
abikouo
fd61f8b15d Move integration test suite from molecule to ansible-test (#392)
Move integration test suite from molecule to ansible-test

SUMMARY

molecule has been replaced with ansible-test
some test cases have been updated

k8s_apply : remove duplicated tasks increasing the running time of the test
helm: use different namespaces for different test cases in order to wait for the namespace deletion before moving to the next test.
all: remove wait: yes at the end of each test when deleting namespace, the role used to create namespace will ensure that it is deleted before if existing.


ISSUE TYPE


Feature Pull Request

COMPONENT NAME

integration testing

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net>
Reviewed-by: None <None>
2022-03-11 08:03:00 +00:00
Major Hayden
db78d3a505 Docs: Small fix for k8s example (#397)
Docs: Small fix for k8s example

SUMMARY
Update the k8s example to use kubernetes.core.k8s instead of the bare k8s and fix the indentation.
ISSUE TYPE

Docs Pull Request

COMPONENT NAME
kubernetes.core
ADDITIONAL INFORMATION
Just a small documentation fix. 😉

Reviewed-by: Mike Graves <mgraves@redhat.com>
2022-03-08 16:06:10 +00:00
Christian von Stebut
73499d9a09 helm_template: add optional show_only and release_namespace arguments (#388)
helm_template: add optional show_only and release_namespace arguments

SUMMARY

This PR adds the "show_only" and "release_namespace" as optional arguments to the helm_template module.
It does some work towards #313.

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

changelogs/fragments/313-helm-template-add-support-for-show-only-and-release-namespace.yml
plugins/modules/helm_template.py
tests/unit/modules/test_helm_template.py
ADDITIONAL INFORMATION


The PR does include unit tests instead of integration test.
Reasoning:
The existing integration tests already include a task based on helm_template. So we know that the module does a proper job of using the command line generated inside the module to call helm.
As I trust helm itself to "do its job" correctly, all that should be necessary is to test the correct generation of the command line itself. The included unit tests hopefully do a proper job.
With regards of the pretty long testing times for the module, I really prefer unit tests, if at all possible.
Please let me know if this fits.

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
2022-03-01 18:57:17 +00:00
abikouo
7031829897 helm - add support for repo location when running helm diff (#389)
helm - add support for repo location when running helm diff

SUMMARY

closes #174

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

helm

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: None <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
2022-02-22 15:52:08 +00:00
abikouo
44c8cff78b k8s_cp turbo mode compliance (#254)
k8s_cp turbo mode compliance

SUMMARY

closes #237

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
2022-02-22 12:01:19 +00:00
Abhijeet Kasurde
aae5960dce helm_info: add release_state argument (#379)
helm_info: add release_state argument

SUMMARY
Specify release state in helm list command as per helm cmdline flags.
Fixes: #377
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/377-helm-info-state.yml
molecule/default/roles/helm/tasks/tests_chart.yml
plugins/modules/helm_info.py

Reviewed-by: None <None>
Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
2022-02-17 06:42:43 +00:00
abikouo
691f0cb235 add support for check_mode for modules k8s_scale and k8s_rollback (#255)
k8s_scale, k8s_rollback - add support for check_mode 

SUMMARY

closes #243 and #244

ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

k8s_scale
k8s_rollback
ADDITIONAL INFORMATION

Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
2022-02-16 19:05:28 +00:00
Abhijeet Kasurde
951be74dc0 Import ApiException from single source (#384)
Import ApiException from single source

SUMMARY
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/exception.yml
plugins/modules/k8s_drain.py
plugins/modules/k8s_taint.py

Reviewed-by: None <None>
2022-02-15 14:47:33 +00:00
Abhijeet Kasurde
1f79a03edf Adding sleep 0 as workaround when copying files with kubectl exec (#378)
Adding sleep 0 as workaround when copying files with kubectl exec

SUMMARY
For all the commands executed remotely, ** && sleep 0** will be
appended as a workaround for all the commands to terminate properly:
16def8050a/lib/ansible/plugins/action/__init__.py (L1243)
Workaround will be applied in case of kubectl exec too:

  
    
      kubernetes.core/plugins/connection/kubectl.py
    
    
         Line 300
      in
      b19ff9d
    
  
  
    

        
          
           super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) 
        
    
  


That is not the case in the case of the file copy executed by using kubectl exec, therefore it is possible for the kubectl exec to
terminate before dd finishes properly causing the file to be truncated.
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/321-kubectl_sleep.yml
plugins/connection/kubectl.py
2022-02-14 06:21:25 +00:00
Abhijeet Kasurde
bf3fe91a5d k8s_exec: Select first container from the pod (#363)
k8s_exec: Select first container from the pod

SUMMARY
kubectl command select first container from the pod in order
to execute commands on. We replicate the same behavior in k8s_exec
module.
Fixes: #358
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/358-k8s_exec.yml
plugins/modules/k8s_exec.py

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2022-02-10 04:19:04 +00:00
Abhijeet Kasurde
791175daef Use resource prefix when apiVersion is v1 (#371)
Use resource prefix when apiVersion is v1

SUMMARY
When getting a resource from the core api group, the prefix was not
passed, leading the lookup to happen in all api groups. This broad
search is not really necessary and leads to problems in some corner
cases, for example, when an api is deleted after the api group list is
cached.
This fix uses the 'api' prefix when the apiVersion is 'v1', as this is
almost certainly what the user wants. As a fallback, to retain backwards
compatibility, the old behavior is used if the first lookup failed to
find a resource. Given that the module defaults to 'v1' for the
apiVersion, there are likely many cases where a resource, such as
StatefulSet, is used while failing to provide an apiVersion. While
technically incorrect, this has worked in most cases, so we probably
shouldn't break this behavior.
Fixes #351
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/364-use-resource-prefix.yaml
plugins/module_utils/common.py
2022-02-10 02:41:52 +00:00
Abhijeet Kasurde
e62a271faf helm_repository: Added support for common options (#370)
helm_repository: Added support for common options

SUMMARY
Added support for host, api_key, ca_cert,
and validate_certs in helm_repository module.
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/helm_repository.yml
plugins/modules/helm_repository.py
2022-02-09 18:27:20 +00:00
Abhijeet Kasurde
583de3217c Fix module_defaults by removing routing hack (#372)
Fix module_defaults by removing routing hack

SUMMARY
Fixes #202
Fixes ansible/ansible#76687
As mentioned here, I'm not sure what the redirection was originally solving, but this would be the ideal solution for module_defaults.
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/347-routing.yml
meta/runtime.yml
2022-02-09 16:31:51 +00:00
Abhijeet Kasurde
82565dad78 helm_template: change order of values_release and values_files (#373)
helm_template: change order of values_release and values_files

SUMMARY
This fix aligns precedence of release_values and values_files in kubernetes.core.helm_template with the one in kubernetes.core.helm.
The values in release_values are now processed last, thus with the highest precedence.
This allows overwriting of values in values_files with the values presented in release_values.
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/348-helm_template-fix-precedence-of-release-values-over-values-files.yaml
plugins/modules/helm_template.py
tests/unit/modules/test_helm_template.py
2022-02-09 15:47:17 +00:00
Abhijeet Kasurde
dde6eb3c06 k8scopy: rely on existing kubectl binary (#369)
k8scopy: rely on existing kubectl binary

SUMMARY
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
molecule/default/roles/k8scopy/defaults/main.yml
molecule/default/roles/k8scopy/tasks/main.yml
molecule/default/roles/k8scopy/tasks/test_copy_directory.yml
molecule/default/roles/k8scopy/tasks/test_copy_file.yml
molecule/default/roles/k8scopy/tasks/test_copy_large_file.yml
molecule/default/roles/k8scopy/tasks/test_multi_container_pod.yml
molecule/default/tasks/lookup_kustomize.yml
2022-02-07 16:58:22 +00:00
Gonéri Le Bouder
a122bad685 remove the .zuul.d directory (#367)
remove the .zuul.d directory

We now avoid as much as possible the local Zuul configuration because
it's easy to break them inadvertently.
2022-02-04 18:06:37 +00:00
Mike Graves
b54e9ef4ef Remove serial deletion of pods in template tests (#349)
Remove serial deletion of pods in template tests

SUMMARY

The template test suite deletes twelve pods in serial during cleanup
which is very slow and leads to frequent timeouts. There's no need to do
this since we delete the namespace the pods are in right after.

ISSUE TYPE

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: None <None>
Reviewed-by: None <None>
2022-01-25 22:23:09 +00:00
abikouo
acb015c788 add patchback bot (#346)
Add patchback bot

Add the configs so that we can use the patchback bot for semi-automated backports.

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2022-01-24 19:42:00 +00:00
Abhijeet Kasurde
ed33d0b56e Prepare for distutils.version being removed in Python 3.12 (#314)
Prepare for distutils.version being removed in Python 3.12

SUMMARY
distutils has been deprecafed and will be removed from
Python's stdlib in Python 3.12 (see python.org/dev/peps/pep-0632).
This PR replaces the use of distutils.version.LooseVersion and distutils.version.StrictVersion
with LooseVersion from the vendored copy of distutils.version
included with ansible-core 2.12 (ansible/ansible#74644) if available,
and falls back to distutils.version for ansible-core 2.11 and before.
Since ansible-core 2.11 and earlier do not support Python 3.12 (since
they use LooseVersion itself in various places), this incomplete fix
should be OK for now. Also, the way this PR works (by adding a new
module_utils version that abstracts away where LooseVersion comes from),
it is easy to also fix this for ansible-core 2.11 and earlier later on.
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
changelogs/fragments/disutils.version.yml
molecule/default/roles/helm/library/helm_test_version.py
plugins/module_utils/common.py
plugins/module_utils/version.py
plugins/modules/helm.py

Reviewed-by: Felix Fontein <felix@fontein.de>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2022-01-18 13:03:14 +00:00
abikouo
10cffc5032 Enable turbo mode for k8s lookup plugin (#335)
Enable turbo mode for k8s lookup plugin

SUMMARY

Enable Turbo mode for k8s lookup plugin
This resolves partially #291

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

k8s lookup

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: None <None>
Reviewed-by: None <None>
2022-01-17 12:04:27 +00:00
Max Gautier
9a0b3fe30c Documentation update for kubernetes.core.helm (#317)
Documentation update for kubernetes.core.helm

Clarify usage of the module for doing helm repo update only.
I used collection_prep_add_docs as explained in CONTRIBUTING.md, not sure if
that's correct ?
Fixes #316
@Akasurde

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2022-01-13 14:47:17 +00:00
Jorn Eilander
c3ecb64b72 Add delete_emptydir_data to drain delete_options (#322)
Add delete_emptydir_data to drain delete_options

SUMMARY
Adds delete_emptydir_data option to k8s_drain.delete_options to evict pods with an emptyDir volume attached.
ISSUE TYPE

Feature Pull Request

COMPONENT NAME
k8s_drain
ADDITIONAL INFORMATION
Be gentle, this is my first pull request 😨 
Basically adds the kubectl drain <node> --delete-emptydir-data feature, including tests.

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Jorn Eilander <None>
Reviewed-by: None <None>
Reviewed-by: None <None>
2022-01-13 13:40:06 +00:00
abikouo
50a1bd9db0 add support for community.okd.openshift_adm_groups_sync (#274)
add support for community.okd.openshift_adm_groups_sync

SUMMARY
new module community.okd.openshift_adm_group_sync requires action group
ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2022-01-13 10:34:06 +00:00
Abhijeet Kasurde
04e14c1f95 DNM: CI fix (#323)
CI fix

SUMMARY
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
molecule/default/tasks/taint.yml

Reviewed-by: None <None>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: None <None>
2022-01-12 08:07:03 +00:00
abikouo
b19ff9d70a k8s - add support for Server Side apply (#260)
k8s - add support for Server Side apply

SUMMARY

Server side apply is now support for k8s module with this Pull request.
The feature is not yet released on kubernetes-client, once this is done, we can merge this pull request.
closes #87

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

k8s
ADDITIONAL INFORMATION

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
Reviewed-by: None <None>
2021-12-16 16:48:00 +00:00
Alessandro Rossi
526f0454ab Fix for common non-ASCII characters in CRDs (#308)
Fix for common non-ASCII characters in CRDs

This should keep the module safe from digesting non-ASCII chars like here (https://github.com/projectcalico/api/pull/46/files)
SUMMARY
Add support for non-ASCII chars in manifests.

ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
core.k8s module failing if resources contain non ascii chars

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Alessandro Rossi <None>
Reviewed-by: None <None>
2021-12-14 22:08:35 +00:00
Alina Buzachis
e77c8f1449 K8s_taint new module (#264)
K8s_taint new module

SUMMARY

k8s_taint - new module to apply/remove taints to/from nodes.

ISSUE TYPE


New Module Pull Request

COMPONENT NAME

k8s_taint

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
Reviewed-by: None <None>
2021-12-09 18:16:55 +00:00
Mandar Kulkarni
79699ba429 Add integration test to check handling of module_defaults (#296)
Add integration test to check handling of module_defaults 

SUMMARY

Add integration test to make sure that module_defaults are handled correctly in tasks.
Related to #126.

ISSUE TYPE


Bugfix Pull Request

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-12-07 21:33:26 +00:00
Mike Graves
fa65698362 Remove binary file from molecule test suite (#298)
Remove binary file from molecule test suite

SUMMARY

The binary file used to test k8s_cp is causing larger problems
downstream. There's no reason why the binary file needs to function as
all we care about is that the content of the file has not changed during
the copy process. This can be accomplished by comparing file hashes.

Fixes #297 #293
ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: None <None>
2021-12-01 20:00:04 +00:00
Fabrice
4ae1856b5c Return diff in helm check mode (#290)
Return diff in helm check mode

When the helm module is executed in check mode with the helm diff plugin
installed, it now returns the diff.
SUMMARY
When the helm module is executed in check mode with the helm diff plugin
installed, it now returns the diff.
COMPONENT NAME
helm
ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Fabrice <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-11-30 17:46:29 +00:00
Mike Graves
ef46c352d0 Fix k8s_drain failing when pod has local storage (#295)
Fix k8s_drain failing when pod has local storage

SUMMARY

The module fails to define the pod_names variable before using it for
pods with local storage.

Fixes #292
ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

k8s_drain
ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: None <None>
Reviewed-by: None <None>
2021-11-30 09:07:08 +00:00
Mike Graves
a62c42782f Show diff for black check (#289)
Show diff for black check

SUMMARY

Show diff for black check
This will make it easier to see from the CI logs what the actual problem
is.

ISSUE TYPE

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: None <None>
Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
2021-11-30 06:11:10 +00:00
Nataliya Romanovich
ba5cb30305 helm: add pass-credentials key (#282)
helm: add pass-credentials key

SUMMARY
In helm version v3.6.1 when downloading charts from password protected repositories that served from a different domain than the repository, need to use --pass-credentials key.
Add possibility to use the pass-credentials key in helm_repository.py
ISSUE TYPE

Feature Pull Request

COMPONENT NAME
helm_repository

Reviewed-by: None <None>
Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-11-19 18:20:16 +00:00
abikouo
39b6c43ab7 add support for user impersonation for k8s modules (#250)
add support for user impersonation for k8s modules

SUMMARY

k8s module should not allow user to perform operation using impersonation as describe here
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
This pull request closes #40

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: None <None>
2021-11-17 13:25:06 +00:00
Gonéri Le Bouder
b0f1501cd4 turn network-ee-sanity-tests non-voting (#284)
turn network-ee-sanity-tests non-voting

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-11-16 20:52:58 +00:00
Mike Graves
1116056eeb Fix sanity tests (#283)
Fix sanity tests

SUMMARY


ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: None <None>
Reviewed-by: None <None>
2021-11-16 16:52:41 +00:00
Mike Graves
60933457e8 Add kubernetes support statement (#279)
Add kubernetes support statement

SUMMARY

Add kubernetes support statement

ISSUE TYPE


Docs Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Timothy Appnel <None>
Reviewed-by: None <None>
2021-11-11 18:25:47 +00:00
itaru2622
9e2d78404f add no_proxy support to k8s* (#272)
add no_proxy support to k8s*

SUMMARY

close #271

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

plugins/module_utils/args_common.py
plugins/modules/k8s*
ADDITIONAL INFORMATION


It requires latest kubernetes library(>=19.15.0) to use this feature.


pip install kubernetes>=19.15.0
then, use following snippet yaml:

  - k8s:
      state: present
      src: "deployment.yaml"
      proxy:      "http://proxy.yourdomain.com:8080/"
      no_proxy:   "localhost,.yourdomain.com,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192,168.0.0/16"

or use environment variable K8S_AUTH_NO_PROXY as well as K8S_AUTH_PROXY.

Reviewed-by: None <None>
Reviewed-by: None <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-11-10 18:25:30 +00:00
Mike Graves
bf26f5a3be Don't wait on *List resources for info module (#253)
Don't wait on *List resources for info module

SUMMARY

We can't use the same wait logic on *List resources because they lack
the same metadata that other resources have. We should ensure that we
are waiting on the items in the list, but not the list itself. Waiting
on the list itself results in unexpected behavior.
This fixes the waiting logic when waiting on a list to wait until the
list being queried contains one or more items, or the wait timeout has
been reached. Each item in the list can then be waited on with the usual
wait logic.

ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: None <None>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
2021-11-08 18:12:25 +00:00
Abhijeet Kasurde
91b80b1d1d Enable black formatting test (#259)
Enable black formatting test

SUMMARY
Signed-off-by: Abhijeet Kasurde akasurde@redhat.com
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
plugins/action/k8s_info.py
plugins/connection/kubectl.py
plugins/doc_fragments/helm_common_options.py
plugins/doc_fragments/k8s_auth_options.py
plugins/doc_fragments/k8s_delete_options.py
plugins/doc_fragments/k8s_name_options.py
plugins/doc_fragments/k8s_resource_options.py
plugins/doc_fragments/k8s_scale_options.py
plugins/doc_fragments/k8s_state_options.py
plugins/doc_fragments/k8s_wait_options.py
plugins/filter/k8s.py
plugins/inventory/k8s.py
plugins/lookup/k8s.py
plugins/lookup/kustomize.py
plugins/module_utils/ansiblemodule.py
plugins/module_utils/apply.py
plugins/module_utils/args_common.py
plugins/module_utils/client/discovery.py
plugins/module_utils/client/resource.py
plugins/module_utils/common.py
plugins/module_utils/exceptions.py
plugins/module_utils/hashes.py
plugins/module_utils/helm.py
plugins/module_utils/k8sdynamicclient.py
plugins/module_utils/selector.py
plugins/modules/helm.py
plugins/modules/helm_info.py
plugins/modules/helm_plugin.py
plugins/modules/helm_plugin_info.py
plugins/modules/helm_repository.py
plugins/modules/helm_template.py
plugins/modules/k8s.py
plugins/modules/k8s_cluster_info.py
plugins/modules/k8s_cp.py
plugins/modules/k8s_drain.py
plugins/modules/k8s_exec.py
plugins/modules/k8s_info.py
plugins/modules/k8s_json_patch.py
plugins/modules/k8s_log.py
plugins/modules/k8s_rollback.py
plugins/modules/k8s_scale.py
plugins/modules/k8s_service.py
tests/integration/targets/kubernetes/library/test_tempfile.py
tests/unit/module_utils/test_apply.py
tests/unit/module_utils/test_common.py
tests/unit/module_utils/test_discoverer.py
tests/unit/module_utils/test_hashes.py
tests/unit/module_utils/test_marshal.py
tests/unit/module_utils/test_selector.py
tox.ini

Reviewed-by: None <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-10-18 15:32:05 +00:00
Mike Graves
4010987d1f Add support for dry run (#245)
Add support for dry run

SUMMARY

Kubernetes server-side dry run will be used when the kubernetes client
version is >=18.20.0. For older versions of the client, the existing
client side speculative change implementation will be used.
The effect of this change should be mostly transparent to the end user
and is reflected in the fact the tests have not changed but should still
pass. With this change, there are a few edge cases that will be
improved. One example of these edge cases is to use check mode on an
existing Service resource. With dry run this will correctly report no
changes, while the older client side implementation will erroneously
report changes to the port spec.

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
Reviewed-by: None <None>
2021-10-15 14:20:43 +00:00
Mike Graves
281ff563ed Use yaml.safe_load in unit tests (#265)
Use yaml.safe_load in unit tests

SUMMARY

The function signature in pyyaml 6 for yaml.load changed. Using
safe_load fixes this.

ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Jill R <None>
Reviewed-by: None <None>
Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net>
2021-10-14 19:48:21 +00:00
Mike Graves
ff43353de6 Remove molecule dependencies (#261)
Remove molecule dependencies

SUMMARY

Depends-on: ansible-collections/cloud.common#92
Molecule is overwriting the cloud.common dependency installed by zuul,
which is causing issues with the CI job for turbo mode. We still need to
find a way to test against the latest released version of cloud.common.

ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net>
Reviewed-by: None <None>
2021-10-13 16:16:04 +00:00
Paul Belanger
d6c06a2078 Add openshift-clients to bindep.txt (#249)
Add openshift-clients to bindep.txt

For RHEL8 builds, we use openshift-clients RPM to install both kubectl /
oc clients.
Signed-off-by: Paul Belanger pabelanger@redhat.com

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-10-05 15:53:19 +00:00
abikouo
8436ad1341 Fix sanity test - devel drops support for python 2.6 (#251)
Fix sanity test - devel drops support for python 2.6

SUMMARY


ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: None <None>
2021-10-04 10:12:05 +00:00
abikouo
c65512357d k8s - allow resource definition using generateName (#238)
k8s - allow resource definition using generateName

SUMMARY

#35

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

k8s
ADDITIONAL INFORMATION



- name: create pod using generateName
  k8s:
    namespace: test
    generate_name: pod-
    definition:
       kind: Pod
       spec:
          containers:
          - name: py
            image: python:3.7-alpine

- name: create pod using generateName
  k8s:
    namespace: test
    definition:
       kind: Pod
       metadata:
          generateName: pod-
       spec:
          containers:
          - name: py
            image: python:3.7-alpine

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
Reviewed-by: None <None>
2021-09-30 14:58:50 +00:00
abikouo
8e46f92703 Helm uninstall now support wait parameter (#235)
Helm uninstall now support wait parameter

SUMMARY

closes #33

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION



- helm:
    chart_name: test
    state: absent
    wait: yes

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
Reviewed-by: None <None>
2021-09-29 16:21:37 +00:00
abikouo
ab0e38753b add plugin_version parameter for helm_plugin module (#226)
add plugin_version parameter for helm_plugin module

SUMMARY

closes #157

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
2021-09-29 11:06:33 +00:00
abikouo
6061586289 helm - allow setting timeout independent of wait parameter (#231)
helm - allow setting timeout independent of wait parameter

SUMMARY

closes #67

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: None <None>
2021-09-28 16:29:26 +00:00
Andrew Klychkov
45ba8b1a0d Copy ignore-2.12.txt to ignore-2.13.txt (#247)
Copy ignore-2.12.txt to ignore-2.13.txt

SUMMARY
Relates to ansible-collections/overview#45 (comment)

Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
2021-09-28 16:03:34 +00:00
Gonéri Le Bouder
d01e4a6e4d molecule: retry the helm download (#232)
molecule: retry the helm download

Retry the helm download 10 times before giving up.

Reviewed-by: None <None>
2021-09-22 22:37:08 +00:00
Gonéri Le Bouder
938f7e12e8 common/_wait_for: ensure label_selectors is optional (#239)
common/_wait_for: ensure label_selectors is optional

Depends-On: ansible/ansible-zuul-jobs#1125
The label_selectors is a new parameter for _wait_for that was
introduced in #158.
The value is new and it can be set to None to make it optional. It should
not be mandatory a non optional parameter.

Reviewed-by: None <None>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
2021-09-22 22:37:06 +00:00
Mike Graves
24ac45741d Increase timeout on scale test (#242)
Increase timeout on scale test

SUMMARY

This test frequently fails with the default 20s timeout. Bumping up to
60s.

Fixes #241
Depends-on: ansible/ansible-zuul-jobs#1131
ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net>
Reviewed-by: None <None>
2021-09-22 22:33:35 +00:00
288 changed files with 10717 additions and 4599 deletions

4
.github/patchback.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
---
backport_branch_prefix: patchback/backports/
backport_label_prefix: backport-
target_branch_prefix: stable-

6
.gitignore vendored
View File

@@ -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

View File

@@ -5,6 +5,75 @@ Kubernetes Collection Release Notes
.. contents:: Topics
v2.3.2
======
Minor Changes
-------------
- helm_repository - mark `pass_credentials` as no_log=True to silence false warning (https://github.com/ansible-collections/kubernetes.core/issues/412).
- kubectl.py - replace distutils.spawn.find_executable with shutil.which in the kubectl connection plugin (https://github.com/ansible-collections/kubernetes.core/pull/456).
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
======

View File

@@ -1,5 +1,5 @@
# Also needs to be updated in galaxy.yml
VERSION = 2.2.0
VERSION = 2.3.2
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
View 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.

View File

@@ -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:
@@ -68,6 +74,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 +92,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.3.2
```
### Installing the Kubernetes Python Library
@@ -165,7 +172,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 +183,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

View File

@@ -1 +1,2 @@
kubernetes-client [platform:fedora]
openshift-clients [platform:rhel-8]

View File

@@ -486,3 +486,115 @@ 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.3.2:
changes:
minor_changes:
- helm_repository - mark `pass_credentials` as no_log=True to silence false
warning (https://github.com/ansible-collections/kubernetes.core/issues/412).
- 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:
- 412_pass_creds.yml
- 456-replace-distutils.yml
release_date: '2022-06-09'

View File

@@ -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.

View File

@@ -172,6 +172,31 @@ Parameters
<div style="font-size: small; color: darkgreen"><br/>aliases: namespace</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>release_state</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>Show releases as per their states.</div>
<div>Default value is <code>deployed</code> and <code>failed</code>.</div>
<div>If set to <code>all</code>, show all releases without any filter applied.</div>
<div>If set to <code>deployed</code>, show deployed releases.</div>
<div>If set to <code>failed</code>, show failed releases.</div>
<div>If set to <code>pending</code>, show pending releases.</div>
<div>If set to <code>superseded</code>, show superseded releases.</div>
<div>If set to <code>uninstalled</code>, show uninstalled releases, if <code>helm uninstall --keep-history</code> was used.</div>
<div>If set to <code>uninstalling</code>, show releases that are currently being uninstalled.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -204,11 +229,18 @@ Examples
.. code-block:: yaml
- name: Deploy latest version of Grafana chart inside monitoring namespace
- name: Gather information of Grafana chart inside monitoring namespace
kubernetes.core.helm_info:
name: test
release_namespace: monitoring
- name: Gather information about test-chart with pending state
kubernetes.core.helm_info:
name: test-chart
release_namespace: testenv
release_state:
- pending
Return Values

View File

@@ -413,6 +413,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 +447,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 +508,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&gt;=3.7.0. Added in version 2.3.0.</div>
</td>
</tr>
<tr>
@@ -506,12 +525,19 @@ 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>
<br/>
Notes
-----
.. note::
- The default idempotency check can fail to report changes when ``release_state`` is set to ``present`` and ``chart_repo_url`` is defined. Install helm diff >= 3.4.1 for better results.
Examples
@@ -561,6 +587,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:

View File

@@ -150,7 +150,7 @@ Parameters
</td>
<td>
<div>Name of Helm plugin.</div>
<div>Required only if <code>state=absent</code>.</div>
<div>Required only if <code>state=absent</code> or <code>state=latest</code>.</div>
</td>
</tr>
<tr>
@@ -170,6 +170,23 @@ Parameters
<div>Required only if <code>state=present</code>.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>plugin_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>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
</td>
<td>
</td>
<td>
<div>Plugin version to install. If this is not specified, the latest version is installed.</div>
<div>Ignored when <code>state=absent</code> or <code>state=latest</code>.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -183,10 +200,12 @@ Parameters
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li>absent</li>
<li><div style="color: blue"><b>present</b>&nbsp;&larr;</div></li>
<li>latest</li>
</ul>
</td>
<td>
<div>If <code>state=present</code> the Helm plugin will be installed.</div>
<div>If <code>state=latest</code> the Helm plugin will be updated. Added in version 2.3.0.</div>
<div>If <code>state=absent</code> the Helm plugin will be removed.</div>
</td>
</tr>
@@ -237,6 +256,17 @@ Examples
plugin_name: env
state: absent
- 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 Values

View File

@@ -40,6 +40,22 @@ Parameters
<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>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>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
</td>
<td>
</td>
<td>
<div>Token used to authenticate with the API. Can also be specified via <code>K8S_AUTH_API_KEY</code> environment variable.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -55,6 +71,59 @@ Parameters
<div>The path of a helm binary to use.</div>
</td>
</tr>
<tr>
<td colspan="1">
<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>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</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 <code>K8S_AUTH_SSL_CA_CERT</code> environment variable.</div>
<div style="font-size: small; color: darkgreen"><br/>aliases: ssl_ca_cert</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>host</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
</div>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
</td>
<td>
</td>
<td>
<div>Provide a URL for accessing the API. Can also be specified via <code>K8S_AUTH_HOST</code> environment variable.</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>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
</td>
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li><div style="color: blue"><b>no</b>&nbsp;&larr;</div></li>
<li>yes</li>
</ul>
</td>
<td>
<div>Pass credentials to all domains.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -142,6 +211,27 @@ Parameters
<div style="font-size: small; color: darkgreen"><br/>aliases: username</div>
</td>
</tr>
<tr>
<td colspan="1">
<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>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
</td>
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li>no</li>
<li><div style="color: blue"><b>yes</b>&nbsp;&larr;</div></li>
</ul>
</td>
<td>
<div>Whether or not to verify the API server&#x27;s SSL certificates. Can also be specified via <code>K8S_AUTH_VERIFY_SSL</code> environment variable.</div>
<div style="font-size: small; color: darkgreen"><br/>aliases: verify_ssl</div>
</td>
</tr>
</table>
<br/>

View File

@@ -131,6 +131,22 @@ Parameters
<div>If the directory already exists, it will be overwritten.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>release_namespace</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>namespace scope for this request.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -148,6 +164,23 @@ Parameters
<div style="font-size: small; color: darkgreen"><br/>aliases: values</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>show_only</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>Only show manifests rendered from the given templates.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -213,6 +246,24 @@ Examples
dest: myfile.yaml
content: "{{ result.stdout }}"
- 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 Values

View File

@@ -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>
@@ -171,6 +206,25 @@ Parameters
<div>The kubernetes configuration can be provided as dictionary. This feature requires a python kubernetes client version &gt;= 17.17.0. Added in version 2.2.0.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>

View File

@@ -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>
@@ -236,6 +271,25 @@ Parameters
<div>This option is ignored when <em>content</em> is set or when <em>state</em> is set to <code>from_pod</code>.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>

View File

@@ -140,6 +140,27 @@ 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>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 2.3.0</div>
</td>
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li><div style="color: blue"><b>no</b>&nbsp;&larr;</div></li>
<li>yes</li>
</ul>
</td>
<td>
<div>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 +287,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>
@@ -298,6 +354,25 @@ Parameters
<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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>

View File

@@ -117,7 +117,7 @@ Parameters
<td>
</td>
<td>
<div>The command to execute</div>
<div>The command to execute.</div>
</td>
</tr>
<tr>
@@ -134,6 +134,7 @@ Parameters
<td>
<div>The name of the container in the pod to connect to.</div>
<div>Defaults to only container if there is only one container in the pod.</div>
<div>If not specified, will choose the first container from the given pod as kubectl cmdline does.</div>
</td>
</tr>
<tr>
@@ -166,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>
@@ -195,7 +231,26 @@ Parameters
<td>
</td>
<td>
<div>The pod namespace name</div>
<div>The pod namespace name.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
@@ -250,7 +305,7 @@ Parameters
<td>
</td>
<td>
<div>The pod name</div>
<div>The pod name.</div>
</td>
</tr>
<tr>
@@ -267,7 +322,7 @@ Parameters
<td>
<div>The URL of an HTTP proxy to use for the connection.</div>
<div>Can also be specified via <em>K8S_AUTH_PROXY</em> environment variable.</div>
<div>Please note that this module does not pick up typical proxy settings from the environment (e.g. HTTP_PROXY).</div>
<div>Please note that this module does not pick up typical proxy settings from the environment (for example, HTTP_PROXY).</div>
</td>
</tr>
<tr>
@@ -414,6 +469,13 @@ Examples
msg: "cmd failed"
when: command_status.rc != 0
- 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 Values

View File

@@ -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>
@@ -260,6 +295,25 @@ Parameters
<div>If <em>resource definition</em> is provided, the <em>metadata.namespace</em> value from the <em>resource_definition</em> will override this option.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>

View File

@@ -351,8 +351,8 @@ Status
Authors
~~~~~~~
- Chris Houseknecht <@chouseknecht>
- Fabian von Feilitzsch <@fabianvf>
- Chris Houseknecht (@chouseknecht)
- Fabian von Feilitzsch (@fabianvf)
.. hint::

View File

@@ -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>
@@ -221,6 +256,25 @@ Parameters
<div>Use in conjunction with <em>api_version</em>, <em>kind</em>, and <em>name</em> to identify a specific object.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>

View File

@@ -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>
@@ -260,6 +295,25 @@ Parameters
<div>If <em>resource definition</em> is provided, the <em>metadata.namespace</em> value from the <em>resource_definition</em> will override this option.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>

View File

@@ -438,106 +438,28 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late
<table border=0 cellpadding=0 class="documentation-table">
<tr>
<th colspan="2">Key</th>
<th colspan="1">Key</th>
<th>Returned</th>
<th width="100%">Description</th>
</tr>
<tr>
<td colspan="2">
<td colspan="1">
<div class="ansibleOptionAnchor" id="return-"></div>
<b>_list</b>
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
<div style="font-size: small">
<span style="color: purple">complex</span>
<span style="color: purple">list</span>
/ <span style="color: purple">elements=dictionary</span>
</div>
</td>
<td></td>
<td>
<div>One ore more object definitions returned from the API.</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;">[{&#x27;kind&#x27;: &#x27;ConfigMap&#x27;, &#x27;apiVersion&#x27;: &#x27;v1&#x27;, &#x27;metadata&#x27;: {&#x27;creationTimestamp&#x27;: &#x27;2022-03-04T13:59:49Z&#x27;, &#x27;name&#x27;: &#x27;my-config-map&#x27;, &#x27;namespace&#x27;: &#x27;default&#x27;, &#x27;resourceVersion&#x27;: &#x27;418&#x27;, &#x27;uid&#x27;: &#x27;5714b011-d090-4eac-8272-a0ea82ec0abd&#x27;}, &#x27;data&#x27;: {&#x27;key1&#x27;: &#x27;val1&#x27;}}]</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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/>
@@ -549,8 +471,8 @@ Status
Authors
~~~~~~~
- Chris Houseknecht <@chouseknecht>
- Fabian von Feilitzsch <@fabianvf>
- Chris Houseknecht (@chouseknecht)
- Fabian von Feilitzsch (@fabianvf)
.. hint::

View File

@@ -336,6 +336,26 @@ Parameters
<div>If set to <code>yes</code>, and <em>state</em> is <code>present</code>, an existing object will be replaced.</div>
</td>
</tr>
<tr>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>generate_name</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>Use to specify the basis of an object name and random characters will be added automatically on server to generate a unique name.</div>
<div>This option is ignored when <em>state</em> is not set to <code>present</code> or when <em>apply</em> is set to <code>yes</code>.</div>
<div>If <em>resource definition</em> is provided, the <em>metadata.generateName</em> value from the <em>resource_definition</em> will override this option.</div>
<div>If <em>resource definition</em> is provided, and contains <em>metadata.name</em>, this option is ignored.</div>
<div>mutually exclusive with <code>name</code>.</div>
</td>
</tr>
<tr>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -351,6 +371,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>
@@ -464,6 +519,25 @@ Parameters
<div>If <em>resource definition</em> is provided, the <em>metadata.namespace</em> value from the <em>resource_definition</em> will override this option.</div>
</td>
</tr>
<tr>
<td colspan="3">
<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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -605,6 +679,63 @@ Parameters
<div style="font-size: small; color: darkgreen"><br/>aliases: definition, inline</div>
</td>
</tr>
<tr>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>server_side_apply</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.3.0</div>
</td>
<td>
</td>
<td>
<div>When this option is set, apply runs in the server instead of the client.</div>
<div>Ignored if <code>apply</code> is not set or is set to False.</div>
<div>This option requires &quot;kubernetes &gt;= 19.15.0&quot;.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>field_manager</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>Name of the manager used to track field ownership.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>force_conflicts</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>&nbsp;&larr;</div></li>
<li>yes</li>
</ul>
</td>
<td>
<div>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.</div>
<div>When set to True, server-side apply will force the changes against conflicts.</div>
</td>
</tr>
<tr>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -619,7 +750,7 @@ Parameters
<td>
<div>Provide a path to a file containing a valid YAML definition of an object or objects to be created or updated. Mutually exclusive with <em>resource_definition</em>. NOTE: <em>kind</em>, <em>api_version</em>, <em>name</em>, and <em>namespace</em> will be overwritten by corresponding values found in the configuration read in from the <em>src</em> file.</div>
<div>Reads from the local file system. To read from the Ansible controller&#x27;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 <em>resource_definition</em>. See Examples below.</div>
<div>Mutually exclusive with <em>template</em> in case of <span class='module'>k8s</span> module.</div>
<div>Mutually exclusive with <em>template</em> in case of <span class='module'>kubernetes.core.k8s</span> module.</div>
</td>
</tr>
<tr>
@@ -1058,6 +1189,33 @@ Examples
labels:
support: patch
# 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 Values

View File

@@ -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>
@@ -258,6 +293,25 @@ Parameters
<div>If <em>resource definition</em> is provided, the <em>metadata.namespace</em> value from the <em>resource_definition</em> will override this option.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>

View File

@@ -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>
@@ -276,6 +311,25 @@ Parameters
<div>If <em>resource definition</em> is provided, the <em>metadata.namespace</em> value from the <em>resource_definition</em> will override this option.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -462,7 +516,7 @@ Parameters
<td>
<div>Provide a path to a file containing a valid YAML definition of an object or objects to be created or updated. Mutually exclusive with <em>resource_definition</em>. NOTE: <em>kind</em>, <em>api_version</em>, <em>name</em>, and <em>namespace</em> will be overwritten by corresponding values found in the configuration read in from the <em>src</em> file.</div>
<div>Reads from the local file system. To read from the Ansible controller&#x27;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 <em>resource_definition</em>. See Examples below.</div>
<div>Mutually exclusive with <em>template</em> in case of <span class='module'>k8s</span> module.</div>
<div>Mutually exclusive with <em>template</em> in case of <span class='module'>kubernetes.core.k8s</span> module.</div>
</td>
</tr>
<tr>

View File

@@ -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>
@@ -245,6 +280,25 @@ Parameters
<div>Use to specify a Service object namespace.</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&#x27;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&gt;=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 &quot;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&quot;</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>
@@ -433,7 +487,7 @@ Parameters
<td>
<div>Provide a path to a file containing a valid YAML definition of an object or objects to be created or updated. Mutually exclusive with <em>resource_definition</em>. NOTE: <em>kind</em>, <em>api_version</em>, <em>name</em>, and <em>namespace</em> will be overwritten by corresponding values found in the configuration read in from the <em>src</em> file.</div>
<div>Reads from the local file system. To read from the Ansible controller&#x27;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 <em>resource_definition</em>. See Examples below.</div>
<div>Mutually exclusive with <em>template</em> in case of <span class='module'>k8s</span> module.</div>
<div>Mutually exclusive with <em>template</em> in case of <span class='module'>kubernetes.core.k8s</span> module.</div>
</td>
</tr>
<tr>

View 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 &gt;= 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&#x27;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&gt;=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 &quot;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&quot;</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>&nbsp;&larr;</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>&nbsp;&larr;</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&#x27;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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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)

View File

@@ -354,7 +354,7 @@ Status
Authors
~~~~~~~
- xuxinkun
- xuxinkun (@xuxinkun)
.. hint::

View File

@@ -133,106 +133,27 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late
<table border=0 cellpadding=0 class="documentation-table">
<tr>
<th colspan="2">Key</th>
<th colspan="1">Key</th>
<th>Returned</th>
<th width="100%">Description</th>
</tr>
<tr>
<td colspan="2">
<td colspan="1">
<div class="ansibleOptionAnchor" id="return-"></div>
<b>_list</b>
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
<div style="font-size: small">
<span style="color: purple">complex</span>
<span style="color: purple">string</span>
</div>
</td>
<td></td>
<td>
<div>One ore more object definitions returned from the tool execution.</div>
<div>YAML string for the object definitions returned from the tool execution.</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;">{&#x27;kind&#x27;: &#x27;ConfigMap&#x27;, &#x27;apiVersion&#x27;: &#x27;v1&#x27;, &#x27;metadata&#x27;: {&#x27;name&#x27;: &#x27;my-config-map&#x27;, &#x27;namespace&#x27;: &#x27;default&#x27;}, &#x27;data&#x27;: {&#x27;key1&#x27;: &#x27;val1&#x27;}}</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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/>
@@ -244,7 +165,7 @@ Status
Authors
~~~~~~~
- Aubin Bikouo <@abikouo>
- Aubin Bikouo (@abikouo)
.. hint::

View File

@@ -25,7 +25,7 @@ tags:
- openshift
- okd
- cluster
version: 2.2.0
version: 2.3.2
build_ignore:
- .DS_Store
- '*.tar.gz'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,12 +0,0 @@
---
- name: Prepare
hosts: localhost
connection: local
collections:
- kubernetes.core
tasks:
- name: Include drain.yml
include_tasks:
file: tasks/drain.yml

View File

@@ -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()

View File

@@ -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

View File

@@ -3,7 +3,8 @@
# 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
@@ -11,15 +12,44 @@ import traceback
import os
from contextlib import contextmanager
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]
class ActionModule(ActionBase):
TRANSFERS_FILES = True
@@ -28,19 +58,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 +78,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 +107,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 +212,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 +227,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 +259,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 +308,22 @@ class ActionModule(ActionBase):
if not remote_transport:
# kubeconfig is local
# find in expected paths
kubeconfig = self._find_needle('files', kubeconfig)
kubeconfig = self._find_needle("files", kubeconfig)
# decrypt kubeconfig found
actual_file = self._loader.get_real_file(kubeconfig, decrypt=True)
new_module_args['kubeconfig'] = actual_file
new_module_args["kubeconfig"] = actual_file
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 +334,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 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)

View File

@@ -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.
@@ -170,9 +171,9 @@ DOCUMENTATION = r"""
aliases: [ kubectl_verify_ssl ]
"""
import distutils.spawn
import os
import os.path
import shutil
import subprocess
from ansible.parsing.yaml.loader import AnsibleLoader
@@ -185,26 +186,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 +218,203 @@ 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))
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 (
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)
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()
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()
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

View File

@@ -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:
@@ -56,4 +57,4 @@ options:
type: path
aliases: [ ssl_ca_cert ]
version_added: "1.2.0"
'''
"""

View File

@@ -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:
@@ -76,6 +77,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 +119,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."
'''
"""

View 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.
'''
"""

View File

@@ -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
'''
"""

View File

@@ -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,6 @@ 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.
- Mutually exclusive with I(template) in case of M(kubernetes.core.k8s) module.
type: path
'''
"""

View File

@@ -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
'''
"""

View File

@@ -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
'''
"""

View 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:
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
'''
"""

View File

@@ -2,12 +2,15 @@
# 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
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 +18,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}

View File

@@ -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
@@ -89,9 +89,9 @@ 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
@@ -112,12 +112,17 @@ 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 (
K8sAnsibleMixin,
HAS_K8S_MODULE_HELPER,
k8s_import_exception,
get_api_client,
)
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
try:
@@ -128,13 +133,13 @@ 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):
@@ -142,10 +147,10 @@ class K8sInventoryException(Exception):
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleMixin):
NAME = 'kubernetes.core.k8s'
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 +159,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 +186,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 +210,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 +250,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 +270,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 +278,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 +366,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 +389,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
)

View File

@@ -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,50 @@ 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.common import (
K8sAnsibleMixin,
get_api_client,
)
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:
@@ -200,12 +211,13 @@ except ImportError as e:
class KubernetesLookup(K8sAnsibleMixin):
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 +238,33 @@ class KubernetesLookup(K8sAnsibleMixin):
self.params = kwargs
self.client = get_api_client(**kwargs)
cluster_info = kwargs.get('cluster_info')
if cluster_info == 'version':
cluster_info = kwargs.get("cluster_info")
if cluster_info == "version":
return [self.client.version]
if cluster_info == 'api_groups':
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]
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(
@@ -260,17 +274,23 @@ class KubernetesLookup(K8sAnsibleMixin):
resource = self.find_resource(self.kind, self.api_version, fail=True)
try:
k8s_obj = resource.get(name=self.name, namespace=self.namespace, label_selector=self.label_selector, field_selector=self.field_selector)
k8s_obj = resource.get(
name=self.name,
namespace=self.namespace,
label_selector=self.label_selector,
field_selector=self.field_selector,
)
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

View File

@@ -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")]

View 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

View File

@@ -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

View File

@@ -14,13 +14,16 @@
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,
)
try:
from kubernetes.dynamic.exceptions import NotFoundError
@@ -28,50 +31,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 +84,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 +114,48 @@ 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:
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"),
)
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 +164,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 +174,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 +195,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 +240,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 +289,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:

View File

@@ -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',
}
}
}
}

View File

@@ -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("/")
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

View File

@@ -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

View File

@@ -0,0 +1,409 @@
# 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.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")
@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):
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 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):
# 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
),
)
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)

View File

@@ -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"""

View File

@@ -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"))
)

View File

@@ -11,12 +11,14 @@ from contextlib import contextmanager
import os
import tempfile
import traceback
import re
from ansible.module_utils.basic import missing_required_lib
try:
import yaml
HAS_YAML = True
except ImportError:
YAML_IMP_ERR = traceback.format_exc()
@@ -27,11 +29,11 @@ except ImportError:
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')
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"):
@@ -40,7 +42,8 @@ def prepare_helm_environ_update(module):
kubeconfig_path = write_temp_kubeconfig(
module.params["host"],
validate_certs=module.params["validate_certs"],
ca_cert=module.params["ca_cert"])
ca_cert=module.params["ca_cert"],
)
file_to_cleam_up = kubeconfig_path
if kubeconfig_path is not None:
environ_update["KUBECONFIG"] = kubeconfig_path
@@ -60,7 +63,9 @@ def run_helm(module, command, fails_on_error=True):
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),
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
rc, out, err
),
stdout=out,
stderr=err,
command=command,
@@ -89,23 +94,11 @@ def write_temp_kubeconfig(server, validate_certs=True, ca_cert=None):
content = {
"apiVersion": "v1",
"kind": "Config",
"clusters": [
{
"cluster": {
"server": server,
},
"name": "generated-cluster"
}
],
"clusters": [{"cluster": {"server": server}, "name": "generated-cluster"}],
"contexts": [
{
"context": {
"cluster": "generated-cluster"
},
"name": "generated-context"
}
{"context": {"cluster": "generated-cluster"}, "name": "generated-context"}
],
"current-context": "generated-context"
"current-context": "generated-context",
}
if not validate_certs:
@@ -114,7 +107,7 @@ def write_temp_kubeconfig(server, validate_certs=True, ca_cert=None):
content["clusters"][0]["cluster"]["certificate-authority"] = ca_cert
_fd, file_name = tempfile.mkstemp()
with os.fdopen(_fd, 'w') as fp:
with os.fdopen(_fd, "w") as fp:
yaml.dump(content, fp)
return file_name
@@ -127,7 +120,7 @@ def get_helm_plugin_list(module, helm_bin=None):
return []
helm_plugin_list = helm_bin + " list"
rc, out, err = run_helm(module, helm_plugin_list)
if rc != 0 or (out == '' and err == ''):
if rc != 0 or (out == "" and err == ""):
module.fail_json(
msg="Failed to get Helm plugin info",
command=helm_plugin_list,
@@ -149,12 +142,23 @@ 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 get_helm_version(module, helm_bin):
helm_version_command = helm_bin + " version"
rc, out, err = module.run_command(helm_version_command)
if rc == 0:
m = re.match(r'version.BuildInfo{Version:"v([0-9\.]*)",', out)
if m:
return m.group(1)
return None

View File

@@ -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)
)

View File

@@ -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)

View 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

View File

@@ -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:
@@ -86,7 +91,7 @@ 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
@@ -108,13 +113,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.
@@ -148,9 +164,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 +209,13 @@ 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
# From git
- name: Git clone stable repo on HEAD
ansible.builtin.git:
@@ -237,7 +260,7 @@ EXAMPLES = r'''
enabled: True
logging:
enabled: True
'''
"""
RETURN = r"""
status:
@@ -296,9 +319,13 @@ command:
import tempfile
import traceback
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
LooseVersion,
)
try:
import yaml
IMP_YAML = True
except ImportError:
IMP_YAML_ERR = traceback.format_exc()
@@ -309,7 +336,8 @@ from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm,
get_values,
get_helm_plugin_list,
parse_helm_plugin_list
parse_helm_plugin_list,
get_helm_version,
)
@@ -320,7 +348,7 @@ 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
@@ -339,7 +367,7 @@ 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"] = get_values(module, command, release_name)
return release
@@ -363,9 +391,23 @@ def fetch_chart_info(module, command, chart_ref):
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,
skip_crds=False,
timeout=None,
):
"""
Install/upgrade/rollback release chart
"""
@@ -386,6 +428,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,8 +448,8 @@ 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
@@ -418,7 +463,7 @@ def deploy(command, release_name, release_values, chart_name, wait,
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 +476,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 +490,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,9 +499,9 @@ def load_values_files(values_files):
return values
def has_plugin(command, plugin):
def get_plugin_version(command, plugin):
"""
Check if helm plugin is installed.
Check if helm plugin is installed and return corresponding version
"""
cmd = command + " plugin"
@@ -458,16 +509,25 @@ def has_plugin(command, plugin):
out = parse_helm_plugin_list(module, 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,
helm_cmd,
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.
"""
@@ -475,14 +535,16 @@ def helmdiff_check(module, helm_cmd, release_name, chart_ref, release_values,
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
@@ -491,7 +553,7 @@ def helmdiff_check(module, helm_cmd, release_name, chart_ref, release_values,
cmd += " -f=" + values_file
rc, out, err = run_helm(module, cmd)
return len(out.strip()) > 0
return (len(out.strip()) > 0, out.strip())
def default_check(release_status, chart_info, values=None, values_files=None):
@@ -499,64 +561,89 @@ 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 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),
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'),
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"),
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']))
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"])
),
),
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,32 +653,33 @@ 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')
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")
# 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')
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")
timeout = module.params.get("timeout")
if bin_path is not None:
helm_cmd_common = bin_path
else:
helm_cmd_common = module.get_bin_path('helm', required=True)
helm_cmd_common = module.get_bin_path("helm", required=True)
if update_repo_cache:
run_repo_update(module, helm_cmd_common)
@@ -601,11 +689,23 @@ def main():
# keep helm_cmd_common for get_release_status in module_exit_json
helm_cmd = helm_cmd_common
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 = get_helm_version(module, helm_cmd_common)
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":
@@ -619,54 +719,99 @@ def main():
chart_info = fetch_chart_info(module, helm_cmd, chart_ref)
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)
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,
timeout=timeout,
)
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(helm_cmd_common, "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,
helm_cmd_common,
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)
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,
timeout=timeout,
)
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)
@@ -677,8 +822,9 @@ def main():
stderr=err,
status=get_release_status(module, helm_cmd_common, release_name),
command=helm_cmd,
**opt_result,
)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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,39 @@ 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
version_added: "2.3.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 +109,61 @@ status:
type: str
returned: always
description: Dict of Values used to deploy
'''
"""
import traceback
try:
import yaml
IMP_YAML = True
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_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm,
get_values,
)
# 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, command, release_name, release_state):
list_command = command + " list --output=yaml"
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 = run_helm(module, 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,7 +171,7 @@ 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"] = get_values(module, command, release_name)
return release
@@ -138,25 +181,43 @@ def main():
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']),
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'])),
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']))
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"])
),
release_state=dict(type="list", default=[], elements="str"),
),
mutually_exclusive=[
("context", "ca_cert"),
("context", "validate_certs"),
("kubeconfig", "ca_cert"),
("kubeconfig", "validate_certs")
("kubeconfig", "validate_certs"),
],
supports_check_mode=True,
)
@@ -164,15 +225,18 @@ def main():
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')
bin_path = module.params.get("binary_path")
release_name = module.params.get("release_name")
release_state = module.params.get("release_state")
if bin_path is not None:
helm_cmd_common = bin_path
else:
helm_cmd_common = module.get_bin_path('helm', required=True)
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, helm_cmd_common, release_name, release_state
)
if release_status is not None:
module.exit_json(changed=False, status=release_status)
@@ -180,5 +244,5 @@ def main():
module.exit_json(changed=False)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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,98 @@ rc:
description: Helm plugin command return code
returned: always
sample: 1
'''
"""
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm,
get_helm_plugin_list,
parse_helm_plugin_list
parse_helm_plugin_list,
)
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',),
binary_path=dict(type="path"),
state=dict(
type="str", default="present", choices=["present", "absent", "latest"]
),
plugin_path=dict(
type="str",
),
plugin_name=dict(
type="str",
),
plugin_version=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'])),
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']))
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"])
),
),
supports_check_mode=True,
required_if=[
("state", "present", ("plugin_path",)),
("state", "absent", ("plugin_name",)),
("state", "latest", ("plugin_name",)),
],
mutually_exclusive=[
('plugin_name', 'plugin_path'),
("plugin_name", "plugin_path"),
("context", "ca_cert"),
("context", "validate_certs"),
("kubeconfig", "ca_cert"),
("kubeconfig", "validate_certs")
("kubeconfig", "validate_certs"),
],
)
bin_path = module.params.get('binary_path')
state = module.params.get('state')
bin_path = module.params.get("binary_path")
state = module.params.get("state")
if bin_path is not None:
helm_cmd_common = bin_path
else:
helm_cmd_common = 'helm'
helm_cmd_common = "helm"
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)
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 +205,7 @@ def main():
command=helm_cmd_common,
stdout=out,
stderr=err,
rc=rc
rc=rc,
)
elif rc == 0:
module.exit_json(
@@ -175,8 +225,8 @@ def main():
stderr=err,
rc=rc,
)
elif state == 'absent':
plugin_name = module.params.get('plugin_name')
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())
@@ -188,7 +238,7 @@ def main():
command=helm_cmd_common + " list",
stdout=output,
stderr=err,
rc=rc
rc=rc,
)
found = False
@@ -204,14 +254,14 @@ def main():
command=helm_cmd_common + " list",
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)
else:
rc, out, err = (0, '', '')
rc, out, err = (0, "", "")
if rc == 0:
module.exit_json(
@@ -220,7 +270,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 +279,61 @@ def main():
stderr=err,
rc=rc,
)
elif state == "latest":
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())
if not out:
module.exit_json(
failed=False,
changed=False,
msg="Plugin not found",
command=helm_cmd_common + " list",
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=helm_cmd_common + " list",
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 = run_helm(module, 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()

View File

@@ -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,7 +68,7 @@ rc:
description: Helm plugin command return code
returned: always
sample: 1
'''
"""
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
@@ -80,39 +80,59 @@ from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
def main():
module = AnsibleModule(
argument_spec=dict(
binary_path=dict(type='path'),
plugin_name=dict(type='str',),
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'])),
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']))
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")
("kubeconfig", "validate_certs"),
],
supports_check_mode=True,
)
bin_path = module.params.get('binary_path')
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 = "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 = []
@@ -123,21 +143,13 @@ def main():
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
@@ -151,5 +163,5 @@ def main():
)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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,41 @@ 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"
"""
EXAMPLES = r'''
EXAMPLES = r"""
- name: Add a repository
kubernetes.core.helm_repository:
name: stable
@@ -76,9 +109,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,18 +142,19 @@ msg:
description: Error message returned by `helm` command
returned: on failure
sample: 'Repository already have a repository named bitnami'
'''
"""
import traceback
try:
import yaml
IMP_YAML = True
except ImportError:
IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.basic import AnsibleModule, env_fallback, missing_required_lib
from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm
@@ -128,7 +162,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_he
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
@@ -144,21 +178,33 @@ def get_repository_status(module, command, repository_name):
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,
):
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"
return install_command
@@ -174,19 +220,34 @@ def main():
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']),
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"]
),
pass_credentials=dict(type="bool", default=False, no_log=True),
# 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"])
),
),
required_together=[
['repo_username', 'repo_password']
],
required_if=[
('repo_state', 'present', ['repo_url']),
],
required_together=[["repo_username", "repo_password"]],
required_if=[("repo_state", "present", ["repo_url"])],
supports_check_mode=True,
)
@@ -195,17 +256,18 @@ 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')
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")
pass_credentials = module.params.get("pass_credentials")
if bin_path is not None:
helm_cmd = bin_path
else:
helm_cmd = module.get_bin_path('helm', required=True)
helm_cmd = module.get_bin_path("helm", required=True)
repository_status = get_repository_status(module, helm_cmd, repo_name)
@@ -214,10 +276,19 @@ def main():
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)
helm_cmd = install_repository(
helm_cmd,
repo_name,
repo_url,
repo_username,
repo_password,
pass_credentials,
)
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)
@@ -227,16 +298,18 @@ def main():
rc, out, err = run_helm(module, 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()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
DOCUMENTATION = r"""
module: helm_template
@@ -57,6 +57,12 @@ options:
- If the directory already exists, it will be overwritten.
required: false
type: path
release_namespace:
description:
- namespace scope for this request.
required: false
type: str
version_added: 2.3.0
release_values:
description:
- Values to pass to chart.
@@ -64,6 +70,13 @@ options:
default: {}
aliases: [ values ]
type: dict
show_only:
description:
- Only show manifests rendered from the given templates.
required: false
type: list
elements: str
version_added: 2.3.0
values_files:
description:
- Value files to pass to chart.
@@ -79,9 +92,9 @@ 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
'''
"""
EXAMPLES = r'''
EXAMPLES = r"""
- name: Render templates to specified directory
kubernetes.core.helm_template:
chart_ref: stable/prometheus
@@ -96,9 +109,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,13 +145,14 @@ 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
except ImportError:
IMP_YAML_ERR = traceback.format_exc()
@@ -130,8 +162,18 @@ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm
def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir=None,
release_values=None, values_files=None, include_crds=False):
def template(
cmd,
chart_ref,
chart_repo_url=None,
chart_version=None,
output_dir=None,
show_only=None,
release_values=None,
release_namespace=None,
values_files=None,
include_crds=False,
):
cmd += " template " + chart_ref
if chart_repo_url:
@@ -143,16 +185,23 @@ def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir
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"
@@ -162,43 +211,55 @@ def template(cmd, chart_ref, chart_repo_url=None, chart_version=None, output_dir
def main():
module = AnsibleModule(
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"),
include_crds=dict(type="bool", default=False),
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),
),
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')
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")
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")
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 = bin_path or module.get_bin_path("helm", required=True)
if update_repo_cache:
update_cmd = helm_cmd + " repo update"
run_helm(module, 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)
tmpl_cmd = template(
helm_cmd,
chart_ref,
chart_repo_url=chart_repo_url,
chart_version=chart_version,
output_dir=output_dir,
release_namespace=release_namespace,
release_values=release_values,
show_only=show_only,
values_files=values_files,
include_crds=include_crds,
)
if not check_mode:
rc, out, err = run_helm(module, tmpl_cmd)
@@ -207,14 +268,9 @@ def main():
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()

View File

@@ -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
@@ -278,9 +308,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 +377,34 @@ 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,
)
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,16 +413,26 @@ 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
@@ -367,10 +448,11 @@ def execute_module(module, k8s_ansible_mixin):
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.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.generate_name = k8s_ansible_mixin.params.get("generate_name")
k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get("namespace")
k8s_ansible_mixin.check_library_version()
k8s_ansible_mixin.set_resource_definitions(module)
@@ -379,19 +461,26 @@ def execute_module(module, k8s_ansible_mixin):
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)
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)
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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,7 +137,7 @@ apis:
description: Resource singular name
returned: success
type: str
'''
"""
import copy
@@ -145,7 +146,10 @@ from collections import defaultdict
HAS_K8S = False
try:
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import ResourceList
from ansible_collections.kubernetes.core.plugins.module_utils.client.resource import (
ResourceList,
)
HAS_K8S = True
except ImportError as e:
K8S_IMP_ERR = e
@@ -154,12 +158,18 @@ except ImportError as e:
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.args_common import (
AUTH_ARG_SPEC,
)
def execute_module(module, client):
invalidate_cache = boolean(module.params.get('invalidate_cache', True), strict=False)
invalidate_cache = boolean(
module.params.get("invalidate_cache", True), strict=False
)
if invalidate_cache:
client.resources.invalidate_cache()
results = defaultdict(dict)
@@ -167,47 +177,60 @@ def execute_module(module, client):
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.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
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))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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:
@@ -125,327 +126,57 @@ EXAMPLES = r'''
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.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):
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module, pyyaml_required=False)
k8s_ansible_mixin.check_library_version()
@@ -457,37 +188,34 @@ def execute_module(module):
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")
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, k8s_ansible_mixin.client)
else:
k8s_copy = K8SCopyFromPod(module, k8s_ansible_mixin.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 = 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)
module = AnsibleModule(
argument_spec=argspec(),
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)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
DOCUMENTATION = r"""
module: k8s_drain
@@ -64,6 +64,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 +89,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
@@ -109,32 +115,53 @@ EXAMPLES = r'''
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.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 +171,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,39 +193,62 @@ 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)
K8sAnsibleMixin,
get_api_client,
)
self._module = module
self._k8s_ansible_mixin = K8sAnsibleMixin(module)
@@ -215,17 +264,18 @@ class K8sDrainAnsible(object):
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._api_instance = core_v1_api.CoreV1Api(
self._k8s_ansible_mixin.client.client
)
self._k8s_ansible_mixin.check_library_version()
# 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 +291,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 +311,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 +361,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 +485,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),
),
),
)
)
@@ -403,9 +505,16 @@ def argspec():
def main():
module = AnsibleModule(argument_spec=argspec())
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),
)
k8s_drain = K8sDrainAnsible(module)
k8s_drain.execute_module()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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,18 @@ 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,
)
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,10 +150,10 @@ 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
@@ -153,8 +164,21 @@ def execute_module(module, k8s_ansible_mixin):
# 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 +189,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,19 +206,23 @@ 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 deprecated. Please use 'rc' instead.",
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,
)
@@ -200,12 +232,14 @@ def main():
supports_check_mode=True,
)
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin, get_api_client)
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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
@@ -52,9 +52,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 +109,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,12 +144,17 @@ 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,
)
def execute_module(module, k8s_ansible_mixin):
@@ -174,11 +179,11 @@ 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
@@ -187,12 +192,18 @@ def argspec():
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)
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
k8s_ansible_mixin.fail_json = module.fail_json
k8s_ansible_mixin.fail = module.fail_json
k8s_ansible_mixin.exit_json = module.exit_json
k8s_ansible_mixin.warn = module.warn
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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
@@ -81,9 +81,9 @@ EXAMPLES = r'''
- op: replace
patch: /spec/containers/0/image
value: nginx
'''
"""
RETURN = r'''
RETURN = r"""
result:
description: The modified object.
returned: success
@@ -122,17 +122,24 @@ 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.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)
get_api_client,
K8sAnsibleMixin,
)
try:
from kubernetes.dynamic.exceptions import DynamicApiError
@@ -143,6 +150,7 @@ except ImportError:
JSON_PATCH_IMPORT_ERR = None
try:
import jsonpatch
HAS_JSON_PATCH = True
except ImportError:
HAS_JSON_PATCH = False
@@ -150,33 +158,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,16 +178,10 @@ 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
@@ -209,15 +196,14 @@ 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']
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,
}
"metadata": {"name": name, "namespace": namespace},
}
def build_error_msg(kind, name, msg):
@@ -228,30 +214,50 @@ def execute_module(k8s_module, module):
try:
existing = resource.get(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 k8s_module.supports_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 = resource.patch(
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)
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)
result["changed"] = not match
if module._diff:

View File

@@ -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
@@ -65,9 +65,9 @@ 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
@@ -100,9 +100,9 @@ EXAMPLES = r'''
namespace: testing
name: example
register: log
'''
"""
RETURN = r'''
RETURN = r"""
log:
type: str
description:
@@ -113,15 +113,20 @@ log_lines:
description:
- The log of the object, split on newlines
returned: success
'''
"""
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.module_utils.six import PY2
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.args_common import (
AUTH_ARG_SPEC,
NAME_ARG_SPEC,
)
def argspec():
@@ -129,55 +134,62 @@ 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=[]),
)
)
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', {}))
name = module.params.get("name")
namespace = module.params.get("namespace")
label_selector = ",".join(module.params.get("label_selectors", {}))
if name and label_selector:
module.fail(msg='Only one of name or label_selectors can be provided')
module.fail(msg="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 = 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)
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')
module.fail(
msg="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(module, 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))
module.fail(
msg="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
kwargs = {}
if module.params.get('container'):
kwargs['query_params'] = dict(container=module.params['container'])
if module.params.get("container"):
kwargs["query_params"] = dict(container=module.params["container"])
if module.params.get('since_seconds'):
kwargs.setdefault('query_params', {}).update({'sinceSeconds': module.params['since_seconds']})
if module.params.get("since_seconds"):
kwargs.setdefault("query_params", {}).update(
{"sinceSeconds": module.params["since_seconds"]}
)
log = serialize_log(resource.log.get(
name=name,
namespace=namespace,
serialize=False,
**kwargs
))
log = serialize_log(
resource.log.get(name=name, namespace=namespace, serialize=False, **kwargs)
)
module.exit_json(changed=False, log=log, log_lines=log.split('\n'))
module.exit_json(changed=False, log=log, log_lines=log.split("\n"))
def extract_selectors(module, instance):
@@ -185,35 +197,46 @@ def extract_selectors(module, instance):
# 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))
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
)
)
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()))
module.fail(
msg="The k8s_log module does not support the {0} matchExpression operator".format(
operator.lower()
)
)
return selectors
@@ -221,18 +244,20 @@ def extract_selectors(module, instance):
def serialize_log(response):
if PY2:
return response.data
return response.data.decode('utf8')
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)
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -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"
@@ -34,18 +35,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,25 +75,29 @@ 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,
)
def get_managed_resource(module):
managed_resource = {}
kind = module.params['kind']
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))
return managed_resource
@@ -102,80 +107,100 @@ def execute_module(module, k8s_ansible_mixin):
results = []
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'])
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']:
changed = False
for resource in resources["resources"]:
result = perform_action(module, k8s_ansible_mixin, 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']
if module.params["kind"] == "DaemonSet":
current_revision = resource["metadata"]["generation"]
elif module.params["kind"] == "Deployment":
current_revision = resource["metadata"]["annotations"][
"deployment.kubernetes.io/revision"
]
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'],
'')
managed_resource["kind"],
managed_resource["api_version"],
"",
module.params["namespace"],
resource["spec"]["selector"]["matchLabels"],
"",
)
prev_managed_resource = get_previous_revision(managed_resources['resources'],
current_revision)
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
if module.params['kind'] == "Deployment":
del prev_managed_resource['spec']['template']['metadata']['labels']['pod-template-hash']
if module.params["kind"] == "Deployment":
del prev_managed_resource["spec"]["template"]["metadata"]["labels"][
"pod-template-hash"
]
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']
}
}]
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":
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 = k8s_ansible_mixin.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 +209,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 +218,40 @@ 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)
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)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -10,7 +10,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
DOCUMENTATION = r"""
module: k8s_scale
@@ -48,9 +48,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 +105,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 +139,170 @@ 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.ansiblemodule import (
AnsibleModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC)
AUTH_ARG_SPEC,
RESOURCE_ARG_SPEC,
NAME_ARG_SPEC,
)
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,):
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')
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')
label_selectors = module.params.get("label_selectors")
if not label_selectors:
label_selectors = []
continue_on_error = module.params.get('continue_on_error')
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')
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
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
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
NotFoundError,
)
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'))
module.fail_json(
msg="Failed to retrieve requested object: {0}".format(exc),
error=exc.value.get("status"),
)
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 module.params["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 module.params["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"] = resource.patch(existing.to_dict()).to_dict()
else:
result = scale(
module,
k8s_ansible_mixin,
resource,
existing,
replicas,
wait,
wait_time,
wait_sleep,
)
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()}
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 +314,87 @@ 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(
module,
k8s_ansible_mixin,
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'):
if not hasattr(resource, "scale"):
module.fail_json(
msg="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)
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
match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj)
if wait:
result["duration"] = 0
result["result"] = k8s_obj
else:
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()
result["result"] = k8s_obj
if wait and not module.check_mode:
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)
match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj)
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)
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)
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -9,7 +9,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
DOCUMENTATION = r"""
module: k8s_service
@@ -85,9 +85,9 @@ options:
requirements:
- python >= 3.6
- kubernetes >= 12.0.0
'''
"""
EXAMPLES = r'''
EXAMPLES = r"""
- name: Expose https port with ClusterIP
kubernetes.core.k8s_service:
state: present
@@ -111,9 +111,9 @@ EXAMPLES = r'''
protocol: TCP
selector:
key: special
'''
"""
RETURN = r'''
RETURN = r"""
result:
description:
- The created, patched, or otherwise present Service object. Will be empty in the case of a deletion.
@@ -140,32 +140,36 @@ result:
description: Current status details for the object.
returned: success
type: complex
'''
"""
import copy
from collections import defaultdict
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, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC)
AUTH_ARG_SPEC,
COMMON_ARG_SPEC,
RESOURCE_ARG_SPEC,
)
SERVICE_ARG_SPEC = {
'apply': {
'type': 'bool',
'default': False,
"apply": {"type": "bool", "default": False},
"name": {"required": True},
"namespace": {"required": True},
"merge_type": {
"type": "list",
"elements": "str",
"choices": ["json", "merge", "strategic-merge"],
},
'name': {'required': True},
'namespace': {'required': True},
'merge_type': {'type': 'list', 'elements': 'str', 'choices': ['json', 'merge', 'strategic-merge']},
'selector': {'type': 'dict'},
'type': {
'type': 'str',
'choices': [
'NodePort', 'ClusterIP', 'LoadBalancer', 'ExternalName'
],
"selector": {"type": "dict"},
"type": {
"type": "str",
"choices": ["NodePort", "ClusterIP", "LoadBalancer", "ExternalName"],
},
'ports': {'type': 'list', 'elements': 'dict'},
"ports": {"type": "list", "elements": "dict"},
}
@@ -183,7 +187,7 @@ def merge_dicts(x, y):
def argspec():
""" argspec property builder """
"""argspec property builder"""
argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
argument_spec.update(COMMON_ARG_SPEC)
argument_spec.update(RESOURCE_ARG_SPEC)
@@ -192,32 +196,34 @@ def argspec():
def execute_module(module, k8s_ansible_mixin):
""" Module execution """
"""Module execution"""
k8s_ansible_mixin.set_resource_definitions(module)
api_version = 'v1'
selector = module.params.get('selector')
service_type = module.params.get('type')
ports = module.params.get('ports')
api_version = "v1"
selector = module.params.get("selector")
service_type = module.params.get("type")
ports = module.params.get("ports")
definition = defaultdict(defaultdict)
definition['kind'] = 'Service'
definition['apiVersion'] = api_version
definition["kind"] = "Service"
definition["apiVersion"] = api_version
def_spec = definition['spec']
def_spec['type'] = service_type
def_spec['ports'] = ports
def_spec['selector'] = selector
def_spec = definition["spec"]
def_spec["type"] = service_type
def_spec["ports"] = ports
def_spec["selector"] = selector
def_meta = definition['metadata']
def_meta['name'] = module.params.get('name')
def_meta['namespace'] = module.params.get('namespace')
def_meta = definition["metadata"]
def_meta["name"] = module.params.get("name")
def_meta["namespace"] = module.params.get("namespace")
# 'resource_definition:' has lower priority than module parameters
definition = dict(merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition))
definition = dict(
merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition)
)
resource = k8s_ansible_mixin.find_resource('Service', api_version, fail=True)
resource = k8s_ansible_mixin.find_resource("Service", api_version, fail=True)
definition = k8s_ansible_mixin.set_defaults(resource, definition)
result = k8s_ansible_mixin.perform_action(resource, definition)
@@ -227,12 +233,14 @@ def execute_module(module, k8s_ansible_mixin):
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)
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,313 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Alina Buzachis <@alinabuzachis>
# 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: k8s_taint
short_description: Taint a node in a Kubernetes/OpenShift cluster
version_added: "2.3.0"
author: Alina Buzachis (@alinabuzachis)
description:
- 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.
extends_documentation_fragment:
- kubernetes.core.k8s_auth_options
options:
state:
description:
- Determines whether to add or remove taints.
type: str
default: present
choices: [ present, absent ]
name:
description:
- The name of the node.
required: true
type: str
taints:
description:
- List containing the taints.
type: list
required: true
elements: dict
suboptions:
key:
description:
- The taint key to be applied to a node.
type: str
value:
description:
- The taint value corresponding to the taint key.
type: str
effect:
description:
- The effect of the taint on Pods that do not tolerate the taint.
- Required when I(state=present).
type: str
choices: [ NoSchedule, NoExecute, PreferNoSchedule ]
replace:
description:
- If C(true), allow taints to be replaced.
required: false
default: false
type: bool
requirements:
- python >= 3.6
- kubernetes >= 12.0.0
"""
EXAMPLES = r"""
- 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 = r"""
result:
description:
- The tainted Node object. Will be empty in the case of a deletion.
returned: success
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
"""
import copy
from ansible.module_utils.basic import AnsibleModule
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.client.exceptions import ApiException
except ImportError:
# ImportError are managed by the common module already.
pass
def _equal_dicts(a, b):
keys = ["key", "effect"]
if "effect" not in set(a).intersection(b):
keys.remove("effect")
return all((a[x] == b[x] for x in keys))
def _get_difference(a, b):
return [
a_item for a_item in a if not any(_equal_dicts(a_item, b_item) for b_item in b)
]
def _get_intersection(a, b):
return [a_item for a_item in a if any(_equal_dicts(a_item, b_item) for b_item in b)]
def _update_exists(a, b):
return any(
(
any(
_equal_dicts(a_item, b_item)
and a_item.get("value") != b_item.get("value")
for b_item in b
)
for a_item in a
)
)
def argspec():
argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
argument_spec.update(
dict(
state=dict(type="str", choices=["present", "absent"], default="present"),
name=dict(type="str", required=True),
taints=dict(type="list", required=True, elements="dict"),
replace=dict(type="bool", default=False),
)
)
return argument_spec
class K8sTaintAnsible:
def __init__(self, module):
self.module = module
self.k8s_ansible_mixin = K8sAnsibleMixin(module=self.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.changed = False
def get_node(self, 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}' has not been found.".format(name))
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)
)
)
return node
def patch_node(self, taints):
body = {"spec": {"taints": taints}}
try:
result = 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))
)
return result.to_dict()
def execute_module(self):
result = {"result": {}}
state = self.module.params.get("state")
taints = self.module.params.get("taints")
name = self.module.params.get("name")
node = self.get_node(name)
existing_taints = node.spec.to_dict().get("taints") or []
diff = _get_difference(taints, existing_taints)
if state == "present":
if diff:
# There are new taints to be added
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
if self.module.params.get("replace"):
# Patch with the new taints
result["result"] = self.patch_node(taints=taints)
self.module.exit_json(changed=self.changed, **result)
result["result"] = self.patch_node(
taints=[*_get_difference(existing_taints, taints), *taints]
)
else:
# No new taints to be added, but maybe there is something to be updated
if _update_exists(existing_taints, taints):
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
result["result"] = self.patch_node(
taints=[*_get_difference(existing_taints, taints), *taints]
)
else:
result["result"] = node.to_dict()
elif state == "absent":
# Nothing to be removed
if not existing_taints:
result["result"] = node.to_dict()
if not diff:
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
self.patch_node(taints=_get_difference(existing_taints, taints))
else:
if _get_intersection(existing_taints, taints):
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
self.patch_node(taints=_get_difference(existing_taints, taints))
else:
self.module.exit_json(changed=self.changed, **result)
self.module.exit_json(changed=self.changed, **result)
def main():
module = AnsibleModule(
argument_spec=argspec(),
supports_check_mode=True,
)
k8s_taint = K8sTaintAnsible(module)
k8s_taint.execute_module()
if __name__ == "__main__":
main()

View File

@@ -1,3 +0,0 @@
collections:
- name: cloud.common
version: ">=2.0.4"

View File

@@ -0,0 +1,8 @@
# slow - 11min
slow
time=313
helm_info
helm_plugin
helm_plugin_info
helm_repository
helm_template

View File

@@ -1,10 +1,6 @@
---
helm_archive_name: "helm-{{ helm_version }}-{{ ansible_system | lower }}-amd64.tar.gz"
helm_binary: "/tmp/helm/{{ ansible_system | lower }}-amd64/helm"
helm_namespace: helm
tiller_namespace: tiller
tiller_cluster_role: cluster-admin
chart_test: "ingress-nginx"
chart_test_local_path: "nginx-ingress"
@@ -17,3 +13,15 @@ chart_test_git_repo: "http://github.com/helm/charts.git"
chart_test_values:
revisionHistoryLimit: 0
myValue: "changed"
test_namespace:
- "helm-diff"
- "helm-envvars"
- "helm-uninstall"
- "helm-not-installed"
- "helm-crd"
- "helm-url"
- "helm-repository"
- "helm-local-path-001"
- "helm-local-path-002"
- "helm-local-path-003"

View File

@@ -1,9 +1,9 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: foos.example.com
name: foos.ansible.com
spec:
group: example.com
group: ansible.com
versions:
- name: v1
served: true

View File

@@ -0,0 +1,96 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, 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_test_version
short_description: check helm executable version
author:
- Aubin Bikouo (@abikouo)
requirements:
- "helm (https://github.com/helm/helm/releases)"
description:
- validate version of helm binary is lower than the specified version.
options:
binary_path:
description:
- The path of a helm binary to use.
required: false
type: path
version:
description:
- version to test against helm binary.
type: str
default: 3.7.0
"""
EXAMPLES = r"""
- name: validate helm binary version is lower than 3.5.0
helm_test_version:
binary_path: path/to/helm
version: "3.5.0"
"""
RETURN = r"""
message:
type: str
description: Text message describing the test result.
returned: always
sample: 'version installed: 3.4.5 is lower than version 3.5.0'
result:
type: bool
description: Test result.
returned: always
sample: 1
"""
import re
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
LooseVersion,
)
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec=dict(
binary_path=dict(type="path"),
version=dict(type="str", default="3.7.0"),
),
)
bin_path = module.params.get("binary_path")
version = module.params.get("version")
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)
rc, out, err = module.run_command([helm_cmd_common, "version"])
if rc != 0:
module.fail_json(msg="helm version failed.", err=err, out=out, rc=rc)
m = re.match(r'version.BuildInfo{Version:"v([0-9\.]*)",', out)
installed_version = m.group(1)
message = "version installed: %s" % installed_version
if LooseVersion(installed_version) < LooseVersion(version):
message += " is lower than version %s" % version
module.exit_json(changed=False, result=True, message=message)
else:
message += " is greater than version %s" % version
module.exit_json(changed=False, result=False, message=message)
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More