Migrate k8s (#311)

* Use refactored module_utils

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix runner

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix runner

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Update runner.py

* black runner

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix units

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix ResourceTimeout

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Attempt to fix 'Create custom resource'

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Update svc.find_resource(..., fail=True)

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Attempt to fix integration tests

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix apiVersion for Job

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix crd

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add exception = None

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix apiVersion for definition

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix assert

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix returned results

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Update runner to return results accordingly

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix assert

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add validate-missing

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Update client.py

* Fix failures

* Fix black formatting

Co-authored-by: Mike Graves <mgraves@redhat.com>
This commit is contained in:
Alina Buzachis
2022-02-04 17:14:21 +01:00
committed by Mike Graves
parent 58a0fb1605
commit 3bf147580f
12 changed files with 114 additions and 84 deletions

View File

@@ -203,8 +203,10 @@ class K8SClient:
params["dry_run"] = True params["dry_run"] = True
return params return params
def validate(self, resource, **params): def validate(
pass self, resource, version: Optional[str] = None, strict: Optional[bool] = False
):
return self.client.validate(resource, version, strict)
def get(self, resource, **params): def get(self, resource, **params):
return resource.get(**params) return resource.get(**params)

View File

@@ -109,6 +109,13 @@ def gather_versions() -> dict:
except ImportError: except ImportError:
pass pass
try:
import kubernetes_validate
versions["kubernetes-validate"] = kubernetes_validate.__version__
except ImportError:
pass
try: try:
import yaml import yaml

View File

@@ -3,6 +3,8 @@
from typing import Dict from typing import Dict
from ansible.module_utils._text import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
get_api_client, get_api_client,
) )
@@ -15,7 +17,6 @@ from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import ( from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
CoreException, CoreException,
) )
from ansible_collections.kubernetes.core.plugins.module_utils.selector import ( from ansible_collections.kubernetes.core.plugins.module_utils.selector import (
LabelSelectorFilter, LabelSelectorFilter,
) )
@@ -42,32 +43,40 @@ def validate(client, module, resource):
def run_module(module) -> None: def run_module(module) -> None:
results = [] results = []
changed = False
client = get_api_client(module) client = get_api_client(module)
svc = K8sService(client, module) svc = K8sService(client, module)
definitions = create_definitions(module.params) definitions = create_definitions(module.params)
for definition in definitions: for definition in definitions:
module.warnings = [] result = {"changed": False, "result": {}, "warnings": []}
warnings = []
if module.params["validate"] is not None: if module.params.get("validate") is not None:
module.warnings = validate(client, module, definition) warnings = validate(client, module, definition)
try: try:
result = perform_action(svc, definition, module.params) result = perform_action(svc, definition, module.params)
except CoreException as e: except CoreException as e:
if module.warnings: msg = to_native(e)
e["msg"] += "\n" + "\n ".join(module.warnings) if warnings:
msg += "\n" + "\n ".join(warnings)
if module.params.get("continue_on_error"): if module.params.get("continue_on_error"):
result = {"error": "{0}".format(e)} result["error"] = {"msg": msg}
else: else:
module.fail_json(msg=e) module.fail_json(msg=msg)
if module.warnings:
result["warnings"] = module.warnings
if warnings:
result.setdefault("warnings", [])
result["warnings"] += warnings
changed |= result["changed"]
results.append(result) results.append(result)
module.exit_json(**results) 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: def perform_action(svc, definition: Dict, params: Dict) -> Dict:
@@ -75,9 +84,13 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict:
namespace = definition["metadata"].get("namespace") namespace = definition["metadata"].get("namespace")
label_selectors = params.get("label_selectors") label_selectors = params.get("label_selectors")
state = params.get("state", None) state = params.get("state", None)
result = {} kind = definition.get("kind")
api_version = definition.get("apiVersion")
result = {"changed": False, "result": {}}
resource = svc.find_resource(definition) resource = svc.find_resource(kind, api_version, fail=True)
definition["kind"] = resource.kind
definition["apiVersion"] = resource.group_version
existing = svc.retrieve(resource, definition) existing = svc.retrieve(resource, definition)
if state == "absent": if state == "absent":
@@ -91,7 +104,7 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict:
result["msg"] = ( result["msg"] = (
"resource 'kind={kind},name={name},namespace={namespace}' " "resource 'kind={kind},name={name},namespace={namespace}' "
"filtered by label_selectors.".format( "filtered by label_selectors.".format(
kind=definition["kind"], name=origin_name, namespace=namespace, kind=kind, name=origin_name, namespace=namespace,
) )
) )
return result return result
@@ -100,6 +113,14 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict:
result = svc.apply(resource, definition, existing) result = svc.apply(resource, definition, existing)
result["method"] = "apply" result["method"] = "apply"
elif not existing: 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
result = svc.create(resource, definition) result = svc.create(resource, definition)
result["method"] = "create" result["method"] = "create"
elif params.get("force", False): elif params.get("force", False):

