From c65512357dd90b477dec683999128e0308a96f88 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Thu, 30 Sep 2021 16:58:50 +0200 Subject: [PATCH] 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 Reviewed-by: None Reviewed-by: None --- .../238-k8s-add-support-for-generate_name.yml | 2 + molecule/default/converge.yml | 8 + molecule/default/tasks/generate_name.yml | 187 ++++++++++++++++++ plugins/module_utils/common.py | 31 ++- plugins/module_utils/hashes.py | 17 +- plugins/modules/k8s.py | 27 +++ 6 files changed, 261 insertions(+), 11 deletions(-) create mode 100644 changelogs/fragments/238-k8s-add-support-for-generate_name.yml create mode 100644 molecule/default/tasks/generate_name.yml diff --git a/changelogs/fragments/238-k8s-add-support-for-generate_name.yml b/changelogs/fragments/238-k8s-add-support-for-generate_name.yml new file mode 100644 index 00000000..771479e3 --- /dev/null +++ b/changelogs/fragments/238-k8s-add-support-for-generate_name.yml @@ -0,0 +1,2 @@ +minor_changes: + - k8s - allow resource definition using metadata.generateName (https://github.com/ansible-collections/kubernetes.core/issues/35). diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index 694357c9..032220b2 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -197,6 +197,14 @@ tags: - always + - name: Include generate_name.yml + include_tasks: + file: tasks/generate_name.yml + apply: + tags: [ generate_name, k8s ] + tags: + - always + roles: - role: helm tags: diff --git a/molecule/default/tasks/generate_name.yml b/molecule/default/tasks/generate_name.yml new file mode 100644 index 00000000..012d46ef --- /dev/null +++ b/molecule/default/tasks/generate_name.yml @@ -0,0 +1,187 @@ +- block: + - set_fact: + pod_00: + apiVersion: v1 + kind: Pod + spec: + containers: + - name: py-container + image: python:3.7-alpine + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + pod_01: + apiVersion: v1 + kind: Pod + metadata: + generateName: pod- + spec: + containers: + - args: + - /bin/sh + - -c + - while true; do echo $(date); sleep 10; done + image: python:3.7-alpine + imagePullPolicy: IfNotPresent + name: py-container + + - name: Create namespace using generateName + k8s: + definition: + kind: Namespace + metadata: + generateName: "test-" + labels: + ansible: test + register: result + + - set_fact: + namespace: "{{ result.result.metadata.name }}" + + - name: Create Pod without name + k8s: + namespace: "{{ namespace }}" + definition: "{{ pod_00 }}" + register: result + ignore_errors: true + + - name: assert pod creation failed + assert: + that: + - result is failed + - "'name or generateName is required' in result.msg" + + - name: create pod using name parameter should succeed + k8s: + namespace: "{{ namespace }}" + definition: "{{ pod_00 }}" + name: pod-01 + + - name: list Pod for namespace + k8s_info: + kind: Pod + namespace: "{{ namespace }}" + register: pods + + - name: assert pod has been created + assert: + that: + - "{{ pods.resources | length == 1 }}" + + - name: create pod using generate_name parameter should succeed + k8s: + namespace: "{{ namespace }}" + definition: "{{ pod_00 }}" + generate_name: pod- + + - name: list Pod for namespace + k8s_info: + kind: Pod + namespace: "{{ namespace }}" + register: pods + + - name: assert pod has been created + assert: + that: + - "{{ pods.resources | length == 2 }}" + + - name: create pod using metadata.generateName parameter should succeed + k8s: + namespace: "{{ namespace }}" + definition: "{{ pod_01 }}" + + - name: list Pod for namespace + k8s_info: + kind: Pod + namespace: "{{ namespace }}" + register: pods + + - name: assert pod has been created + assert: + that: + - "{{ pods.resources | length == 3 }}" + + - name: create object using metadata.generateName should support wait option + k8s: + namespace: "{{ namespace }}" + definition: + apiVersion: apps/v1 + kind: StatefulSet + metadata: + generateName: test- + spec: + selector: + matchLabels: + app: nginx + serviceName: "nginx" + replicas: 3 + template: + metadata: + labels: + app: nginx + spec: + terminationGracePeriodSeconds: 10 + containers: + - name: nginx + image: k8s.gcr.io/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + wait: yes + wait_sleep: 3 + wait_timeout: 180 + + - name: Create ConfigMap using generateName + kubernetes.core.k8s: + kind: ConfigMap + namespace: "{{ namespace }}" + generate_name: cmap- + append_hash: yes + register: config + + - name: assert that configmap has been created using generateName + assert: + that: + - "config.result.metadata.name.startswith('cmap-')" + + - name: Create Pod with failing container + kubernetes.core.k8s: + namespace: "{{ namespace }}" + definition: + apiVersion: v1 + kind: Pod + metadata: + name: pod1 + spec: + containers: + - image: adslfkjadslfkjadslkfjsadf + name: non-existent-container-image + + - name: Create second Pod using wait (it should not wait for the first container) + kubernetes.core.k8s: + namespace: "{{ namespace }}" + generate_name: "pod2-" + definition: + apiVersion: v1 + kind: Pod + spec: + containers: + - args: + - /bin/sh + - -c + - while true; do echo $(date); sleep 10; done + image: python:3.7-alpine + imagePullPolicy: Always + name: c0 + wait: yes + wait_timeout: 10 + + always: + - name: Delete namespace + k8s: + kind: Namespace + name: "{{ namespace }}" + state: absent + ignore_errors: true diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 37884589..2ace5779 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -580,8 +580,11 @@ class K8sAnsibleMixin(object): definition['kind'] = resource.kind definition['apiVersion'] = resource.group_version metadata = definition.get('metadata', {}) - if self.name and not metadata.get('name'): - metadata['name'] = self.name + if not metadata.get('name') and not metadata.get('generateName'): + if self.name: + metadata['name'] = self.name + elif self.generate_name: + metadata['generateName'] = self.generate_name if resource.namespaced and self.namespace and not metadata.get('namespace'): metadata['namespace'] = self.namespace definition['metadata'] = metadata @@ -595,6 +598,7 @@ class K8sAnsibleMixin(object): state = self.params.get('state', None) force = self.params.get('force', False) name = definition['metadata'].get('name') + generate_name = definition['metadata'].get('generateName') origin_name = definition['metadata'].get('name') namespace = definition['metadata'].get('namespace') existing = None @@ -615,14 +619,28 @@ class K8sAnsibleMixin(object): try: # ignore append_hash for resources other than ConfigMap and Secret if append_hash and definition['kind'] in ['ConfigMap', 'Secret']: - name = '%s-%s' % (name, generate_hash(definition)) - definition['metadata']['name'] = name - params = dict(name=name) + if name: + name = '%s-%s' % (name, generate_hash(definition)) + definition['metadata']['name'] = name + elif generate_name: + definition['metadata']['generateName'] = '%s-%s' % (generate_name, generate_hash(definition)) + params = {} + if name: + params['name'] = name if namespace: params['namespace'] = namespace if label_selectors: params['label_selector'] = ','.join(label_selectors) - existing = resource.get(**params) + + if "name" in params or "label_selector" in params: + existing = resource.get(**params) + elif state == 'absent': + msg = "At least one of name|label_selectors is required to delete object." + if continue_on_error: + result['error'] = dict(msg=msg) + return result + else: + self.fail_json(msg=msg) except (NotFoundError, MethodNotAllowedError): # Remove traceback so that it doesn't show up in later failures try: @@ -791,6 +809,7 @@ class K8sAnsibleMixin(object): success = True result['result'] = k8s_obj if wait and not self.check_mode: + definition['metadata'].update({'name': k8s_obj['metadata']['name']}) success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) result['changed'] = True result['method'] = 'create' diff --git a/plugins/module_utils/hashes.py b/plugins/module_utils/hashes.py index 5edbc6d9..6af6bc1b 100644 --- a/plugins/module_utils/hashes.py +++ b/plugins/module_utils/hashes.py @@ -44,14 +44,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']) + 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']) + marshalled = marshal(sorted_dict(resource), ['data', 'kind', key, 'type']) + del(resource[key]) return encode(marshalled) raise NotImplementedError diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index e7bbd7e4..ced9c016 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -142,6 +142,16 @@ 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 requirements: - "python >= 3.6" @@ -278,6 +288,20 @@ EXAMPLES = r''' metadata: 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 ''' RETURN = r''' @@ -352,6 +376,7 @@ def argspec(): 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() return argument_spec @@ -370,6 +395,7 @@ def execute_module(module, k8s_ansible_mixin): 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() @@ -383,6 +409,7 @@ def main(): ('merge_type', 'apply'), ('template', 'resource_definition'), ('template', 'src'), + ('name', 'generate_name'), ] module = AnsibleModule(argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True) from ansible_collections.kubernetes.core.plugins.module_utils.common import (