mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-26 21:33:02 +00:00
Add resource definition refactor (#278)
Add resource definition refactor SUMMARY This refactors most of the logic around creating a list of functional resource definitions based on input parameters for the module. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis <None> Reviewed-by: Abhijeet Kasurde <None> Reviewed-by: Mike Graves <mgraves@redhat.com> Reviewed-by: None <None>
This commit is contained in:
129
plugins/module_utils/k8s/resource.py
Normal file
129
plugins/module_utils/k8s/resource.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Copyright: (c) 2021, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
import os
|
||||
from typing import cast, Dict, Iterable, List, Optional, Union
|
||||
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
# Handled in module setup
|
||||
pass
|
||||
|
||||
|
||||
class ResourceDefinition(dict):
|
||||
"""Representation of a resource definition.
|
||||
|
||||
This is a thin wrapper around a dictionary representation of a resource
|
||||
definition, with a few properties defined for conveniently accessing the
|
||||
commonly used fields.
|
||||
"""
|
||||
|
||||
@property
|
||||
def kind(self) -> Optional[str]:
|
||||
return self.get("kind")
|
||||
|
||||
@property
|
||||
def api_version(self) -> Optional[str]:
|
||||
return self.get("apiVersion")
|
||||
|
||||
@property
|
||||
def namespace(self) -> Optional[str]:
|
||||
metadata = self.get("metadata", {})
|
||||
return metadata.get("namespace")
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
metadata = self.get("metadata", {})
|
||||
return metadata.get("name")
|
||||
|
||||
|
||||
def create_definitions(params: Dict) -> List[ResourceDefinition]:
|
||||
"""Create a list of ResourceDefinitions from module inputs.
|
||||
|
||||
This will take the module's inputs and return a list of ResourceDefintion
|
||||
objects. The resource definitions returned by this function should be as
|
||||
complete a definition as we can create based on the input. Any *List kinds
|
||||
will be removed and replaced by the resources contained in it.
|
||||
"""
|
||||
if params.get("resource_definition"):
|
||||
d = cast(Union[str, List, Dict], params.get("resource_definition"))
|
||||
definitions = from_yaml(d)
|
||||
elif params.get("src"):
|
||||
d = cast(str, params.get("src"))
|
||||
definitions = from_file(d)
|
||||
else:
|
||||
# We'll create an empty definition and let merge_params set values
|
||||
# from the module parameters.
|
||||
definitions = [{}]
|
||||
|
||||
resource_definitions: List[Dict] = []
|
||||
for definition in definitions:
|
||||
merge_params(definition, params)
|
||||
kind = cast(Optional[str], definition.get("kind"))
|
||||
if kind and kind.endswith("List"):
|
||||
resource_definitions += flatten_list_kind(definition, params)
|
||||
else:
|
||||
resource_definitions.append(definition)
|
||||
return list(map(ResourceDefinition, resource_definitions))
|
||||
|
||||
|
||||
def from_yaml(definition: Union[str, List, Dict]) -> Iterable[Dict]:
|
||||
"""Load resource definitions from a yaml definition."""
|
||||
definitions: List[Dict] = []
|
||||
if isinstance(definition, string_types):
|
||||
definitions += yaml.safe_load_all(definition)
|
||||
elif isinstance(definition, list):
|
||||
for item in definition:
|
||||
if isinstance(item, string_types):
|
||||
definitions += yaml.safe_load_all(item)
|
||||
else:
|
||||
definitions.append(item)
|
||||
else:
|
||||
definition = cast(Dict, definition)
|
||||
definitions.append(definition)
|
||||
return filter(None, definitions)
|
||||
|
||||
|
||||
def from_file(filepath: str) -> Iterable[Dict]:
|
||||
"""Load resource definitions from a path to a yaml file."""
|
||||
path = os.path.normpath(filepath)
|
||||
with open(path, "r") as f:
|
||||
definitions = list(yaml.safe_load_all(f))
|
||||
return filter(None, definitions)
|
||||
|
||||
|
||||
def merge_params(definition: Dict, params: Dict) -> Dict:
|
||||
"""Merge module parameters with the resource definition.
|
||||
|
||||
Fields in the resource definition take precedence over module parameters.
|
||||
"""
|
||||
definition.setdefault("kind", params.get("kind"))
|
||||
definition.setdefault("apiVersion", params.get("api_version"))
|
||||
metadata = definition.setdefault("metadata", {})
|
||||
# The following should only be set if we have values for them
|
||||
if params.get("namespace"):
|
||||
metadata.setdefault("namespace", params.get("namespace"))
|
||||
if params.get("name"):
|
||||
metadata.setdefault("name", params.get("name"))
|
||||
if params.get("generate_name"):
|
||||
metadata.setdefault("generateName", params.get("generate_name"))
|
||||
return definition
|
||||
|
||||
|
||||
def flatten_list_kind(definition: Dict, params: Dict) -> List[Dict]:
|
||||
"""Replace *List kind with the items it contains.
|
||||
|
||||
This will take a definition for a *List resource and return a list of
|
||||
definitions for the items contained within the List.
|
||||
"""
|
||||
items = []
|
||||
kind = cast(str, definition.get("kind"))[:-4]
|
||||
api_version = definition.get("apiVersion")
|
||||
for item in definition.get("items", []):
|
||||
item.setdefault("kind", kind)
|
||||
item.setdefault("apiVersion", api_version)
|
||||
items.append(merge_params(item, params))
|
||||
return items
|
||||
11
tests/unit/module_utils/fixtures/definitions.yml
Normal file
11
tests/unit/module_utils/fixtures/definitions.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
kind: Pod
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
---
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
187
tests/unit/module_utils/test_resource.py
Normal file
187
tests/unit/module_utils/test_resource.py
Normal file
@@ -0,0 +1,187 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
||||
create_definitions,
|
||||
flatten_list_kind,
|
||||
from_file,
|
||||
from_yaml,
|
||||
merge_params,
|
||||
)
|
||||
|
||||
|
||||
def test_create_definitions_loads_from_definition():
|
||||
params = {
|
||||
"resource_definition": {
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {"name": "foo", "namespace": "bar"},
|
||||
}
|
||||
}
|
||||
results = create_definitions(params)
|
||||
assert len(results) == 1
|
||||
assert results[0].kind == "Pod"
|
||||
assert results[0].api_version == "v1"
|
||||
assert results[0].name == "foo"
|
||||
assert results[0].namespace == "bar"
|
||||
|
||||
|
||||
def test_create_definitions_loads_from_file():
|
||||
current = Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
params = {"src": current / "fixtures/definitions.yml"}
|
||||
results = create_definitions(params)
|
||||
assert len(results) == 2
|
||||
assert results[0].kind == "Pod"
|
||||
assert results[1].kind == "ConfigMap"
|
||||
|
||||
|
||||
def test_create_definitions_loads_from_params():
|
||||
params = {
|
||||
"kind": "Pod",
|
||||
"api_version": "v1",
|
||||
"name": "foo",
|
||||
"namespace": "foobar",
|
||||
}
|
||||
results = create_definitions(params)
|
||||
assert len(results) == 1
|
||||
assert results[0] == {
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {"name": "foo", "namespace": "foobar"},
|
||||
}
|
||||
|
||||
|
||||
def test_create_definitions_loads_list_kind():
|
||||
params = {
|
||||
"resource_definition": {
|
||||
"kind": "PodList",
|
||||
"apiVersion": "v1",
|
||||
"items": [
|
||||
{"kind": "Pod", "metadata": {"name": "foo"}},
|
||||
{"kind": "Pod", "metadata": {"name": "bar"}},
|
||||
],
|
||||
}
|
||||
}
|
||||
results = create_definitions(params)
|
||||
assert len(results) == 2
|
||||
assert results[0].name == "foo"
|
||||
assert results[1].name == "bar"
|
||||
|
||||
|
||||
def test_merge_params_does_not_overwrite():
|
||||
definition = {
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {"name": "foo", "namespace": "bar"},
|
||||
}
|
||||
params = {
|
||||
"kind": "Service",
|
||||
"api_version": "v2",
|
||||
"name": "baz",
|
||||
"namespace": "gaz",
|
||||
}
|
||||
result = merge_params(definition, params)
|
||||
assert result == definition
|
||||
|
||||
|
||||
def test_merge_params_adds_module_params():
|
||||
params = {
|
||||
"kind": "Pod",
|
||||
"api_version": "v1",
|
||||
"namespace": "bar",
|
||||
"generate_name": "foo-",
|
||||
}
|
||||
result = merge_params({}, params)
|
||||
assert result == {
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {"generateName": "foo-", "namespace": "bar"},
|
||||
}
|
||||
|
||||
|
||||
def test_from_yaml_loads_string_docs():
|
||||
definition = """
|
||||
kind: Pod
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
---
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
"""
|
||||
result = list(from_yaml(definition))
|
||||
assert result[0]["kind"] == "Pod"
|
||||
assert result[1]["kind"] == "ConfigMap"
|
||||
|
||||
|
||||
def test_from_yaml_loads_list():
|
||||
definition = [
|
||||
"""
|
||||
kind: Pod
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
""",
|
||||
"""
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
""",
|
||||
{
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {"name": "baz", "namespace": "bar"},
|
||||
},
|
||||
]
|
||||
result = list(from_yaml(definition))
|
||||
assert len(result) == 3
|
||||
assert result[0]["kind"] == "Pod"
|
||||
assert result[1]["kind"] == "ConfigMap"
|
||||
assert result[2]["metadata"]["name"] == "baz"
|
||||
|
||||
|
||||
def test_from_yaml_loads_dictionary():
|
||||
definition = {
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {"name": "foo", "namespace": "bar"},
|
||||
}
|
||||
result = list(from_yaml(definition))
|
||||
assert result[0]["kind"] == "Pod"
|
||||
|
||||
|
||||
def test_from_file_loads_definitions():
|
||||
current = Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
result = list(from_file(current / "fixtures/definitions.yml"))
|
||||
assert result[0]["kind"] == "Pod"
|
||||
assert result[1]["kind"] == "ConfigMap"
|
||||
|
||||
|
||||
def test_flatten_list_kind_flattens():
|
||||
definition = {
|
||||
"kind": "PodList",
|
||||
"apiVersion": "v1",
|
||||
"items": [
|
||||
{"kind": "Pod", "metadata": {"name": "foo"}},
|
||||
{"kind": "Pod", "metadata": {"name": "bar"}},
|
||||
],
|
||||
}
|
||||
result = flatten_list_kind(definition, {"namespace": "foobar"})
|
||||
assert len(result) == 2
|
||||
|
||||
assert result[0]["kind"] == "Pod"
|
||||
assert result[0]["apiVersion"] == "v1"
|
||||
assert result[0]["metadata"]["name"] == "foo"
|
||||
assert result[0]["metadata"]["namespace"] == "foobar"
|
||||
|
||||
assert result[1]["kind"] == "Pod"
|
||||
assert result[1]["apiVersion"] == "v1"
|
||||
assert result[1]["metadata"]["name"] == "bar"
|
||||
assert result[1]["metadata"]["namespace"] == "foobar"
|
||||
Reference in New Issue
Block a user