From b69e783b212ea4b816376249961928fa88172d7d Mon Sep 17 00:00:00 2001 From: Fabian von Feilitzsch Date: Thu, 6 Feb 2020 16:05:56 -0500 Subject: [PATCH 1/6] Add module for retrieving pod logs --- plugins/modules/k8s_log.py | 225 +++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 plugins/modules/k8s_log.py diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py new file mode 100644 index 00000000..68d5f621 --- /dev/null +++ b/plugins/modules/k8s_log.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2019, Fabian von Feilitzsch <@fabianvf> +# 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +module: k8s_log + +short_description: Fetch logs from Kubernetes resources + +version_added: "2.8" + +author: + - "Fabian von Feilitzsch (@fabianvf)" + +description: + - Use the OpenShift Python client to perform read operations on K8s log endpoints. + - Authenticate using either a config file, certificates, password or token. + - Supports check mode. + - Analogous to `kubectl logs` or `oc logs` + +options: + api_version: + description: + - Use to specify the API version. in conjunction with I(kind), I(name), and I(namespace) to identify a + specific object. + - If using I(label_selector), cannot be overridden + default: v1 + aliases: + - api + - version + kind: + description: + - Use to specify an object model. Use in conjunction with I(api_version), I(name), and I(namespace) to identify a + specific object. + - If using I(label_selector), cannot be overridden + required: no + default: Pod + namespace: + description: + - Use to specify an object namespace. Use in conjunction with I(api_version), I(kind), and I(name) + to identify a specfic object. + name: + description: + - Use to specify an object name. Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a + specific object. + - Only one of I(name) or I(label_selector) may be provided + label_selectors: + description: + - List of label selectors to use to filter results + - Only one of I(name) or I(label_selector) may be provided + container: + description: + - Use to specify the container within a pod to grab the log from. + - If there is only one container, this will default to that container. + - If there is more than one container, this option is required. + required: no + +extends_documentation_fragment: + - k8s_auth_options + +requirements: + - "python >= 2.7" + - "openshift >= 0.6" + - "PyYAML >= 3.11" +''' + +EXAMPLES = ''' +- name: Get a log from a Pod + k8s_log: + name: example-1 + namespace: testing + register: log + +# This will get the log from the first Pod found matching the selector +- name: Log a Pod matching a label selector + k8s_log: + namespace: testing + label_selectors: + - app=example + register: log + +# This will get the log from a single Pod managed by this Deployment +- name: Get a log from a Deployment + k8s_log: + api_version: apps/v1 + kind: Deployment + namespace: testing + name: example + register: log + +# This will get the log from a single Pod managed by this DeploymentConfig +- name: Get a log from a DeploymentConfig + k8s_log: + api_version: apps.openshift.io/v1 + kind: DeploymentConfig + namespace: testing + name: example + register: log +''' + +RETURN = ''' +log: + type: str + description: + - The text log of the object + returned: success +''' + + +from ansible.module_utils.k8s.common import KubernetesAnsibleModule, AUTH_ARG_SPEC +import copy + + +class KubernetesLogModule(KubernetesAnsibleModule): + + def __init__(self, *args, **kwargs): + KubernetesAnsibleModule.__init__(self, *args, + supports_check_mode=True, + **kwargs) + + @property + def argspec(self): + args = copy.deepcopy(AUTH_ARG_SPEC) + args.update( + dict( + kind=dict(default='Pod'), + api_version=dict(default='v1', aliases=['api', 'version']), + name=dict(), + namespace=dict(), + container=dict(), + label_selectors=dict(type='list', default=[]), + ) + ) + return args + + def execute_module(self): + name = self.params.get('name') + label_selector = ','.join(self.params.get('label_selectors', {})) + if name and label_selector: + self.fail(msg='Only one of name or label_selectors can be provided') + + self.client = self.get_api_client() + resource = self.find_resource(self.params['kind'], self.params['api_version'], fail=True) + v1_pods = self.find_resource('Pod', 'v1', fail=True) + + if 'log' not in resource.subresources: + if not self.params.get('name'): + self.fail(msg='name must be provided for resources that do not support the log subresource') + instance = resource.get(name=self.params['name'], namespace=self.params.get('namespace')) + label_selector = ','.join(self.extract_selectors(instance)) + resource = v1_pods + + if label_selector: + instances = v1_pods.get(namespace=self.params['namespace'], label_selector=label_selector) + if not instances.items: + self.fail(msg='No pods in namespace {0} matched selector {1}'.format(self.params['namespace'], label_selector)) + # This matches the behavior of kubectl when logging pods via a selector + name = instances.items[0].metadata.name + resource = v1_pods + + kwargs = {} + if self.params.get('container'): + kwargs['query_params'] = dict(container=self.params['container']) + + self.exit_json(changed=False, log=resource.log.get( + name=name, + namespace=self.params.get('namespace'), + **kwargs + )) + + def extract_selectors(self, instance): + # Parses selectors on an object based on the specifications documented here: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors + selectors = [] + if not instance.spec.selector: + self.fail(msg='{0} {1} does not support the log subresource directly, and no Pod selector was found on the object'.format( + '/'.join(instance.group, instance.apiVersion), instance.kind)) + + if not (instance.spec.selector.matchLabels or instance.spec.selector.matchExpressions): + # A few resources (like DeploymentConfigs) just use a simple key:value style instead of supporting expressions + for k, v in dict(instance.spec.selector).items(): + selectors.append('{0}={1}'.format(k, v)) + return selectors + + if instance.spec.selector.matchLabels: + for k, v in dict(instance.spec.selector.matchLabels).items(): + selectors.append('{0}={1}'.format(k, v)) + + if instance.spec.selector.matchExpressions: + for expression in instance.spec.selector.matchExpressions: + operator = expression.operator + + if operator == 'Exists': + selectors.append(expression.key) + elif operator == 'DoesNotExist': + selectors.append('!{0}'.format(expression.key)) + elif operator in ['In', 'NotIn']: + selectors.append('{key} {operator} {values}'.format( + key=expression.key, + operator=operator.lower(), + values='({0})'.format(', '.join(expression.values)) + )) + else: + self.fail(msg='The k8s_log module does not support the {0} matchExpression operator'.format(operator.lower())) + + return selectors + + +def main(): + KubernetesLogModule().execute_module() + + +if __name__ == '__main__': + main() From 373b0fc93c45bac2972b6b40292b21d5b1f1cfb8 Mon Sep 17 00:00:00 2001 From: Fabian von Feilitzsch Date: Thu, 6 Feb 2020 17:07:30 -0500 Subject: [PATCH 2/6] Also provide log_lines --- plugins/modules/k8s_log.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index 68d5f621..bc5bb2c3 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -115,6 +115,11 @@ log: description: - The text log of the object returned: success +log_lines: + type: list + description: + - The log of the object, split on newlines + returned: success ''' @@ -173,11 +178,13 @@ class KubernetesLogModule(KubernetesAnsibleModule): if self.params.get('container'): kwargs['query_params'] = dict(container=self.params['container']) - self.exit_json(changed=False, log=resource.log.get( + log = resource.log.get( name=name, namespace=self.params.get('namespace'), **kwargs - )) + ) + + self.exit_json(changed=False, log=log, log_lines=log.split('\n')) def extract_selectors(self, instance): # Parses selectors on an object based on the specifications documented here: From 4a29ce08dec0dc320970c238045d1d83b7831992 Mon Sep 17 00:00:00 2001 From: Fabian von Feilitzsch Date: Mon, 17 Feb 2020 16:17:53 -0500 Subject: [PATCH 3/6] Add test --- molecule/default/playbook.yml | 1 + molecule/default/tasks/log.yml | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 molecule/default/tasks/log.yml diff --git a/molecule/default/playbook.yml b/molecule/default/playbook.yml index d8c04705..b4402410 100644 --- a/molecule/default/playbook.yml +++ b/molecule/default/playbook.yml @@ -26,3 +26,4 @@ - include_tasks: tasks/waiter.yml - include_tasks: tasks/full.yml - include_tasks: tasks/exec.yml + - include_tasks: tasks/log.yml diff --git a/molecule/default/tasks/log.yml b/molecule/default/tasks/log.yml new file mode 100644 index 00000000..ba9d80b9 --- /dev/null +++ b/molecule/default/tasks/log.yml @@ -0,0 +1,87 @@ +--- +- block: + - name: ensure that k8s-log namespace exists + k8s: + kind: Namespace + name: k8s-log + + - name: create hello-world deployment + k8s: + wait: yes + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: hello-world + namespace: k8s-log + spec: + selector: + matchLabels: + app: hello-world + template: + metadata: + labels: + app: hello-world + spec: + containers: + - image: busybox + name: hello-world + command: ['sh'] + args: ['-c', 'while true ; do echo "hello world" && sleep 10 ; done'] + restartPolicy: Always + + - name: retrieve the log by providing the deployment + community.kubernetes.k8s_log: + api_version: apps/v1 + kind: Deployment + namespace: k8s-log + name: hello-world + register: deployment_log + + - name: verify that the log can be retrieved via the deployment + assert: + that: + - "'hello world' in deployment_log.log" + - item == 'hello world' or item == '' + with_items: '{{ deployment_log.log_lines }}' + + - name: retrieve the log with a label selector + community.kubernetes.k8s_log: + namespace: k8s-log + label_selectors: + - 'app=hello-world' + register: label_selector_log + + - name: verify that the log can be retrieved via the label + assert: + that: + - "'hello world' in label_selector_log.log" + - item == 'hello world' or item == '' + with_items: '{{ label_selector_log.log_lines }}' + + - name: get the hello-world pod + k8s_info: + kind: Pod + namespace: k8s-log + label_selectors: + - 'app=hello-world' + register: k8s_log_pods + + - name: retrieve the log directly with the pod name + community.kubernetes.k8s_log: + namespace: k8s-log + name: '{{ k8s_log_pods.resources.0.metadata.name }}' + register: pod_log + + - name: verify that the log can be retrieved via the pod name + assert: + that: + - "'hello world' in pod_log.log" + - item == 'hello world' or item == '' + with_items: '{{ pod_log.log_lines }}' + always: + - name: ensure that namespace is removed + k8s: + kind: Namespace + name: k8s-log + state: absent From 75bf82d42c9c90df2141cbf6c7274686a704a7eb Mon Sep 17 00:00:00 2001 From: Fabian von Feilitzsch Date: Mon, 17 Feb 2020 16:42:32 -0500 Subject: [PATCH 4/6] Fix paths to properly use FQCN where necessary --- molecule/default/tasks/log.yml | 6 +++--- plugins/modules/k8s_log.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/molecule/default/tasks/log.yml b/molecule/default/tasks/log.yml index ba9d80b9..75322a37 100644 --- a/molecule/default/tasks/log.yml +++ b/molecule/default/tasks/log.yml @@ -31,7 +31,7 @@ restartPolicy: Always - name: retrieve the log by providing the deployment - community.kubernetes.k8s_log: + k8s_log: api_version: apps/v1 kind: Deployment namespace: k8s-log @@ -46,7 +46,7 @@ with_items: '{{ deployment_log.log_lines }}' - name: retrieve the log with a label selector - community.kubernetes.k8s_log: + k8s_log: namespace: k8s-log label_selectors: - 'app=hello-world' @@ -68,7 +68,7 @@ register: k8s_log_pods - name: retrieve the log directly with the pod name - community.kubernetes.k8s_log: + k8s_log: namespace: k8s-log name: '{{ k8s_log_pods.resources.0.metadata.name }}' register: pod_log diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index bc5bb2c3..1094e90e 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -67,7 +67,7 @@ options: required: no extends_documentation_fragment: - - k8s_auth_options + - community.kubernetes.k8s_auth_options requirements: - "python >= 2.7" @@ -123,8 +123,9 @@ log_lines: ''' -from ansible.module_utils.k8s.common import KubernetesAnsibleModule, AUTH_ARG_SPEC import copy +from ansible_collections.community.kubernetes.plugins.module_utils.common import KubernetesAnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC class KubernetesLogModule(KubernetesAnsibleModule): From 14bc933a2124a7d4f81d2b075f5c500f71cf511a Mon Sep 17 00:00:00 2001 From: Fabian von Feilitzsch Date: Tue, 18 Feb 2020 13:01:41 -0500 Subject: [PATCH 5/6] Add types to arguments --- plugins/modules/k8s_log.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index 1094e90e..3a4518ad 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -28,7 +28,8 @@ description: - Authenticate using either a config file, certificates, password or token. - Supports check mode. - Analogous to `kubectl logs` or `oc logs` - +extends_documentation_fragment: + - community.kubernetes.k8s_auth_options options: api_version: description: @@ -39,6 +40,7 @@ options: aliases: - api - version + type: str kind: description: - Use to specify an object model. Use in conjunction with I(api_version), I(name), and I(namespace) to identify a @@ -46,28 +48,31 @@ options: - If using I(label_selector), cannot be overridden required: no default: Pod + type: str namespace: description: - Use to specify an object namespace. Use in conjunction with I(api_version), I(kind), and I(name) to identify a specfic object. + type: str name: description: - Use to specify an object name. Use in conjunction with I(api_version), I(kind) and I(namespace) to identify a specific object. - Only one of I(name) or I(label_selector) may be provided + type: str label_selectors: description: - List of label selectors to use to filter results - Only one of I(name) or I(label_selector) may be provided + type: list + elements: str container: description: - Use to specify the container within a pod to grab the log from. - If there is only one container, this will default to that container. - If there is more than one container, this option is required. required: no - -extends_documentation_fragment: - - community.kubernetes.k8s_auth_options + type: str requirements: - "python >= 2.7" @@ -145,7 +150,7 @@ class KubernetesLogModule(KubernetesAnsibleModule): name=dict(), namespace=dict(), container=dict(), - label_selectors=dict(type='list', default=[]), + label_selectors=dict(type='list', elements='str', default=[]), ) ) return args From 875c5dfafea325d20cabbb694c255732af9d5497 Mon Sep 17 00:00:00 2001 From: Fabian von Feilitzsch Date: Tue, 18 Feb 2020 15:20:30 -0500 Subject: [PATCH 6/6] Update version --- plugins/modules/k8s_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index 3a4518ad..d836cf23 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -18,7 +18,7 @@ module: k8s_log short_description: Fetch logs from Kubernetes resources -version_added: "2.8" +version_added: "1.0.0" author: - "Fabian von Feilitzsch (@fabianvf)"