Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate k8s #311

Merged
merged 23 commits into from
Feb 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion molecule/default/tasks/gc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# This is a job definition that runs for 10 minutes and won't gracefully
# shutdown. It allows us to test foreground vs background deletion.
job_definition:
apiVersion: v1
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ gc_name }}"
Expand Down
2 changes: 1 addition & 1 deletion molecule/default/tasks/json_patch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
block:
- name: Ensure namespace exists
kubernetes.core.k8s:
kind: namespace
kind: Namespace
name: "{{ namespace }}"

- name: Create a simple pod
Expand Down
3 changes: 2 additions & 1 deletion molecule/default/tasks/merge_type.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

- name: Ensure the namespace exist
kubernetes.core.k8s:
kind: namespace
kind: Namespace
name: "{{ k8s_patch_namespace }}"


Expand Down Expand Up @@ -103,6 +103,7 @@
- name: patch service using json merge patch
kubernetes.core.k8s:
kind: Deployment
api_version: apps/v1
namespace: "{{ k8s_patch_namespace }}"
name: "{{ k8s_merge }}"
merge_type:
Expand Down
2 changes: 1 addition & 1 deletion molecule/default/tasks/patched.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
assert:
that:
- 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.first }} was patched correctly
kubernetes.core.k8s_info:
Expand Down
2 changes: 1 addition & 1 deletion molecule/default/tasks/scale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
- name: Reapply the earlier deployment
k8s:
definition:
api_version: apps/v1
apiVersion: apps/v1
kind: Deployment
metadata:
name: scale-deploy
Expand Down
2 changes: 1 addition & 1 deletion molecule/default/tasks/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
- assert:
that:
- 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:
path: "{{ virtualenv }}"
Expand Down
6 changes: 4 additions & 2 deletions plugins/module_utils/k8s/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,10 @@ def _ensure_dry_run(self, params: Dict) -> Dict:
params["dry_run"] = True
return params

def validate(self, resource, **params):
pass
def validate(
self, resource, version: Optional[str] = None, strict: Optional[bool] = False
):
return self.client.validate(resource, version, strict)

def get(self, resource, **params):
return resource.get(**params)
Expand Down
7 changes: 7 additions & 0 deletions plugins/module_utils/k8s/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ def gather_versions() -> dict:
except ImportError:
pass

try:
import kubernetes_validate

versions["kubernetes-validate"] = kubernetes_validate.__version__
except ImportError:
pass

try:
import yaml

Expand Down
51 changes: 36 additions & 15 deletions plugins/module_utils/k8s/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from typing import Dict

from ansible.module_utils._text import to_native

from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
get_api_client,
)
Expand All @@ -15,7 +17,6 @@
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
CoreException,
)

from ansible_collections.kubernetes.core.plugins.module_utils.selector import (
LabelSelectorFilter,
)
Expand All @@ -42,42 +43,54 @@ def _prepend_resource_info(resource, msg):

def run_module(module) -> None:
results = []

changed = False
client = get_api_client(module)
svc = K8sService(client, module)
definitions = create_definitions(module.params)

for definition in definitions:
module.warnings = []
result = {"changed": False, "result": {}, "warnings": []}
warnings = []

if module.params["validate"] is not None:
module.warnings = validate(client, module, definition)
if module.params.get("validate") is not None:
warnings = validate(client, module, definition)

try:
result = perform_action(svc, definition, module.params)
except CoreException as e:
if module.warnings:
e["msg"] += "\n" + "\n ".join(module.warnings)
msg = to_native(e)
if warnings:
msg += "\n" + "\n ".join(warnings)
if module.params.get("continue_on_error"):
result = {"error": "{0}".format(e)}
result["error"] = {"msg": msg}
else:
module.fail_json(msg=e)
if module.warnings:
result["warnings"] = module.warnings
module.fail_json(msg=msg)

if warnings:
result.setdefault("warnings", [])
result["warnings"] += warnings

changed |= result["changed"]
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:
origin_name = definition["metadata"].get("name")
namespace = definition["metadata"].get("namespace")
label_selectors = params.get("label_selectors")
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)

if state == "absent":
Expand All @@ -91,7 +104,7 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict:
result["msg"] = (
"resource 'kind={kind},name={name},namespace={namespace}' "
"filtered by label_selectors.".format(
kind=definition["kind"], name=origin_name, namespace=namespace,
kind=kind, name=origin_name, namespace=namespace,
)
)
return result
Expand All @@ -100,6 +113,14 @@ def perform_action(svc, definition: Dict, params: Dict) -> Dict:
result = svc.apply(resource, definition, existing)
result["method"] = "apply"
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["method"] = "create"
elif params.get("force", False):
Expand Down
73 changes: 48 additions & 25 deletions plugins/module_utils/k8s/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,29 +171,35 @@ def patch_resource(
msg = "Failed to patch object: {0}".format(reason)
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)
append_hash = self.module.params.get("append_hash", False)
name = definition["metadata"].get("name")
generate_name = definition["metadata"].get("generateName")
namespace = definition["metadata"].get("namespace")
label_selectors = self.module.params.get("label_selectors")
results = {
"changed": False,
"result": {},
}
existing = None
existing: ResourceInstance = None

