diff --git a/changelogs/fragments/528-k8s_log-support-all_containers-options.yml b/changelogs/fragments/528-k8s_log-support-all_containers-options.yml new file mode 100644 index 00000000..a188278c --- /dev/null +++ b/changelogs/fragments/528-k8s_log-support-all_containers-options.yml @@ -0,0 +1,5 @@ +--- +bugfixes: + - k8s_log - fix exception raised when the name is not provided for resources requiring. (https://github.com/ansible-collections/kubernetes.core/issues/514) +minor_changes: + - k8s_log - add the ``all_containers`` for retrieving all containers' logs in the pod(s). diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index 41f4d49f..e52d5bce 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -51,7 +51,8 @@ options: 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. + - If there is more than one container, this option is required or set I(all_containers) to C(true). + - mutually exclusive with C(all_containers). required: no type: str since_seconds: @@ -73,6 +74,12 @@ options: required: no type: int version_added: '2.4.0' + all_containers: + description: + - If set to C(true), retrieve all containers' logs in the pod(s). + - mutually exclusive with C(container). + type: bool + version_added: '2.4.0' requirements: - "python >= 3.6" @@ -114,6 +121,13 @@ EXAMPLES = r""" name: example tail_lines: 100 register: log + +# This will get the logs from all containers in Pod +- name: Get the logs from all containers in pod + kubernetes.core.k8s_log: + namespace: testing + name: some-pod + all_containers: true """ RETURN = r""" @@ -131,6 +145,7 @@ log_lines: import copy +import json from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( AnsibleModule, @@ -170,11 +185,35 @@ def argspec(): label_selectors=dict(type="list", elements="str", default=[]), previous=dict(type="bool", default=False), tail_lines=dict(type="int"), + all_containers=dict(type="bool"), ) ) return args +def get_exception_message(exc): + try: + d = json.loads(exc.body.decode("utf8")) + return d["message"] + except Exception: + return exc + + +def list_containers_in_pod(svc, resource, namespace, name): + try: + result = svc.client.get(resource, name=name, namespace=namespace) + containers = [ + c["name"] for c in result.to_dict()["status"]["containerStatuses"] + ] + return containers + except Exception as exc: + raise CoreException( + "Unable to retrieve log from Pod due to: {0}".format( + get_exception_message(exc) + ) + ) + + def execute_module(svc, params): name = params.get("name") namespace = params.get("namespace") @@ -206,6 +245,11 @@ def execute_module(svc, params): name = instances.items[0].metadata.name resource = v1_pods + if "base" not in resource.log.urls and not name: + raise CoreException( + "name must be provided for resources that do not support namespaced base url" + ) + kwargs = {} if params.get("container"): kwargs["query_params"] = {"container": params["container"]} @@ -223,19 +267,28 @@ def execute_module(svc, params): {"tailLines": params["tail_lines"]} ) + pod_containers = [None] + if params.get("all_containers"): + pod_containers = list_containers_in_pod(svc, resource, namespace, name) + + log = "" try: - response = resource.log.get( - name=name, namespace=namespace, serialize=False, **kwargs - ) + for container in pod_containers: + if container is not None: + kwargs.setdefault("query_params", {}).update({"container": container}) + response = resource.log.get( + name=name, namespace=namespace, serialize=False, **kwargs + ) + log += response.data.decode("utf8") except ApiException as exc: if exc.reason == "Not Found": raise CoreException("Pod {0}/{1} not found.".format(namespace, name)) raise CoreException( - "Unable to retrieve log from Pod due to: {0}".format(exc.reason) + "Unable to retrieve log from Pod due to: {0}".format( + get_exception_message(exc) + ) ) - log = response.data.decode("utf8") - return {"changed": False, "log": log, "log_lines": log.split("\n")} @@ -290,7 +343,10 @@ def extract_selectors(instance): def main(): module = AnsibleK8SModule( - module_class=AnsibleModule, argument_spec=argspec(), supports_check_mode=True + module_class=AnsibleModule, + argument_spec=argspec(), + supports_check_mode=True, + mutually_exclusive=[("container", "all_containers")], ) try: diff --git a/tests/integration/targets/k8s_log/tasks/main.yml b/tests/integration/targets/k8s_log/tasks/main.yml index d4769e09..fd312e3b 100644 --- a/tests/integration/targets/k8s_log/tasks/main.yml +++ b/tests/integration/targets/k8s_log/tasks/main.yml @@ -165,6 +165,79 @@ assert: that: tailed_log.log_lines | length == 5 + 1 + # Trying to call module without name and label_selectors + - name: Retrieve without neither name nor label_selectors provided + k8s_log: + namespace: "{{ test_namespace }}" + register: noname_log + ignore_errors: true + + - name: Ensure task failed + assert: + that: + - noname_log is failed + - 'noname_log.msg == "name must be provided for resources that do not support namespaced base url"' + + # Test retrieve all containers logs + - name: Create deployments + k8s: + namespace: "{{ test_namespace }}" + wait: yes + wait_timeout: "{{ k8s_wait_timeout | default(omit) }}" + wait_condition: + type: Complete + status: 'True' + definition: + apiVersion: batch/v1 + kind: Job + metadata: + name: multicontainer-log + spec: + template: + spec: + containers: + - name: p01 + image: busybox + command: ['sh'] + args: ['-c', 'for i in $(seq 0 9); do echo $i; done'] + - name: p02 + image: busybox + command: ['sh'] + args: ['-c', 'for i in $(seq 10 19); do echo $i; done'] + restartPolicy: Never + + - name: Retrieve logs from all containers + k8s_log: + api_version: batch/v1 + kind: Job + namespace: "{{ test_namespace }}" + name: multicontainer-log + all_containers: true + register: all_logs + + - name: Retrieve logs from first job + k8s_log: + api_version: batch/v1 + kind: Job + namespace: "{{ test_namespace }}" + name: multicontainer-log + container: p01 + register: log_1 + + - name: Retrieve logs from second job + k8s_log: + api_version: batch/v1 + kind: Job + namespace: "{{ test_namespace }}" + name: multicontainer-log + container: p02 + register: log_2 + + - name: Validate that log using all_containers=true is the sum of all logs + assert: + that: + - all_logs.log == (log_1.log + log_2.log) + always: - name: ensure that namespace is removed k8s: