mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-05-07 05:22:39 +00:00
* 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
---------
(cherry picked from commit e79ed52a4d)
Co-authored-by: Youssef Ali <154611350+YoussefKhalidAli@users.noreply.github.com>
Co-authored-by: Bianca Henderson <bianca@redhat.com>
This commit is contained in:
1
tests/integration/targets/kubeconfig/defaults/main.yml
Normal file
1
tests/integration/targets/kubeconfig/defaults/main.yml
Normal file
@@ -0,0 +1 @@
|
||||
test_directory: /tmp
|
||||
122
tests/integration/targets/kubeconfig/tasks/main.yml
Normal file
122
tests/integration/targets/kubeconfig/tasks/main.yml
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
- name: Set test variables
|
||||
set_fact:
|
||||
test_config_path: /tmp/test-kubeconfig
|
||||
test_cluster_name: test-cluster
|
||||
test_user_name: test-user
|
||||
test_context_name: test-context
|
||||
|
||||
# Test 1: Create new kubeconfig
|
||||
- name: Create new kubeconfig file
|
||||
kubernetes.core.kubeconfig:
|
||||
path: "{{ test_config_path }}"
|
||||
clusters:
|
||||
- name: "{{ test_cluster_name }}"
|
||||
cluster:
|
||||
server: https://test.example.com:6443
|
||||
insecure-skip-tls-verify: true
|
||||
users:
|
||||
- name: "{{ test_user_name }}"
|
||||
user:
|
||||
token: test-token-123
|
||||
contexts:
|
||||
- name: "{{ test_context_name }}"
|
||||
context:
|
||||
cluster: "{{ test_cluster_name }}"
|
||||
user: "{{ test_user_name }}"
|
||||
namespace: default
|
||||
current_context: "{{ test_context_name }}"
|
||||
register: create_result
|
||||
|
||||
- name: Verify file was created
|
||||
assert:
|
||||
that:
|
||||
- create_result is changed
|
||||
- create_result.kubeconfig.clusters | length == 1
|
||||
- create_result.kubeconfig['current-context'] == test_context_name
|
||||
|
||||
# Test 2: Idempotency check
|
||||
- name: Run same configuration again
|
||||
kubernetes.core.kubeconfig:
|
||||
path: "{{ test_config_path }}"
|
||||
clusters:
|
||||
- name: "{{ test_cluster_name }}"
|
||||
cluster:
|
||||
server: https://test.example.com:6443
|
||||
insecure-skip-tls-verify: true
|
||||
users:
|
||||
- name: "{{ test_user_name }}"
|
||||
user:
|
||||
token: test-token-123
|
||||
contexts:
|
||||
- name: "{{ test_context_name }}"
|
||||
context:
|
||||
cluster: "{{ test_cluster_name }}"
|
||||
user: "{{ test_user_name }}"
|
||||
namespace: default
|
||||
current_context: "{{ test_context_name }}"
|
||||
register: idempotent_result
|
||||
|
||||
- name: Verify idempotency
|
||||
assert:
|
||||
that:
|
||||
- idempotent_result is not changed
|
||||
|
||||
# Test 3: Merge new cluster
|
||||
- name: Add second cluster
|
||||
kubernetes.core.kubeconfig:
|
||||
path: "{{ test_config_path }}"
|
||||
clusters:
|
||||
- name: cluster-2
|
||||
cluster:
|
||||
server: https://cluster2.example.com:6443
|
||||
users:
|
||||
- name: user-2
|
||||
user:
|
||||
token: token-2
|
||||
contexts:
|
||||
- name: context-2
|
||||
context:
|
||||
cluster: cluster-2
|
||||
user: user-2
|
||||
register: merge_result
|
||||
|
||||
- name: Verify merge
|
||||
assert:
|
||||
that:
|
||||
- merge_result is changed
|
||||
- merge_result.kubeconfig.clusters | length == 2
|
||||
|
||||
# Test 4: Update existing entry
|
||||
- name: Update cluster server
|
||||
kubernetes.core.kubeconfig:
|
||||
path: "{{ test_config_path }}"
|
||||
clusters:
|
||||
- name: "{{ test_cluster_name }}"
|
||||
cluster:
|
||||
server: https://updated.example.com:6443
|
||||
insecure-skip-tls-verify: true
|
||||
register: update_result
|
||||
|
||||
- name: Verify update
|
||||
assert:
|
||||
that:
|
||||
- update_result is changed
|
||||
- update_result.kubeconfig.clusters[0].cluster.server == "https://updated.example.com:6443"
|
||||
|
||||
# Test 5: Check mode
|
||||
- name: Test check mode
|
||||
kubernetes.core.kubeconfig:
|
||||
path: "{{ test_config_path }}"
|
||||
clusters:
|
||||
- name: check-mode-cluster
|
||||
cluster:
|
||||
server: https://check.example.com:6443
|
||||
check_mode: true
|
||||
register: check_mode_result
|
||||
|
||||
- name: Verify check mode didn't write
|
||||
assert:
|
||||
that:
|
||||
- check_mode_result is changed
|
||||
- check_mode_result.kubeconfig.clusters | length == 3 # Includes new cluster in output
|
||||
231
tests/unit/module_utils/test_kubeconfig.py
Normal file
231
tests/unit/module_utils/test_kubeconfig.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import yaml
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.kubeconfig import (
|
||||
deep_merge,
|
||||
hash_data,
|
||||
load_yaml_file,
|
||||
merge_by_name,
|
||||
write_file,
|
||||
)
|
||||
|
||||
|
||||
# load_yaml_file
|
||||
def test_load_yaml_file_returns_empty_dict_for_missing_file():
|
||||
assert load_yaml_file("/nonexistent/path/config") == {}
|
||||
|
||||
|
||||
def test_load_yaml_file_returns_empty_dict_for_none():
|
||||
assert load_yaml_file(None) == {}
|
||||
|
||||
|
||||
def test_load_yaml_file_returns_empty_dict_for_empty_string():
|
||||
assert load_yaml_file("") == {}
|
||||
|
||||
|
||||
def test_load_yaml_file_loads_valid_yaml(tmp_path):
|
||||
config = {"apiVersion": "v1", "kind": "Config", "clusters": []}
|
||||
f = tmp_path / "config"
|
||||
f.write_text(yaml.safe_dump(config))
|
||||
assert load_yaml_file(str(f)) == config
|
||||
|
||||
|
||||
def test_load_yaml_file_returns_empty_dict_for_empty_file(tmp_path):
|
||||
f = tmp_path / "config"
|
||||
f.write_text("")
|
||||
assert load_yaml_file(str(f)) == {}
|
||||
|
||||
|
||||
# deep_merge
|
||||
def test_deep_merge_adds_new_keys():
|
||||
base = {"a": 1}
|
||||
updates = {"b": 2}
|
||||
assert deep_merge(base, updates) == {"a": 1, "b": 2}
|
||||
|
||||
|
||||
def test_deep_merge_overwrites_scalar():
|
||||
base = {"a": 1}
|
||||
updates = {"a": 99}
|
||||
assert deep_merge(base, updates) == {"a": 99}
|
||||
|
||||
|
||||
def test_deep_merge_recursively_merges_dicts():
|
||||
base = {
|
||||
"cluster": {
|
||||
"server": "https://old.example.com",
|
||||
"insecure-skip-tls-verify": True,
|
||||
}
|
||||
}
|
||||
updates = {"cluster": {"server": "https://new.example.com"}}
|
||||
result = deep_merge(base, updates)
|
||||
assert result["cluster"]["server"] == "https://new.example.com"
|
||||
assert result["cluster"]["insecure-skip-tls-verify"] is True
|
||||
|
||||
|
||||
def test_deep_merge_does_not_mutate_base():
|
||||
base = {"a": {"b": 1}}
|
||||
updates = {"a": {"c": 2}}
|
||||
deep_merge(base, updates)
|
||||
assert base == {"a": {"b": 1}}
|
||||
|
||||
|
||||
def test_deep_merge_overwrites_dict_with_scalar():
|
||||
base = {"a": {"nested": 1}}
|
||||
updates = {"a": "flat"}
|
||||
assert deep_merge(base, updates) == {"a": "flat"}
|
||||
|
||||
|
||||
# merge_by_name
|
||||
def test_merge_by_name_adds_new_entry():
|
||||
existing = []
|
||||
new = [{"name": "cluster-a", "cluster": {"server": "https://a.example.com"}}]
|
||||
result = merge_by_name(existing, new)
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "cluster-a"
|
||||
|
||||
|
||||
def test_merge_by_name_preserves_existing_when_no_new():
|
||||
existing = [{"name": "cluster-a", "cluster": {"server": "https://a.example.com"}}]
|
||||
result = merge_by_name(existing, [])
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "cluster-a"
|
||||
|
||||
|
||||
def test_merge_by_name_default_behavior_merges_fields():
|
||||
existing = [
|
||||
{
|
||||
"name": "cluster-a",
|
||||
"cluster": {"server": "https://old.com", "insecure-skip-tls-verify": True},
|
||||
}
|
||||
]
|
||||
new = [{"name": "cluster-a", "cluster": {"server": "https://new.com"}}]
|
||||
result = merge_by_name(existing, new)
|
||||
assert len(result) == 1
|
||||
assert result[0]["cluster"]["server"] == "https://new.com"
|
||||
assert result[0]["cluster"]["insecure-skip-tls-verify"] is True
|
||||
|
||||
|
||||
def test_merge_by_name_replace_behavior_replaces_entire_entry():
|
||||
existing = [
|
||||
{
|
||||
"name": "cluster-a",
|
||||
"cluster": {"server": "https://old.com", "insecure-skip-tls-verify": True},
|
||||
}
|
||||
]
|
||||
new = [
|
||||
{
|
||||
"name": "cluster-a",
|
||||
"behavior": "replace",
|
||||
"cluster": {"server": "https://new.com"},
|
||||
}
|
||||
]
|
||||
result = merge_by_name(existing, new)
|
||||
assert result[0]["cluster"] == {"server": "https://new.com"}
|
||||
assert "insecure-skip-tls-verify" not in result[0]["cluster"]
|
||||
|
||||
|
||||
def test_merge_by_name_keep_behavior_preserves_existing():
|
||||
existing = [{"name": "cluster-a", "cluster": {"server": "https://old.com"}}]
|
||||
new = [
|
||||
{
|
||||
"name": "cluster-a",
|
||||
"behavior": "keep",
|
||||
"cluster": {"server": "https://new.com"},
|
||||
}
|
||||
]
|
||||
result = merge_by_name(existing, new)
|
||||
assert result[0]["cluster"]["server"] == "https://old.com"
|
||||
|
||||
|
||||
def test_merge_by_name_behavior_key_not_in_output():
|
||||
existing = []
|
||||
new = [
|
||||
{
|
||||
"name": "cluster-a",
|
||||
"behavior": "replace",
|
||||
"cluster": {"server": "https://a.com"},
|
||||
}
|
||||
]
|
||||
result = merge_by_name(existing, new)
|
||||
assert "behavior" not in result[0]
|
||||
|
||||
|
||||
def test_merge_by_name_skips_items_without_name():
|
||||
existing = []
|
||||
new = [{"cluster": {"server": "https://a.com"}}]
|
||||
result = merge_by_name(existing, new)
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_merge_by_name_skips_non_dict_items():
|
||||
existing = []
|
||||
new = ["not-a-dict", 42]
|
||||
result = merge_by_name(existing, new)
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_merge_by_name_adds_multiple_new_entries():
|
||||
existing = []
|
||||
new = [
|
||||
{"name": "cluster-a", "cluster": {"server": "https://a.com"}},
|
||||
{"name": "cluster-b", "cluster": {"server": "https://b.com"}},
|
||||
]
|
||||
result = merge_by_name(existing, new)
|
||||
names = [r["name"] for r in result]
|
||||
assert "cluster-a" in names
|
||||
assert "cluster-b" in names
|
||||
|
||||
|
||||
def test_merge_by_name_existing_non_dict_items_are_skipped():
|
||||
existing = ["not-a-dict", {"cluster": {"server": "https://a.com"}}]
|
||||
new = [{"name": "cluster-b", "cluster": {"server": "https://b.com"}}]
|
||||
result = merge_by_name(existing, new)
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "cluster-b"
|
||||
|
||||
|
||||
# hash_data
|
||||
def test_hash_data_returns_string():
|
||||
assert isinstance(hash_data({}), str)
|
||||
|
||||
|
||||
def test_hash_data_different_input_different_hash():
|
||||
assert hash_data({"a": 1}) != hash_data({"a": 2})
|
||||
|
||||
|
||||
def test_hash_data_order_independent():
|
||||
a = {"x": 1, "y": 2}
|
||||
b = {"y": 2, "x": 1}
|
||||
assert hash_data(a) == hash_data(b)
|
||||
|
||||
|
||||
# write_file
|
||||
def test_write_file_returns_false_for_empty_dest():
|
||||
assert write_file("", {"apiVersion": "v1"}) is False
|
||||
|
||||
|
||||
def test_write_file_returns_false_for_none_dest():
|
||||
assert write_file(None, {"apiVersion": "v1"}) is False
|
||||
|
||||
|
||||
def test_write_file_writes_valid_yaml(tmp_path):
|
||||
dest = str(tmp_path / "config")
|
||||
data = {"apiVersion": "v1", "kind": "Config"}
|
||||
result = write_file(dest, data)
|
||||
assert result is True
|
||||
with open(dest, "r") as f:
|
||||
written = yaml.safe_load(f)
|
||||
assert written == data
|
||||
|
||||
|
||||
def test_write_file_overwrites_existing_file(tmp_path):
|
||||
dest = str(tmp_path / "config")
|
||||
write_file(dest, {"apiVersion": "v1"})
|
||||
write_file(dest, {"apiVersion": "v2"})
|
||||
with open(dest, "r") as f:
|
||||
written = yaml.safe_load(f)
|
||||
assert written["apiVersion"] == "v2"
|
||||
Reference in New Issue
Block a user