try:
# ignore append_hash for resources other than ConfigMap and Secret
if append_hash and definition["kind"] in ["ConfigMap", "Secret"]:
name = "%s-%s" % (name, generate_hash(definition))
definition["metadata"]["name"] = name
params = dict(name=name)
if name:
name = "%s-%s" % (name, generate_hash(definition))
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:
params["namespace"] = namespace
if 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):
pass
except ForbiddenError as e:
Expand All @@ -210,10 +216,7 @@ def retrieve(self, resource: Resource, definition: Dict) -> Dict:
msg = "Failed to retrieve requested object: {0}".format(reason)
raise CoreException(msg) from e

if existing:
results["result"] = existing.to_dict()

return results
return existing

def find(
self,
Expand Down Expand Up @@ -345,10 +348,12 @@ def create(self, resource: Resource, definition: Dict) -> Dict:
results["result"] = k8s_obj

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)
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
Expand All @@ -358,7 +363,7 @@ def create(self, resource: Resource, definition: Dict) -> Dict:
'"{0}" "{1}": Resource creation timed out'.format(
definition["kind"], origin_name
),
**results
results,
)

return results
Expand Down Expand Up @@ -431,7 +436,7 @@ def apply(
'"{0}" "{1}": Resource apply timed out'.format(
definition["kind"], origin_name
),
**results
results,
)

return results
Expand Down Expand Up @@ -493,7 +498,7 @@ def replace(
'"{0}" "{1}": Resource replacement timed out'.format(
definition["kind"], origin_name
),
**results
results,
)

return results
Expand All @@ -514,16 +519,25 @@ def update(
wait_condition = self.module.params["wait_condition"]
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))
else:
exception = None
for merge_type in self.module.params.get("merge_type") or [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure with the crd test is coming from here. We need to carry over the logic to test multiple merge types, since we are right now failing on the first try. So something like this in the else block:

exception = None
for merge_type in self.module.params.get("merge_type") or ["strategic-merge", "merge",]:
    try:
        k8s_obj = self.patch_resource(...)
        exception = None
    except CoreException as e:
        exception = e
        continue
    break
if exception:
    raise exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the changed I make to the crd.yml file and added this block. But it keeps failing.

TASK [Patch custom resource definition] ****************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "Failed to patch object: b'{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"the body of the request was in an unknown format - accepted media types include: application/json-patch+json, application/merge-patch+json, application/apply-patch+yaml\",\"reason\":\"UnsupportedMediaType\",\"code\":415}\\n'"}
...ignoring

TASK [Assert that recreating crd is as expected] *******************************
fatal: [localhost]: FAILED! => {
    "assertion": "recreate_crd is not failed",
    "changed": false,
    "evaluated_to": false,
    "msg": "Assertion failed"
}

"strategic-merge",
"merge",
]:
k8s_obj = self.patch_resource(
resource, definition, name, namespace, merge_type=merge_type,
)
try:
k8s_obj = self.patch_resource(
resource, definition, name, namespace, merge_type=merge_type,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to add exception = None right here, otherwise the behavior is effectively the same as before this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! Thanks, missed that!

exception = None
except CoreException as e:
exception = e
continue
break
if exception:
raise exception

success = True
results["result"] = k8s_obj
Expand All @@ -545,7 +559,7 @@ def update(
'"{0}" "{1}": Resource update timed out'.format(
definition["kind"], origin_name
),
**results
results,
)

return results
Expand Down Expand Up @@ -581,6 +595,15 @@ def _empty_resource_list() -> bool:
if self.module.check_mode and not self.client.dry_run:
return results
else:
if name:
params["name"] = name

if namespace:
params["namespace"] = namespace

if label_selectors:
params["label_selector"] = ",".join(label_selectors)

if delete_options:
body = {
"apiVersion": "v1",
Expand Down Expand Up @@ -614,7 +637,7 @@ def _empty_resource_list() -> bool:
'"{0}" "{1}": Resource deletion timed out'.format(
definition["kind"], origin_name
),
**results
results,
)

return results
2 changes: 1 addition & 1 deletion plugins/module_utils/k8s/waiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def wait(
try:
response = self.client.get(self.resource, **params)
except NotFoundError:
pass
response = None
if self.predicate(response):
break
if response:
Expand Down
Loading