Skip to content

Commit

Permalink
k8s - add support for generateName for resource definition
Browse files Browse the repository at this point in the history
  • Loading branch information
abikouo committed Sep 24, 2021
1 parent d01e4a6 commit 2ffe813
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- k8s - allow resource definition using metadata.generateName (https://github.com/ansible-collections/kubernetes.core/issues/35).
8 changes: 8 additions & 0 deletions molecule/default/converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
188 changes: 188 additions & 0 deletions molecule/default/tasks/generate_name.yml
Original file line number Diff line number Diff line change
@@ -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
39 changes: 35 additions & 4 deletions plugins/module_utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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'
Expand Down
17 changes: 12 additions & 5 deletions plugins/module_utils/hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions plugins/modules/k8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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'''
Expand Down Expand Up @@ -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

Expand All @@ -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()
Expand All @@ -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 (
Expand Down

0 comments on commit 2ffe813

Please sign in to comment.