handle aliases for lookup and inventory plugins for authentication options (#500)

Honor aliases for lookup and inventory plugins

rebase and extend the following PR #71
ISSUE TYPE


Bugfix Pull Request

Reviewed-by: Mike Graves <mgraves@redhat.com>
This commit is contained in:
Bikouo Aubin
2022-08-23 09:58:08 +02:00
committed by GitHub
parent c4c12ca2c3
commit 5ff3566f30
15 changed files with 477 additions and 24 deletions

View File

@@ -0,0 +1,46 @@
---
- name: Create inventory files
hosts: localhost
gather_facts: false
collections:
- kubernetes.core
roles:
- role: setup_kubeconfig
kubeconfig_operation: 'save'
tasks:
- name: Create inventory files
copy:
content: "{{ item.content }}"
dest: "{{ item.path }}"
vars:
hostname: "{{ lookup('file', user_credentials_dir + '/host_data.txt') }}"
test_cert_file: "{{ user_credentials_dir | realpath + '/cert_file_data.txt' }}"
test_key_file: "{{ user_credentials_dir | realpath + '/key_file_data.txt' }}"
test_ca_cert: "{{ user_credentials_dir | realpath + '/ssl_ca_cert_data.txt' }}"
with_items:
- path: "test_inventory_aliases_with_ssl_k8s.yml"
content: |
---
plugin: kubernetes.core.k8s
connections:
- namespaces:
- inventory
host: "{{ hostname }}"
cert_file: "{{ test_cert_file }}"
key_file: "{{ test_key_file }}"
verify_ssl: true
ssl_ca_cert: "{{ test_ca_cert }}"
- path: "test_inventory_aliases_no_ssl_k8s.yml"
content: |
---
plugin: kubernetes.core.k8s
connections:
- namespaces:
- inventory
host: "{{ hostname }}"
cert_file: "{{ test_cert_file }}"
key_file: "{{ test_key_file }}"
verify_ssl: false

View File

@@ -0,0 +1,30 @@
---
- name: Delete inventory namespace
hosts: localhost
connection: local
gather_facts: true
roles:
- role: setup_kubeconfig
kubeconfig_operation: 'revert'
tasks:
- name: Delete temporary files
file:
state: absent
path: "{{ user_credentials_dir ~ '/' ~ item }}"
ignore_errors: true
with_items:
- test_inventory_aliases_with_ssl_k8s.yml
- test_inventory_aliases_no_ssl_k8s.yml
- ssl_ca_cert_data.txt
- key_file_data.txt
- cert_file_data.txt
- host_data.txt
- name: Remove inventory namespace
k8s:
api_version: v1
kind: Namespace
name: inventory
state: absent

View File

@@ -88,15 +88,3 @@
- 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

@@ -2,7 +2,28 @@
set -eux
export ANSIBLE_ROLES_PATH="../"
USER_CREDENTIALS_DIR=$(pwd)
ansible-playbook playbooks/delete_resources.yml -e "user_credentials_dir=${USER_CREDENTIALS_DIR}" "$@"
{
export ANSIBLE_INVENTORY_ENABLED=kubernetes.core.k8s,yaml
export ANSIBLE_PYTHON_INTERPRETER=auto_silent
ansible-playbook playbooks/play.yml -i playbooks/test.inventory_k8s.yml "$@"
ansible-playbook playbooks/play.yml -i playbooks/test.inventory_k8s.yml "$@" &&
ansible-playbook playbooks/create_resources.yml -e "user_credentials_dir=${USER_CREDENTIALS_DIR}" "$@" &&
ansible-inventory -i playbooks/test_inventory_aliases_with_ssl_k8s.yml --list "$@" &&
ansible-inventory -i playbooks/test_inventory_aliases_no_ssl_k8s.yml --list "$@" &&
unset ANSIBLE_INVENTORY_ENABLED &&
ansible-playbook playbooks/delete_resources.yml -e "user_credentials_dir=${USER_CREDENTIALS_DIR}" "$@"
} || {
ansible-playbook playbooks/delete_resources.yml -e "user_credentials_dir=${USER_CREDENTIALS_DIR}" "$@"
exit 1
}

View File

@@ -2,3 +2,6 @@
test_namespace:
- app-development-one
- app-development-two
- app-development-three
configmap_data: "This is a simple config map data."
configmap_name: "test-configmap"

View File

@@ -5,6 +5,8 @@
pre_test2: "{{ lookup('kubernetes.core.k8s', kind='Namespace', resource_name=test_namespace[0]) }}"
pre_test3: "{{ query('kubernetes.core.k8s', kind='Namespace', label_selector='namespace_label=app_development') }}"
pre_test4: "{{ query('kubernetes.core.k8s', kind='Namespace', resource_name=test_namespace[0]) }}"
cluster_version: "{{ query('kubernetes.core.k8s', cluster_info='version') }}"
cluster_api_groups: "{{ query('kubernetes.core.k8s', cluster_info='api_groups') }}"
# https://github.com/ansible-collections/kubernetes.core/issues/147
- name: Create a namespace with label
@@ -101,6 +103,130 @@
- test8 is mapping
- test9 is mapping
# test using resource_definition
- k8s:
name: "{{ test_namespace[2] }}"
kind: Namespace
- set_fact:
configmap_def:
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ configmap_name }}"
namespace: "{{ test_namespace[2] }}"
data:
value: "{{ configmap_data }}"
- name: Create simple configmap
k8s:
definition: "{{ configmap_def }}"
- name: Retrieve configmap using resource_definition parameter
set_fact:
result_configmap: "{{ lookup('kubernetes.core.k8s', resource_definition=configmap_def) }}"
- name: Validate configmap result
assert:
that:
- result_configmap.apiVersion == 'v1'
- result_configmap.metadata.name == "{{ configmap_name }}"
- result_configmap.metadata.namespace == "{{ test_namespace[2] }}"
- result_configmap.data.value == "{{ configmap_data }}"
# test lookup plugin using src parameter
- block:
- name: Create temporary file to store content
tempfile:
suffix: ".yaml"
register: tmpfile
- name: Copy content into file
copy:
content: |
kind: ConfigMap
apiVersion: v1
metadata:
name: "{{ configmap_name }}"
namespace: "{{ test_namespace[2] }}"
dest: "{{ tmpfile.path }}"
- name: Retrieve configmap using src parameter
set_fact:
src_configmap: "{{ lookup('kubernetes.core.k8s', src=tmpfile.path) }}"
- name: Validate configmap result
assert:
that:
- src_configmap.apiVersion == 'v1'
- src_configmap.metadata.name == "{{ configmap_name }}"
- src_configmap.metadata.namespace == "{{ test_namespace[2] }}"
- src_configmap.data.value == "{{ configmap_data }}"
always:
- name: Delete temporary file created
file:
state: absent
path: "{{ tmpfile.path }}"
ignore_errors: true
# test using aliases for user authentication
- block:
- name: Create temporary directory to save user credentials
tempfile:
state: directory
suffix: ".config"
register: tmpdir
- include_role:
name: setup_kubeconfig
vars:
user_credentials_dir: "{{ tmpdir.path }}"
kubeconfig_operation: "save"
- set_fact:
cluster_host: "{{ lookup('file', tmpdir.path + '/host_data.txt') }}"
user_cert_file: "{{ tmpdir.path }}/cert_file_data.txt"
user_key_file: "{{ tmpdir.path }}/key_file_data.txt"
ssl_ca_cert: "{{ tmpdir.path }}/ssl_ca_cert_data.txt"
- name: Retrieve configmap using authentication aliases (validate_certs=false)
set_fact:
configmap_no_ssl: "{{ lookup('kubernetes.core.k8s', host=cluster_host, cert_file=user_cert_file, key_file=user_key_file, verify_ssl=false, resource_definition=configmap_def) }}"
- name: Validate configmap result
assert:
that:
- configmap_no_ssl.apiVersion == 'v1'
- configmap_no_ssl.metadata.name == "{{ configmap_name }}"
- configmap_no_ssl.metadata.namespace == "{{ test_namespace[2] }}"
- configmap_no_ssl.data.value == "{{ configmap_data }}"
- name: Retrieve configmap using authentication aliases (validate_certs=true)
set_fact:
configmap_with_ssl: "{{ lookup('kubernetes.core.k8s', host=cluster_host, cert_file=user_cert_file, key_file=user_key_file, ssl_ca_cert=ssl_ca_cert, verify_ssl=true, resource_definition=configmap_def) }}"
- name: Validate configmap result
assert:
that:
- configmap_with_ssl.apiVersion == 'v1'
- configmap_with_ssl.metadata.name == "{{ configmap_name }}"
- configmap_with_ssl.metadata.namespace == "{{ test_namespace[2] }}"
- configmap_with_ssl.data.value == "{{ configmap_data }}"
always:
- name: Delete temporary directory
file:
state: absent
path: "{{ tmpdir.path }}"
ignore_errors: true
- include_role:
name: setup_kubeconfig
ignore_errors: true
vars:
kubeconfig_operation: revert
always:
- name: Ensure that namespace is removed
k8s:
@@ -110,4 +236,5 @@
with_items:
- one
- two
- three
ignore_errors: true