View File

@@ -171,29 +171,35 @@ class K8sService:
msg = "Failed to patch object: {0}".format(reason) msg = "Failed to patch object: {0}".format(reason)
raise CoreException(msg) from e raise CoreException(msg) from e
def retrieve(self, resource: Resource, definition: Dict) -> Dict: def retrieve(self, resource: Resource, definition: Dict) -> ResourceInstance:
state = self.module.params.get("state", None) state = self.module.params.get("state", None)
append_hash = self.module.params.get("append_hash", False) append_hash = self.module.params.get("append_hash", False)
name = definition["metadata"].get("name") name = definition["metadata"].get("name")
generate_name = definition["metadata"].get("generateName")
namespace = definition["metadata"].get("namespace") namespace = definition["metadata"].get("namespace")
label_selectors = self.module.params.get("label_selectors") label_selectors = self.module.params.get("label_selectors")
results = { existing: ResourceInstance = None
"changed": False,
"result": {},
}
existing = None
try: try:
# ignore append_hash for resources other than ConfigMap and Secret # ignore append_hash for resources other than ConfigMap and Secret
if append_hash and definition["kind"] in ["ConfigMap", "Secret"]: if append_hash and definition["kind"] in ["ConfigMap", "Secret"]:
name = "%s-%s" % (name, generate_hash(definition)) if name:
definition["metadata"]["name"] = name name = "%s-%s" % (name, generate_hash(definition))
params = dict(name=name) definition["metadata"]["name"] = name
elif generate_name:
definition["metadata"]["generateName"] = "%s-%s" % (
generate_name,
generate_hash(definition),
)
params = {}
if name:
params["name"] = name
if namespace: if namespace:
params["namespace"] = namespace params["namespace"] = namespace
if label_selectors: if label_selectors:
params["label_selector"] = ",".join(label_selectors) params["label_selector"] = ",".join(label_selectors)
existing = self.client.get(resource, **params) if "name" in params or "label_selector" in params:
existing = self.client.get(resource, **params)
except (NotFoundError, MethodNotAllowedError): except (NotFoundError, MethodNotAllowedError):
pass pass
except ForbiddenError as e: except ForbiddenError as e:
@@ -210,10 +216,7 @@ class K8sService:
msg = "Failed to retrieve requested object: {0}".format(reason) msg = "Failed to retrieve requested object: {0}".format(reason)
raise CoreException(msg) from e raise CoreException(msg) from e
if existing: return existing
results["result"] = existing.to_dict()
return results
def find( def find(
self, self,
@@ -345,10 +348,12 @@ class K8sService:
results["result"] = k8s_obj results["result"] = k8s_obj
if wait and not self.module.check_mode: if wait and not self.module.check_mode:
definition["metadata"].update({"name": k8s_obj["metadata"]["name"]})
waiter = get_waiter(self.client, resource, condition=wait_condition) waiter = get_waiter(self.client, resource, condition=wait_condition)
success, results["result"], results["duration"] = waiter.wait( success, results["result"], results["duration"] = waiter.wait(
timeout=wait_timeout, sleep=wait_sleep, name=name, namespace=namespace, timeout=wait_timeout,
sleep=wait_sleep,
name=k8s_obj["metadata"]["name"],
namespace=namespace,
) )
results["changed"] = True results["changed"] = True
@@ -358,7 +363,7 @@ class K8sService:
'"{0}" "{1}": Resource creation timed out'.format( '"{0}" "{1}": Resource creation timed out'.format(
definition["kind"], origin_name definition["kind"], origin_name
), ),
**results results,
) )
return results return results
@@ -431,7 +436,7 @@ class K8sService:
'"{0}" "{1}": Resource apply timed out'.format( '"{0}" "{1}": Resource apply timed out'.format(
definition["kind"], origin_name definition["kind"], origin_name
), ),
**results results,
) )
return results return results
@@ -493,7 +498,7 @@ class K8sService:
'"{0}" "{1}": Resource replacement timed out'.format( '"{0}" "{1}": Resource replacement timed out'.format(
definition["kind"], origin_name definition["kind"], origin_name
), ),
**results results,
) )
return results return results
@@ -514,16 +519,25 @@ class K8sService:
wait_condition = self.module.params["wait_condition"] wait_condition = self.module.params["wait_condition"]
results = {"changed": False, "result": {}} results = {"changed": False, "result": {}}
if self.module.check_mode and not self.module.client.dry_run: if self.module.check_mode and not self.client.dry_run:
k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition)) k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition))
else: else:
exception = None
for merge_type in self.module.params.get("merge_type") or [ for merge_type in self.module.params.get("merge_type") or [
"strategic-merge", "strategic-merge",
"merge", "merge",
]: ]:
k8s_obj = self.patch_resource( try:
resource, definition, name, namespace, merge_type=merge_type, k8s_obj = self.patch_resource(
) resource, definition, name, namespace, merge_type=merge_type,
)
exception = None
except CoreException as e:
exception = e
continue
break
if exception:
raise exception
success = True success = True
results["result"] = k8s_obj results["result"] = k8s_obj
@@ -545,7 +559,7 @@ class K8sService:
'"{0}" "{1}": Resource update timed out'.format( '"{0}" "{1}": Resource update timed out'.format(
definition["kind"], origin_name definition["kind"], origin_name
), ),
**results results,
) )
return results return results
@@ -581,6 +595,15 @@ class K8sService:
if self.module.check_mode and not self.client.dry_run: if self.module.check_mode and not self.client.dry_run:
return results return results
else: else:
if name:
params["name"] = name
if namespace:
params["namespace"] = namespace
if label_selectors:
params["label_selector"] = ",".join(label_selectors)
if delete_options: if delete_options:
body = { body = {
"apiVersion": "v1", "apiVersion": "v1",
@@ -614,7 +637,7 @@ class K8sService:
'"{0}" "{1}": Resource deletion timed out'.format( '"{0}" "{1}": Resource deletion timed out'.format(
definition["kind"], origin_name definition["kind"], origin_name
), ),
**results results,
) )
return results return results

