mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-06-09 18:16:09 +00:00
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:
2
changelogs/fragments/1123-kubeconfig-remove-behavior.yml
Normal file
2
changelogs/fragments/1123-kubeconfig-remove-behavior.yml
Normal 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).
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user