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 00000000000..771479e37e9 --- /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 694357c99b9..032220b2896 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 00000000000..313747819b2 --- /dev/null +++ b/molecule/default/tasks/generate_name.yml @@ -0,0 +1,188 @@ +- 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 + - "result.msg == 'At least one of metadata.name|metadata.generateName is required to create object.'" + + - 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 }}' + definition: + apiVersion: v1 + kind: Pod + metadata: + name: pod2 + 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 37884589666..2ce06d9cf20 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -612,17 +612,38 @@ def build_error_msg(kind, name, msg): self.remove_aliases() + def _test_metadata(definition, key="name"): + return key in definition['metadata'] and definition['metadata'].get(key) + 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 self.generate_name or _test_metadata(definition, key='generateName'): + gen_name = self.generate_name or definition.get('metadata', {}).get('generateName') + definition['metadata']['generateName'] = '%s-%s' % (gen_name, generate_hash(definition)) + params = {} + required_fields = False + if _test_metadata(definition): + required_fields = True + params['name'] = name if namespace: params['namespace'] = namespace if label_selectors: + required_fields = True params['label_selector'] = ','.join(label_selectors) - existing = resource.get(**params) + + if required_fields: + 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: @@ -761,6 +782,15 @@ def _empty_resource_list(): k8s_obj = _encode_stringdata(definition) else: try: + if not _test_metadata(definition, key="name") and not _test_metadata(definition, key="generateName") and self.generate_name is not None: + definition['metadata'].update({'generateName': self.generate_name}) + if not _test_metadata(definition, key="name") and not _test_metadata(definition, key="generateName"): + msg = "At least one of metadata.name|metadata.generateName is required to create object." + if continue_on_error: + result['error'] = dict(msg=msg) + return result + else: + self.fail_json(msg=msg) k8s_obj = resource.create(definition, namespace=namespace).to_dict() except ConflictError: # Some resources, like ProjectRequests, can't be created multiple times, @@ -791,6 +821,7 @@ def _empty_resource_list(): 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 5edbc6d94b5..6af6bc1be6f 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 e7bbd7e46ec..ced9c016cce 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -142,6 +142,16 @@ 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 @@ 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 (