View File

@@ -157,7 +157,7 @@ class Waiter:
try: try:
response = self.client.get(self.resource, **params) response = self.client.get(self.resource, **params)
except NotFoundError: except NotFoundError:
pass response = None
if self.predicate(response): if self.predicate(response):
break break
if response: if response:

View File

@@ -391,6 +391,12 @@ from ansible_collections.kubernetes.core.plugins.module_utils.args_common import
RESOURCE_ARG_SPEC, RESOURCE_ARG_SPEC,
DELETE_OPTS_ARG_SPEC, DELETE_OPTS_ARG_SPEC,
) )
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
AnsibleK8SModule,
)
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.runner import (
run_module,
)
def validate_spec(): def validate_spec():
@@ -437,28 +443,6 @@ def argspec():
return argument_spec return argument_spec
def execute_module(module, k8s_ansible_mixin):
k8s_ansible_mixin.module = module
k8s_ansible_mixin.argspec = module.argument_spec
k8s_ansible_mixin.check_mode = k8s_ansible_mixin.module.check_mode
k8s_ansible_mixin.params = k8s_ansible_mixin.module.params
k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json
k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json
k8s_ansible_mixin.exit_json = k8s_ansible_mixin.module.exit_json
k8s_ansible_mixin.warn = k8s_ansible_mixin.module.warn
k8s_ansible_mixin.warnings = []
k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get("kind")
k8s_ansible_mixin.api_version = k8s_ansible_mixin.params.get("api_version")
k8s_ansible_mixin.name = k8s_ansible_mixin.params.get("name")
k8s_ansible_mixin.generate_name = k8s_ansible_mixin.params.get("generate_name")
k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get("namespace")
k8s_ansible_mixin.check_library_version()
k8s_ansible_mixin.set_resource_definitions(module)
k8s_ansible_mixin.execute_module()
def main(): def main():
mutually_exclusive = [ mutually_exclusive = [
("resource_definition", "src"), ("resource_definition", "src"),
@@ -467,19 +451,14 @@ def main():
("template", "src"), ("template", "src"),
("name", "generate_name"), ("name", "generate_name"),
] ]
module = AnsibleModule(
module = AnsibleK8SModule(
module_class=AnsibleModule,
argument_spec=argspec(), argument_spec=argspec(),
mutually_exclusive=mutually_exclusive, mutually_exclusive=mutually_exclusive,
supports_check_mode=True, supports_check_mode=True,
) )
from ansible_collections.kubernetes.core.plugins.module_utils.common import ( run_module(module)
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -5,7 +5,7 @@
# This is a job definition that runs for 10 minutes and won't gracefully # This is a job definition that runs for 10 minutes and won't gracefully
# shutdown. It allows us to test foreground vs background deletion. # shutdown. It allows us to test foreground vs background deletion.
job_definition: job_definition:
apiVersion: v1 apiVersion: batch/v1
kind: Job kind: Job
metadata: metadata:
name: "{{ gc_name }}" name: "{{ gc_name }}"

View File

@@ -97,6 +97,7 @@
- name: patch service using json merge patch - name: patch service using json merge patch
kubernetes.core.k8s: kubernetes.core.k8s:
kind: Deployment kind: Deployment
api_version: apps/v1
namespace: "{{ k8s_patch_namespace }}" namespace: "{{ k8s_patch_namespace }}"
name: "{{ k8s_merge }}" name: "{{ k8s_merge }}"
merge_type: merge_type:

View File

@@ -52,7 +52,7 @@
assert: assert:
that: that:
- patch_resource.changed - patch_resource.changed
- patch_resource.result.results | selectattr('warning', 'defined') | list | length == 1 - patch_resource.result.results | selectattr('warnings', 'defined') | list | length == 1
- name: Ensure namespace {{ patch_only_namespace[0] }} was patched correctly - name: Ensure namespace {{ patch_only_namespace[0] }} was patched correctly
kubernetes.core.k8s_info: kubernetes.core.k8s_info:

View File

@@ -150,7 +150,7 @@
- name: Reapply the earlier deployment - name: Reapply the earlier deployment
k8s: k8s:
definition: definition:
api_version: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: scale-deploy name: scale-deploy

View File

@@ -42,7 +42,7 @@
- assert: - assert:
that: that:
- k8s_no_validate is failed - k8s_no_validate is failed
- "k8s_no_validate.msg == 'kubernetes-validate python library is required to validate resources'" - "'Failed to import the required Python library (kubernetes-validate)' in k8s_no_validate.msg"
- file: - file:
path: "{{ virtualenv }}" path: "{{ virtualenv }}"

View File

@@ -192,9 +192,8 @@ def test_service_retrieve_existing_resource(mock_pod_resource_instance):
svc = K8sService(client, module) svc = K8sService(client, module)
results = svc.retrieve(Mock(), pod_definition) results = svc.retrieve(Mock(), pod_definition)
assert isinstance(results, dict) assert isinstance(results, ResourceInstance)
assert results["changed"] is False assert results.to_dict() == pod_definition
assert results["result"] == pod_definition
def test_service_retrieve_no_existing_resource(): def test_service_retrieve_no_existing_resource():
@@ -205,9 +204,7 @@ def test_service_retrieve_no_existing_resource():
svc = K8sService(client, module) svc = K8sService(client, module)
results = svc.retrieve(Mock(), pod_definition) results = svc.retrieve(Mock(), pod_definition)
assert isinstance(results, dict) assert results is None
assert results["changed"] is False
assert results["result"] == {}
def test_create_project_request(): def test_create_project_request():