Kubeconfig module improvement (#1123)

* Add kubeconfig module for managing Kubernetes config files

* Remove unnecessary requirement & Change version

* Move functions to module_utils

* Add unit tests

* Add kubeconfig module for managing Kubernetes config files

* Remove unnecessary requirement & Change version

* Move functions to module_utils

* Add unit tests

* Avoid linter errors

* Improve documentation clarity

* Redact sensitive kubeconfig information

* Imprvoe verbosity

* Move import statement for to_native to avoid linters check failure

* Fix linting error

* Add remove behavior

* Add tests for remove behavior

* Imporve documentation

* Add changelog

---------

Co-authored-by: Bianca Henderson <bianca@redhat.com>
This commit is contained in:
Youssef Ali
2026-05-19 17:10:19 +03:00
committed by GitHub
parent b58b2ca70e
commit 53c6c0ee80
5 changed files with 175 additions and 23 deletions

View File

@@ -0,0 +1,2 @@
minor_changes:
- kubeconfig - add ``remove`` value to the ``behavior`` option, allowing entries to be deleted from the kubeconfig file by name (https://github.com/ansible-collections/kubernetes.core/pull/1123).

View File

@@ -53,6 +53,8 @@ def merge_by_name(existing, new):
if name in merged: if name in merged:
if behavior == "keep": if behavior == "keep":
continue continue
elif behavior == "remove":
del merged[name]
elif behavior == "replace": elif behavior == "replace":
merged[name] = item_copy merged[name] = item_copy
else: else:
@@ -73,6 +75,8 @@ def merge_by_name(existing, new):
result[key] = item_copy[key] result[key] = item_copy[key]
merged[name] = result merged[name] = result
else: else:
if behavior == "remove":
continue
merged[name] = item_copy merged[name] = item_copy
return list(merged.values()) return list(merged.values())

View File

@@ -29,6 +29,7 @@ notes:
- The default is V(merge), which merges nested C(cluster), C(user), and C(context) data so unspecified keys are preserved. - The default is V(merge), which merges nested C(cluster), C(user), and C(context) data so unspecified keys are preserved.
- With V(replace), the previous entry for that name is dropped and only the new definition is used. - With V(replace), the previous entry for that name is dropped and only the new definition is used.
- With V(keep), the existing entry is left unchanged. - With V(keep), the existing entry is left unchanged.
- With V(remove), the existing entry is deleted from the kubeconfig entirely. If no entry with that name exists, the operation is silently skipped.
- This can be used to move kubeconfig files to a different location with different content. - This can be used to move kubeconfig files to a different location with different content.
- This module does not validate cluster connectivity or authentication. - This module does not validate cluster connectivity or authentication.
- The module supports C(check_mode) and will not write files when enabled. - The module supports C(check_mode) and will not write files when enabled.
@@ -57,7 +58,7 @@ options:
- List of cluster definitions to merge into the kubeconfig. - List of cluster definitions to merge into the kubeconfig.
- Each cluster is identified by its C(name). - Each cluster is identified by its C(name).
- When C(name) matches an existing cluster, the default C(behavior) is V(merge). - When C(name) matches an existing cluster, the default C(behavior) is V(merge).
- See the C(behavior) suboption for V(replace) and V(keep). - See the C(behavior) suboption for V(replace), V(keep), and V(remove).
type: list type: list
elements: dict elements: dict
required: false required: false
@@ -74,14 +75,15 @@ options:
- C(merge) - Update only the specified fields, preserve others (default). - C(merge) - Update only the specified fields, preserve others (default).
- C(replace) - Replace the entire cluster definition. - C(replace) - Replace the entire cluster definition.
- C(keep) - Keep existing cluster, skip this entry. - C(keep) - Keep existing cluster, skip this entry.
- C(remove) - Remove the cluster entry entirely. Silently skipped if the entry does not exist.
type: str type: str
choices: ['merge', 'replace', 'keep'] choices: ['merge', 'replace', 'keep', 'remove']
default: merge default: merge
cluster: cluster:
description: description:
- Cluster configuration details. - Cluster configuration details.
- Not required when C(behavior) is V(remove).
type: dict type: dict
required: true
suboptions: suboptions:
server: server:
description: description:
@@ -115,7 +117,7 @@ options:
- List of user authentication configurations. - List of user authentication configurations.
- Each user is identified by its C(name). - Each user is identified by its C(name).
- When C(name) matches an existing user, the default C(behavior) is V(merge). - When C(name) matches an existing user, the default C(behavior) is V(merge).
- See the C(behavior) suboption for V(replace) and V(keep). - See the C(behavior) suboption for V(replace), V(keep), and V(remove).
type: list type: list
elements: dict elements: dict
required: false required: false
@@ -132,14 +134,15 @@ options:
- C(merge) - Update only the specified fields, preserve others (default). - C(merge) - Update only the specified fields, preserve others (default).
- C(replace) - Replace the entire user definition. - C(replace) - Replace the entire user definition.
- C(keep) - Keep existing user, skip this entry. - C(keep) - Keep existing user, skip this entry.
- C(remove) - Remove the user entry entirely. Silently skipped if the entry does not exist.
type: str type: str
choices: ['merge', 'replace', 'keep'] choices: ['merge', 'replace', 'keep', 'remove']
default: merge default: merge
user: user:
description: description:
- User authentication configuration. - User authentication configuration.
- Not required when C(behavior) is V(remove).
type: dict type: dict
required: true
suboptions: suboptions:
token: token:
description: description:
@@ -188,7 +191,7 @@ options:
- List of context definitions linking users and clusters. - List of context definitions linking users and clusters.
- Each context is identified by its C(name). - Each context is identified by its C(name).
- When C(name) matches an existing context, the default C(behavior) is V(merge). - When C(name) matches an existing context, the default C(behavior) is V(merge).
- See the C(behavior) suboption for V(replace) and V(keep). - See the C(behavior) suboption for V(replace), V(keep), and V(remove).
type: list type: list
elements: dict elements: dict
required: false required: false
@@ -205,14 +208,15 @@ options:
- C(merge) - Update only the specified fields, preserve others (default). - C(merge) - Update only the specified fields, preserve others (default).
- C(replace) - Replace the entire context definition. - C(replace) - Replace the entire context definition.
- C(keep) - Keep existing context, skip this entry. - C(keep) - Keep existing context, skip this entry.
- C(remove) - Remove the context entry entirely. Silently skipped if the entry does not exist.
type: str type: str
choices: ['merge', 'replace', 'keep'] choices: ['merge', 'replace', 'keep', 'remove']
default: merge default: merge
context: context:
description: description:
- Context configuration linking cluster and user. - Context configuration linking cluster and user.
- Not required when C(behavior) is V(remove).
type: dict type: dict
required: true
suboptions: suboptions:
cluster: cluster:
description: description:
@@ -277,27 +281,72 @@ EXAMPLES = r"""
namespace: production namespace: production
current_context: prod-admin current_context: prod-admin
- name: Copy and modify kubeconfig - name: Add a second cluster to an existing kubeconfig without touching other entries
kubernetes.core.kubeconfig: kubernetes.core.kubeconfig:
path: /home/user/.kube/config path: /home/user/.kube/config
dest: /home/user/.kube/config-backup
clusters: clusters:
- name: new-cluster - name: staging-cluster
cluster: cluster:
server: https://new.example.com:6443 server: https://staging.k8s.example.com:6443
insecure-skip-tls-verify: true
users:
- name: staging-user
user:
client-certificate: /path/to/staging.crt
client-key: /path/to/staging.key
contexts:
- name: staging-admin
context:
cluster: staging-cluster
user: staging-user
namespace: staging
- name: Switch current context - name: Update only the token for an existing user, preserving all other user fields
kubernetes.core.kubeconfig: kubernetes.core.kubeconfig:
path: ~/.kube/config path: /home/user/.kube/config
current_context: prod-context
- name: Update user credentials
kubernetes.core.kubeconfig:
path: ~/.kube/config
users: users:
- name: admin-user - name: admin-user
behavior: merge
user: user:
token: "{{ new_admin_token }}" token: "{{ new_admin_token }}"
- name: Replace a cluster definition entirely.
kubernetes.core.kubeconfig:
path: /home/user/.kube/config
clusters:
- name: production-cluster
behavior: replace
cluster:
server: https://new-prod.k8s.example.com:6443
certificate-authority-data: LS0tLS1CRUdJTi...
- name: Remove a decommissioned cluster, user, and context
kubernetes.core.kubeconfig:
path: /home/user/.kube/config
clusters:
- name: old-cluster
behavior: remove
users:
- name: old-user
behavior: remove
contexts:
- name: old-context
behavior: remove
- name: Switch the active context
kubernetes.core.kubeconfig:
path: /home/user/.kube/config
current_context: staging-admin
- name: Copy a kubeconfig to a new location with an additional cluster merged in
kubernetes.core.kubeconfig:
path: /home/user/.kube/config
dest: /home/user/.kube/config-ci
clusters:
- name: ci-cluster
cluster:
server: https://ci.k8s.example.com:6443
insecure-skip-tls-verify: true
""" """
RETURN = r""" RETURN = r"""

View File

@@ -102,7 +102,7 @@
assert: assert:
that: that:
- update_result is changed - update_result is changed
- update_result.kubeconfig.clusters[0].cluster.server == "https://updated.example.com:6443" - update_result.kubeconfig.clusters | selectattr('name', 'equalto', test_cluster_name) | map(attribute='cluster') | map(attribute='server') | first == "https://updated.example.com:6443"
# Test 5: Check mode # Test 5: Check mode
- name: Test check mode - name: Test check mode
@@ -115,8 +115,72 @@
check_mode: true check_mode: true
register: check_mode_result register: check_mode_result
- name: Verify check mode didn't write - name: Verify check mode reports change but does not write
assert: assert:
that: that:
- check_mode_result is changed - check_mode_result is changed
- check_mode_result.kubeconfig.clusters | length == 3 # Includes new cluster in output - check_mode_result.kubeconfig.clusters | length == 3
- name: Verify check mode cluster was not actually written to disk
kubernetes.core.kubeconfig:
path: "{{ test_config_path }}"
register: after_check_mode
- name: Confirm check-mode-cluster is absent from disk
assert:
that:
- after_check_mode.kubeconfig.clusters | selectattr('name', 'equalto', 'check-mode-cluster') | list | length == 0
# Test 6: Remove behavior
- name: Remove cluster-2, user-2, and context-2
kubernetes.core.kubeconfig:
path: "{{ test_config_path }}"
clusters:
- name: cluster-2
behavior: remove
users:
- name: user-2
behavior: remove
contexts:
- name: context-2
behavior: remove
register: remove_result
- name: Verify entries were removed
assert:
that:
- remove_result is changed
- remove_result.kubeconfig.clusters | selectattr('name', 'equalto', 'cluster-2') | list | length == 0
- remove_result.kubeconfig.users | selectattr('name', 'equalto', 'user-2') | list | length == 0
- remove_result.kubeconfig.contexts | selectattr('name', 'equalto', 'context-2') | list | length == 0
# Test 7: Remove behavior is idempotent when entry does not exist
- name: Remove already-absent entry
kubernetes.core.kubeconfig:
path: "{{ test_config_path }}"
clusters:
- name: cluster-2
behavior: remove
register: remove_idempotent_result
- name: Verify no change when removing nonexistent entry
assert:
that:
- remove_idempotent_result is not changed
# Test 8: Keep behavior protects existing entry
- name: Attempt to overwrite protected cluster
kubernetes.core.kubeconfig:
path: "{{ test_config_path }}"
clusters:
- name: "{{ test_cluster_name }}"
behavior: keep
cluster:
server: https://should-not-apply.example.com:6443
register: keep_result
- name: Verify keep behavior left existing entry unchanged
assert:
that:
- keep_result is not changed
- keep_result.kubeconfig.clusters | selectattr('name', 'equalto', test_cluster_name) | map(attribute='cluster') | map(attribute='server') | first == "https://updated.example.com:6443"

View File

@@ -141,6 +141,39 @@ def test_merge_by_name_keep_behavior_preserves_existing():
assert result[0]["cluster"]["server"] == "https://old.com" assert result[0]["cluster"]["server"] == "https://old.com"
def test_merge_by_name_remove_behavior_removes_existing_entry():
existing = [{"name": "cluster-a", "cluster": {"server": "https://a.com"}}]
new = [{"name": "cluster-a", "behavior": "remove"}]
result = merge_by_name(existing, new)
assert result == []
def test_merge_by_name_remove_behavior_only_removes_target_entry():
existing = [
{"name": "cluster-a", "cluster": {"server": "https://a.com"}},
{"name": "cluster-b", "cluster": {"server": "https://b.com"}},
]
new = [{"name": "cluster-a", "behavior": "remove"}]
result = merge_by_name(existing, new)
assert len(result) == 1
assert result[0]["name"] == "cluster-b"
def test_merge_by_name_remove_behavior_silently_skips_nonexistent_entry():
existing = [{"name": "cluster-a", "cluster": {"server": "https://a.com"}}]
new = [{"name": "cluster-nonexistent", "behavior": "remove"}]
result = merge_by_name(existing, new)
assert len(result) == 1
assert result[0]["name"] == "cluster-a"
def test_merge_by_name_remove_behavior_on_empty_existing():
existing = []
new = [{"name": "cluster-a", "behavior": "remove"}]
result = merge_by_name(existing, new)
assert result == []
def test_merge_by_name_behavior_key_not_in_output(): def test_merge_by_name_behavior_key_not_in_output():
existing = [] existing = []
new = [ new = [