diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index e4751307..ab9ce8c2 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -28,6 +28,7 @@ - include_tasks: tasks/exec.yml - include_tasks: tasks/log.yml - include_tasks: tasks/info.yml + - include_tasks: tasks/template.yml roles: - helm diff --git a/molecule/default/tasks/template.yml b/molecule/default/tasks/template.yml new file mode 100644 index 00000000..ea9344b3 --- /dev/null +++ b/molecule/default/tasks/template.yml @@ -0,0 +1,155 @@ +--- +- block: + - set_fact: + template_namespace: template-test + + - name: Ensure namespace exists + k8s: + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ template_namespace }}" + + - name: Check if k8s_service does not inherit parameter + community.kubernetes.k8s_service: + template: "pod_template_one.j2" + state: present + ignore_errors: yes + register: r + + - name: Check for expected failures in last tasks + assert: + that: + - r.failed + - "'is only supported parameter for' in r.msg" + + - name: Specify both definition and template + community.kubernetes.k8s: + state: present + template: "pod_template_one.j2" + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: apply-deploy + namespace: "{{ template_namespace }}" + spec: + replicas: 1 + selector: + matchLabels: + app: "{{ k8s_pod_name }}" + vars: + k8s_pod_name: pod + k8s_pod_namespace: "{{ template_namespace }}" + register: r + ignore_errors: yes + + - name: Check if definition and template are mutually exclusive + assert: + that: + - r.failed + - "'parameters are mutually exclusive' in r.msg" + + - name: Specify both src and template + community.kubernetes.k8s: + state: present + src: "../templates/pod_template_one.j2" + template: "pod_template_one.j2" + vars: + k8s_pod_name: pod + k8s_pod_namespace: "{{ template_namespace }}" + register: r + ignore_errors: yes + + - name: Check if src and template are mutually exclusive + assert: + that: + - r.failed + - "'parameters are mutually exclusive' in r.msg" + + - name: Create pod using template (direct specification) + community.kubernetes.k8s: + template: "pod_template_one.j2" + wait: yes + vars: + k8s_pod_name: pod-1 + k8s_pod_namespace: "{{ template_namespace }}" + register: r + + - name: Assert that pod creation succeeded using template + assert: + that: + - r is successful + + - name: Create pod using template with wrong parameter + community.kubernetes.k8s: + template: + - default + wait: yes + vars: + k8s_pod_name: pod-2 + k8s_pod_namespace: "{{ template_namespace }}" + register: r + ignore_errors: True + + - name: Assert that pod creation failed using template due to wrong parameter + assert: + that: + - r is failed + - "'Error while reading template file' in r.msg" + + - name: Create pod using template (path parameter) + community.kubernetes.k8s: + template: + path: "pod_template_one.j2" + wait: yes + vars: + k8s_pod_name: pod-3 + k8s_pod_namespace: "{{ template_namespace }}" + register: r + + - name: Assert that pod creation succeeded using template + assert: + that: + - r is successful + + - name: Create pod using template (different variable string) + community.kubernetes.k8s: + template: + path: "pod_template_two.j2" + variable_start_string: '[[' + variable_end_string: ']]' + wait: yes + vars: + k8s_pod_name: pod-4 + k8s_pod_namespace: "[[ template_namespace ]]" + ansible_python_interpreter: "[[ ansible_playbook_python ]]" + register: r + + - name: Assert that pod creation succeeded using template + assert: + that: + - r is successful + + - name: Remove Pod (Cleanup) + k8s: + api_version: v1 + kind: Pod + name: "{{ item }}" + namespace: "{{ template_namespace }}" + state: absent + wait: yes + ignore_errors: yes + with_items: + - pod-1 + - pod-2 + - pod-3 + - pod-4 + + always: + - name: Remove namespace (Cleanup) + k8s: + kind: Namespace + name: "{{ template_namespace }}" + state: absent diff --git a/molecule/default/templates/pod_template_one.j2 b/molecule/default/templates/pod_template_one.j2 new file mode 100644 index 00000000..bafb7d9f --- /dev/null +++ b/molecule/default/templates/pod_template_one.j2 @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + app: "{{ k8s_pod_name }}" + name: '{{ k8s_pod_name }}' + namespace: '{{ k8s_pod_namespace }}' +spec: + containers: + - args: + - /bin/sh + - -c + - while true; do echo $(date); sleep 10; done + image: python:3.7-alpine + imagePullPolicy: Always + name: '{{ k8s_pod_name }}' diff --git a/molecule/default/templates/pod_template_two.j2 b/molecule/default/templates/pod_template_two.j2 new file mode 100644 index 00000000..cef89bf1 --- /dev/null +++ b/molecule/default/templates/pod_template_two.j2 @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + app: '[[ k8s_pod_name ]]' + name: '[[ k8s_pod_name ]]' + namespace: '[[ k8s_pod_namespace ]]' +spec: + containers: + - args: + - /bin/sh + - -c + - while true; do echo $(date); sleep 10; done + image: python:3.7-alpine + imagePullPolicy: Always + name: '[[ k8s_pod_name ]]' diff --git a/plugins/action/k8s_info.py b/plugins/action/k8s_info.py index 2b9b84ad..6b26225f 100644 --- a/plugins/action/k8s_info.py +++ b/plugins/action/k8s_info.py @@ -9,14 +9,18 @@ __metaclass__ = type import copy import traceback -from ansible.module_utils._text import to_text +from ansible.config.manager import ensure_type +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 +from ansible.module_utils._text import to_text, to_bytes, to_native from ansible.plugins.action import ActionBase -from ansible.errors import AnsibleError class ActionModule(ActionBase): TRANSFERS_FILES = True + DEFAULT_NEWLINE_SEQUENCE = "\n" def _ensure_invocation(self, result): # NOTE: adding invocation arguments here needs to be kept in sync with @@ -71,6 +75,118 @@ class ActionModule(ActionBase): if src: new_module_args['src'] = src + template = self._task.args.get('template', None) + if template: + # template is only supported by k8s module. + if self._task.action not in ('k8s', 'community.kubernetes.k8s', 'community.okd.k8s'): + raise AnsibleActionFail("'template' is only supported parameter for 'k8s' module.") + if isinstance(template, string_types): + # treat this as raw_params + template_path = template + newline_sequence = self.DEFAULT_NEWLINE_SEQUENCE + variable_start_string = None + variable_end_string = None + block_start_string = None + block_end_string = None + trim_blocks = True + lstrip_blocks = False + elif isinstance(template, dict): + template_args = template + template_path = template_args.get('path', None) + if not template: + raise AnsibleActionFail("Please specify path for template.") + + # Options type validation strings + 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') + 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))) + try: + 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)) + + 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)) + try: + source = self._find_needle('templates', template_path) + except AnsibleError as e: + raise AnsibleActionFail(to_text(e)) + + # Option `lstrip_blocks' was added in Jinja2 version 2.7. + if lstrip_blocks: + try: + import jinja2.defaults + except ImportError: + 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") + + wrong_sequences = ["\\n", "\\r", "\\r\\n"] + allowed_sequences = ["\n", "\r", "\r\n"] + + # We need to convert unescaped sequences to proper escaped sequences for Jinja2 + if newline_sequence in wrong_sequences: + 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") + + # Get vault decrypted tmp file + 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') + + # template the source data locally & get ready to transfer + try: + with open(b_tmp_source, 'rb') as f: + try: + template_data = to_text(f.read(), errors='surrogate_or_strict') + except UnicodeError: + raise AnsibleActionFail("Template source files must be utf-8 encoded") + + # add ansible 'template' vars + temp_vars = task_vars.copy() + old_vars = self._templar.available_variables + + self._templar.environment.newline_sequence = newline_sequence + if block_start_string is not None: + self._templar.environment.block_start_string = block_start_string + if block_end_string is not None: + self._templar.environment.block_end_string = block_end_string + if variable_start_string is not None: + self._templar.environment.variable_start_string = variable_start_string + if variable_end_string is not None: + self._templar.environment.variable_end_string = variable_end_string + self._templar.environment.trim_blocks = trim_blocks + self._templar.environment.lstrip_blocks = lstrip_blocks + self._templar.available_variables = temp_vars + resultant = self._templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False) + self._templar.available_variables = old_vars + resource_definition = self._task.args.get('definition', None) + if not resource_definition: + new_module_args.pop('template') + new_module_args['definition'] = resultant + except AnsibleAction: + raise + except Exception as e: + raise AnsibleActionFail("%s: %s" % (type(e).__name__, to_text(e))) + finally: + self._loader.cleanup_tmp_file(b_tmp_source) + # Execute the k8s_* module. module_return = self._execute_module(module_name=self._task.action, module_args=new_module_args, task_vars=task_vars) diff --git a/plugins/doc_fragments/k8s_resource_options.py b/plugins/doc_fragments/k8s_resource_options.py index b5721453..b9dcfe16 100644 --- a/plugins/doc_fragments/k8s_resource_options.py +++ b/plugins/doc_fragments/k8s_resource_options.py @@ -28,5 +28,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. type: path ''' diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index df7dd2a8..0c57c3e1 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -96,6 +96,30 @@ options: - Requires openshift >= 0.9.2 - mutually exclusive with C(merge_type) type: bool + template: + description: + - Provide a valid YAML template definition file for an object when creating or updating. + - Value can be provided as string or dictionary. + - Mutually exclusive with C(src) and C(resource_definition). + - Template files needs to be present on the Ansible Controller's file system. + - Additional parameters can be specified using dictionary. + - 'Valid additional parameters - ' + - 'C(newline_sequence) (str): Specify the newline sequence to use for templating files. + valid choices are "\n", "\r", "\r\n". Default value "\n".' + - 'C(block_start_string) (str): The string marking the beginning of a block. + Default value "{%".' + - 'C(block_end_string) (str): The string marking the end of a block. + Default value "%}".' + - 'C(variable_start_string) (str): The string marking the beginning of a print statement. + Default value "{{".' + - 'C(variable_end_string) (str): The string marking the end of a print statement. + Default value "}}".' + - 'C(trim_blocks) (bool): Determine when newlines should be removed from blocks. When set to C(yes) the first newline + after a block is removed (block, not variable tag!). Default value is true.' + - 'C(lstrip_blocks) (bool): Determine when leading spaces and tabs should be stripped. + When set to C(yes) leading spaces and tabs are stripped from the start of a line to a block. + This functionality requires Jinja 2.7 or newer. Default value is false.' + type: raw requirements: - "python >= 2.7" @@ -160,6 +184,19 @@ EXAMPLES = r''' state: present definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}" +- name: Read definition template file from the Ansible controller file system + community.kubernetes.k8s: + state: present + template: '/testing/deployment.j2' + +- name: Read definition template file from the Ansible controller file system + community.kubernetes.k8s: + state: present + template: + path: '/testing/deployment.j2' + variable_start_string: '[[' + variable_end_string: ']]' + - name: fail on validation errors community.kubernetes.k8s: state: present @@ -242,12 +279,15 @@ class KubernetesModule(K8sAnsibleMixin): argument_spec['validate'] = dict(type='dict', default=None, options=self.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) return argument_spec def __init__(self, *args, k8s_kind=None, **kwargs): mutually_exclusive = [ ('resource_definition', 'src'), ('merge_type', 'apply'), + ('template', 'resource_definition'), + ('template', 'src'), ] module = AnsibleModule(