mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-27 05:43:02 +00:00
Provide a mechanism to hide fields from output
SUMMARY
The k8s and k8s_info modules can be a little noisy in verbose mode, and most of that is due to managedFields.
If we can provide a mechanism to hide managedFields, the output is a lot more useful.
ISSUE TYPE
Feature Pull Request
COMPONENT NAME
k8s, k8s_info
ADDITIONAL INFORMATION
Before
ANSIBLE_COLLECTIONS_PATH=../../.. ansible -m k8s_info -a 'kind=ConfigMap name=hide-fields-cm namespace=hide-fields' localhost
[WARNING]: No inventory was parsed, only implicit localhost is available
localhost | SUCCESS => {
"api_found": true,
"changed": false,
"resources": [
{
"apiVersion": "v1",
"data": {
"another": "value",
"hello": "world"
},
"kind": "ConfigMap",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"another\":\"value\",\"hello\":\"world\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"hide-fields-cm\",\"namespace\":\"hide-fields\"}}\n"
},
"creationTimestamp": "2023-06-13T01:47:47Z",
"managedFields": [
{
"apiVersion": "v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:another": {},
"f:hello": {}
},
"f:metadata": {
"f:annotations": {
".": {},
"f:kubectl.kubernetes.io/last-applied-configuration": {}
}
}
},
"manager": "kubectl-client-side-apply",
"operation": "Update",
"time": "2023-06-13T01:47:47Z"
}
],
"name": "hide-fields-cm",
"namespace": "hide-fields",
"resourceVersion": "2557394",
"uid": "f233da63-6374-4079-9825-3562c0ed123c"
}
}
]
}
After
ANSIBLE_COLLECTIONS_PATH=../../.. ansible -m k8s_info -a 'kind=ConfigMap name=hide-fields-cm namespace=hide-fields hidden_fields=metadata.managedFields' localhost
[WARNING]: No inventory was parsed, only implicit localhost is available
localhost | SUCCESS => {
"api_found": true,
"changed": false,
"resources": [
{
"apiVersion": "v1",
"data": {
"another": "value",
"hello": "world"
},
"kind": "ConfigMap",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"another\":\"value\",\"hello\":\"world\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"hide-fields-cm\",\"namespace\":\"hide-fields\"}}\n"
},
"creationTimestamp": "2023-06-13T01:47:47Z",
"name": "hide-fields-cm",
"namespace": "hide-fields",
"resourceVersion": "2557394",
"uid": "f233da63-6374-4079-9825-3562c0ed123c"
}
}
]
}
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Will Thames
237 lines
8.0 KiB
Python
237 lines
8.0 KiB
Python
# Copyright: (c) 2021, Red Hat | Ansible
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from typing import Dict
|
|
|
|
from ansible.module_utils._text import to_native
|
|
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
|
get_api_client,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
|
CoreException,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
|
create_definitions,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
|
K8sService,
|
|
diff_objects,
|
|
hide_fields,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
|
ResourceTimeout,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import exists
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.selector import (
|
|
LabelSelectorFilter,
|
|
)
|
|
|
|
|
|
def validate(client, module, resource):
|
|
def _prepend_resource_info(resource, msg):
|
|
return "%s %s: %s" % (resource["kind"], resource["metadata"]["name"], msg)
|
|
|
|
module.requires("kubernetes-validate")
|
|
|
|
warnings, errors = client.validate(
|
|
resource,
|
|
module.params["validate"].get("version"),
|
|
module.params["validate"].get("strict"),
|
|
)
|
|
|
|
if errors and module.params["validate"]["fail_on_error"]:
|
|
module.fail_json(
|
|
msg="\n".join([_prepend_resource_info(resource, error) for error in errors])
|
|
)
|
|
return [_prepend_resource_info(resource, msg) for msg in warnings + errors]
|
|
|
|
|
|
def get_definitions(svc, params):
|
|
try:
|
|
definitions = create_definitions(params)
|
|
except Exception as e:
|
|
msg = "Failed to load resource definition: {0}".format(e)
|
|
raise CoreException(msg) from e
|
|
|
|
delete_all = params.get("delete_all")
|
|
src = params.get("src")
|
|
resource_definition = params.get("resource_definition")
|
|
name = params.get("name")
|
|
state = params.get("state")
|
|
|
|
if (
|
|
delete_all
|
|
and state == "absent"
|
|
and name is None
|
|
and resource_definition is None
|
|
and src is None
|
|
):
|
|
# Delete all resources in the namespace for the specified resource type
|
|
if params.get("kind") is None:
|
|
raise CoreException(
|
|
"'kind' option is required to specify the resource type."
|
|
)
|
|
|
|
resource = svc.find_resource(
|
|
params.get("kind"), params.get("api_version"), fail=True
|
|
)
|
|
definitions = svc.retrieve_all(
|
|
resource,
|
|
params.get("namespace"),
|
|
params.get("label_selectors"),
|
|
)
|
|
|
|
return definitions
|
|
|
|
|
|
def run_module(module) -> None:
|
|
results = []
|
|
changed = False
|
|
client = get_api_client(module)
|
|
svc = K8sService(client, module)
|
|
|
|
definitions = get_definitions(svc, module.params)
|
|
|
|
for definition in definitions:
|
|
result = {"changed": False, "result": {}}
|
|
warnings = []
|
|
|
|
if module.params.get("validate") is not None:
|
|
warnings = validate(client, module, definition)
|
|
|
|
try:
|
|
result = perform_action(svc, definition, module.params)
|
|
except Exception as e:
|
|
try:
|
|
error = e.result
|
|
except AttributeError:
|
|
error = {}
|
|
try:
|
|
error["reason"] = e.__cause__.reason
|
|
except AttributeError:
|
|
pass
|
|
error["msg"] = to_native(e)
|
|
if warnings:
|
|
error.setdefault("warnings", []).extend(warnings)
|
|
|
|
if module.params.get("continue_on_error"):
|
|
result["error"] = error
|
|
else:
|
|
module.fail_json(**error)
|
|
|
|
if warnings:
|
|
result.setdefault("warnings", []).extend(warnings)
|
|
changed |= result["changed"]
|
|
results.append(result)
|
|
|
|
if len(results) == 1:
|
|
module.exit_json(**results[0])
|
|
|
|
module.exit_json(**{"changed": changed, "result": {"results": results}})
|
|
|
|
|
|
def perform_action(svc, definition: Dict, params: Dict) -> Dict:
|
|
origin_name = definition["metadata"].get("name")
|
|
namespace = definition["metadata"].get("namespace")
|
|
label_selectors = params.get("label_selectors")
|
|
state = params.get("state", None)
|
|
kind = definition.get("kind")
|
|
api_version = definition.get("apiVersion")
|
|
hidden_fields = params.get("hidden_fields")
|
|
|
|
result = {"changed": False, "result": {}}
|
|
instance = {}
|
|
|
|
resource = svc.find_resource(kind, api_version, fail=True)
|
|
definition["kind"] = resource.kind
|
|
definition["apiVersion"] = resource.group_version
|
|
existing = svc.retrieve(resource, definition)
|
|
|
|
if state == "absent":
|
|
if exists(existing) and existing.kind.endswith("List"):
|
|
instance = []
|
|
for item in existing.items:
|
|
r = svc.delete(resource, item, existing)
|
|
instance.append(r)
|
|
else:
|
|
instance = svc.delete(resource, definition, existing)
|
|
result["method"] = "delete"
|
|
if exists(existing):
|
|
result["changed"] = True
|
|
else:
|
|
if label_selectors:
|
|
filter_selector = LabelSelectorFilter(label_selectors)
|
|
if not filter_selector.isMatching(definition):
|
|
result["changed"] = False
|
|
result["msg"] = (
|
|
"resource 'kind={kind},name={name},namespace={namespace}' "
|
|
"filtered by label_selectors.".format(
|
|
kind=kind,
|
|
name=origin_name,
|
|
namespace=namespace,
|
|
)
|
|
)
|
|
return result
|
|
|
|
if params.get("apply"):
|
|
instance = svc.apply(resource, definition, existing)
|
|
result["method"] = "apply"
|
|
elif not existing:
|
|
if state == "patched":
|
|
result.setdefault("warnings", []).append(
|
|
"resource 'kind={kind},name={name}' was not found but will not be "
|
|
"created as 'state' parameter has been set to '{state}'".format(
|
|
kind=kind, name=definition["metadata"].get("name"), state=state
|
|
)
|
|
)
|
|
return result
|
|
instance = svc.create(resource, definition)
|
|
result["method"] = "create"
|
|
result["changed"] = True
|
|
elif params.get("force", False):
|
|
instance = svc.replace(resource, definition, existing)
|
|
result["method"] = "replace"
|
|
else:
|
|
instance = svc.update(resource, definition, existing)
|
|
result["method"] = "update"
|
|
|
|
# If needed, wait and/or create diff
|
|
success = True
|
|
|
|
if result["method"] == "delete":
|
|
# wait logic is a bit different for delete as `instance` may be a status object
|
|
if params.get("wait") and not svc.module.check_mode:
|
|
success, waited, duration = svc.wait(resource, definition)
|
|
result["duration"] = duration
|
|
else:
|
|
if params.get("wait") and not svc.module.check_mode:
|
|
success, instance, duration = svc.wait(resource, instance)
|
|
result["duration"] = duration
|
|
|
|
if result["method"] not in ("create", "delete"):
|
|
if existing:
|
|
existing = existing.to_dict()
|
|
else:
|
|
existing = {}
|
|
match, diffs = diff_objects(existing, instance, hidden_fields)
|
|
if match and diffs:
|
|
result.setdefault("warnings", []).append(
|
|
"No meaningful diff was generated, but the API may not be idempotent "
|
|
"(only metadata.generation or metadata.resourceVersion were changed)"
|
|
)
|
|
result["changed"] = not match
|
|
if svc.module._diff:
|
|
result["diff"] = diffs
|
|
|
|
result["result"] = hide_fields(instance, hidden_fields)
|
|
if not success:
|
|
raise ResourceTimeout(
|
|
'"{0}" "{1}": Timed out waiting on resource'.format(
|
|
definition["kind"], origin_name
|
|
),
|
|
result,
|
|
)
|
|
|
|
return result
|