#!/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 DOCUMENTATION = r""" module: k8s_log short_description: Fetch logs from Kubernetes resources version_added: "0.10.0" author: - "Fabian von Feilitzsch (@fabianvf)" description: - Use the Kubernetes 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` extends_documentation_fragment: - kubernetes.core.k8s_auth_options - kubernetes.core.k8s_name_options options: 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_selectors), cannot be overridden. type: str default: Pod 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_selectors) 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_selectors) 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 type: str since_seconds: description: - A relative time in seconds before the current time from which to show logs. required: no type: str version_added: '2.2.0' previous: description: - If C(true), print the logs for the previous instance of the container in a pod if it exists. required: no type: bool default: False version_added: '2.4.0' requirements: - "python >= 3.6" - "kubernetes >= 12.0.0" - "PyYAML >= 3.11" """ EXAMPLES = r""" - name: Get a log from a Pod kubernetes.core.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 kubernetes.core.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 kubernetes.core.k8s_log: api_version: apps/v1 kind: Deployment namespace: testing name: example since_seconds: "4000" register: log # This will get the log from a single Pod managed by this DeploymentConfig - name: Get a log from a DeploymentConfig kubernetes.core.k8s_log: api_version: apps.openshift.io/v1 kind: DeploymentConfig namespace: testing name: example register: log """ RETURN = r""" log: type: str description: - The text log of the object returned: success log_lines: type: list description: - The log of the object, split on newlines returned: success """ import copy from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( AnsibleModule, ) from ansible.module_utils.six import PY2 from ansible_collections.kubernetes.core.plugins.module_utils.args_common import ( AUTH_ARG_SPEC, NAME_ARG_SPEC, ) def argspec(): args = copy.deepcopy(AUTH_ARG_SPEC) args.update(NAME_ARG_SPEC) args.update( dict( kind=dict(type="str", default="Pod"), container=dict(), since_seconds=dict(), label_selectors=dict(type="list", elements="str", default=[]), previous=dict(type="bool", default=False), ) ) return args def execute_module(module, k8s_ansible_mixin): name = module.params.get("name") namespace = module.params.get("namespace") label_selector = ",".join(module.params.get("label_selectors", {})) if name and label_selector: module.fail(msg="Only one of name or label_selectors can be provided") resource = k8s_ansible_mixin.find_resource( module.params["kind"], module.params["api_version"], fail=True ) v1_pods = k8s_ansible_mixin.find_resource("Pod", "v1", fail=True) if "log" not in resource.subresources: if not name: module.fail( msg="name must be provided for resources that do not support the log subresource" ) instance = resource.get(name=name, namespace=namespace) label_selector = ",".join(extract_selectors(module, instance)) resource = v1_pods if label_selector: instances = v1_pods.get(namespace=namespace, label_selector=label_selector) if not instances.items: module.fail( msg="No pods in namespace {0} matched selector {1}".format( 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 module.params.get("container"): kwargs["query_params"] = dict(container=module.params["container"]) if module.params.get("since_seconds"): kwargs.setdefault("query_params", {}).update( {"sinceSeconds": module.params["since_seconds"]} ) if module.params.get("previous"): kwargs.setdefault("query_params", {}).update( {"previous": module.params["previous"]} ) log = serialize_log( resource.log.get(name=name, namespace=namespace, serialize=False, **kwargs) ) module.exit_json(changed=False, log=log, log_lines=log.split("\n")) def extract_selectors(module, 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: module.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: module.fail( msg="The k8s_log module does not support the {0} matchExpression operator".format( operator.lower() ) ) return selectors def serialize_log(response): if PY2: return response.data return response.data.decode("utf8") def main(): module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) from ansible_collections.kubernetes.core.plugins.module_utils.common import ( K8sAnsibleMixin, get_api_client, ) k8s_ansible_mixin = K8sAnsibleMixin(module) k8s_ansible_mixin.client = get_api_client(module=module) execute_module(module, k8s_ansible_mixin) if __name__ == "__main__": main()