View File

@@ -0,0 +1 @@
disabled

View File

@@ -0,0 +1,6 @@
---
# When set to 'revert', the role will copy saved kubeconfig to the default location
# When set to 'save', the role will copy default kubeconfig to the custom location
kubeconfig_operation: "revert"
kubeconfig_default_path: "~/.kube/config"
kubeconfig_custom_path: "~/.kube/customconfig"

View File

@@ -0,0 +1,140 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2022, 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: test_inventory_read_credentials
short_description: Generate cert_file, key_file, host and server certificate
author:
- Aubin Bikouo (@abikouo)
description:
- This module is used for integration testing only for this collection
- The module load a kube_config file and generate parameters used to authenticate the client.
options:
kube_config:
description:
- Path to a valid kube config file to test.
type: path
required: yes
dest_dir:
description:
- Path to a directory where file will be generated.
type: path
required: yes
"""
EXAMPLES = r"""
- name: Generate authentication parameters for current context
test_inventory_read_credentials:
kube_config: ~/.kube/config
dest_dir: /tmp
"""
RETURN = """
auth:
description:
- User information used to authenticate to the cluster.
returned: always
type: complex
contains:
cert_file:
description:
- Path to the generated user certificate file.
type: str
key_file:
description:
- Path to the generated user key file.
type: str
ssl_ca_cert:
description:
- Path to the generated server certificate file.
type: str
host:
description:
- Path to the file containing cluster host.
type: str
"""
import os
import shutil
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
try:
from kubernetes import client, config
from kubernetes.dynamic import DynamicClient, LazyDiscoverer
HAS_KUBERNETES_MODULE = True
except ImportError:
HAS_KUBERNETES_MODULE = False
class K8SInventoryTestModule(AnsibleModule):
def __init__(self):
argument_spec = dict(
kube_config=dict(required=True, type="path"),
dest_dir=dict(required=True, type="path"),
)
super(K8SInventoryTestModule, self).__init__(argument_spec=argument_spec)
if not HAS_KUBERNETES_MODULE:
self.fail_json(msg=missing_required_lib("kubernetes"))
self.execute_module()
def execute_module(self):
dest_dir = os.path.abspath(self.params.get("dest_dir"))
kubeconfig_path = self.params.get("kube_config")
if not os.path.isdir(dest_dir):
self.fail_json(
msg="The following {0} does not exist or is not a directory.".format(
dest_dir
)
)
if not os.path.isfile(kubeconfig_path):
self.fail_json(
msg="The following {0} does not exist or is not a valid file.".format(
kubeconfig_path
)
)
client_config = type.__call__(client.Configuration)
config.load_kube_config(
config_file=kubeconfig_path, client_configuration=client_config
)
DynamicClient(client.ApiClient(client_config), discoverer=LazyDiscoverer)
result = dict(host=os.path.join(dest_dir, "host_data.txt"))
# create file containing host information
with open(result["host"], "w") as fd:
fd.write(client_config.host)
for key in ("cert_file", "key_file", "ssl_ca_cert"):
dest_file = os.path.join(dest_dir, "{0}_data.txt".format(key))
shutil.copyfile(getattr(client_config, key), dest_file)
result[key] = dest_file
self.exit_json(auth=result)
def main():
K8SInventoryTestModule()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,46 @@
---
- fail:
msg: "kubeconfig_operation must be one of 'revert' or 'save'"
when: kubeconfig_operation not in ["revert", "save"]
- set_fact:
src_kubeconfig: "{{ (kubeconfig_operation == 'save') | ternary(kubeconfig_default_path, kubeconfig_custom_path) }}"
dest_kubeconfig: "{{ (kubeconfig_operation == 'save') | ternary(kubeconfig_custom_path, kubeconfig_default_path) }}"
- name: check if source kubeconfig exists
stat:
path: "{{ src_kubeconfig }}"
register: _src
- name: check if destination kubeconfig exists
stat:
path: "{{ dest_kubeconfig }}"
register: _dest
- fail:
msg: "Both {{ src_kubeconfig }} and {{ dest_kubeconfig }} do not exist."
when:
- not _src.stat.exists
- not _dest.stat.exists
- name: Generate user cert_file, key_file, and hostname
block:
- name: Generate user credentials files
test_inventory_read_credentials:
kube_config: "{{ (_src.stat.exists) | ternary(src_kubeconfig, dest_kubeconfig) }}"
dest_dir: "{{ user_credentials_dir }}"
when: user_credentials_dir is defined
- block:
- name: "Copy {{ src_kubeconfig }} into {{ dest_kubeconfig }}"
copy:
remote_src: true
src: "{{ src_kubeconfig }}"
dest: "{{ dest_kubeconfig }}"
- name: "Delete {{ src_kubeconfig }}"
file:
state: absent
path: "{{ src_kubeconfig }}"
when: _src.stat.exists

View File

@@ -160,3 +160,25 @@ def test_load_kube_config_from_dict():
assert expected_configuration.items() <= actual_configuration.__dict__.items()
_remove_temp_file()
def test_create_auth_spec_with_aliases_in_kwargs():
auth_options = {
"host": TEST_HOST,
"cert_file": TEST_CLIENT_CERT,
"ssl_ca_cert": TEST_CERTIFICATE_AUTH,
"key_file": TEST_CLIENT_KEY,
"verify_ssl": True,
}
expected_auth_spec = {
"host": TEST_HOST,
"cert_file": TEST_CLIENT_CERT,
"ssl_ca_cert": TEST_CERTIFICATE_AUTH,
"key_file": TEST_CLIENT_KEY,
"verify_ssl": True,
}
actual_auth_spec = _create_auth_spec(module=None, **auth_options)
for key, value in expected_auth_spec.items():
assert value == actual_auth_spec.get(key)