Skip to content

Commit

Permalink
Add ability to use server_side_apply to utils.create_from_yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
dcmcand committed Jun 27, 2024
1 parent 139dc0c commit b40eaf2
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 50 deletions.
42 changes: 42 additions & 0 deletions kubernetes/e2e_test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,48 @@ def test_create_apps_deployment_from_yaml(self):
except ApiException:
continue

def test_create_apps_deployment_from_yaml_with_apply_is_idempotent(self):
"""
Should be able to create an apps/v1 deployment.
"""
k8s_client = client.api_client.ApiClient(configuration=self.config)
try:
utils.create_from_yaml(
k8s_client, self.path_prefix + "apps-deployment.yaml")
app_api = client.AppsV1Api(k8s_client)
dep = app_api.read_namespaced_deployment(name="nginx-app",
namespace="default")
self.assertIsNotNone(dep)
self.assertEqual("nginx-app", dep.metadata.name)
self.assertEqual("nginx:1.15.4", dep.spec.template.spec.containers[0].image)
self.assertEqual(80, dep.spec.template.spec.containers[0].ports[0].container_port)
self.assertEqual("nginx", dep.spec.template.spec.containers[0].name)
self.assertEqual("nginx", dep.spec.template.metadata.labels["app"])
self.assertEqual(3, dep.spec.replicas)

utils.create_from_yaml(
k8s_client, self.path_prefix + "apps-deployment.yaml", apply=True)
dep = app_api.read_namespaced_deployment(name="nginx-app",
namespace="default")
self.assertIsNotNone(dep)
self.assertEqual("nginx-app", dep.metadata.name)
self.assertEqual("nginx:1.15.4", dep.spec.template.spec.containers[0].image)
self.assertEqual(80, dep.spec.template.spec.containers[0].ports[0].container_port)
self.assertEqual("nginx", dep.spec.template.spec.containers[0].name)
self.assertEqual("nginx", dep.spec.template.metadata.labels["app"])
self.assertEqual(3, dep.spec.replicas)
except Exception as e:
self.fail(e)
finally:
while True:
try:
app_api.delete_namespaced_deployment(
name="nginx-app", namespace="default",
body={})
break
except ApiException:
continue

def test_create_apps_deployment_from_yaml_object(self):
"""
Should be able to pass YAML objects directly to helper function.
Expand Down
137 changes: 87 additions & 50 deletions kubernetes/utils/create_from_yaml.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2018 The Kubernetes Authors.
# Copyright 2019 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -13,23 +13,20 @@
# limitations under the License.


import re
import os
import re

import yaml

from kubernetes import client
from kubernetes.dynamic.client import DynamicClient

UPPER_FOLLOWED_BY_LOWER_RE = re.compile('(.)([A-Z][a-z]+)')
LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE = re.compile('([a-z0-9])([A-Z])')
UPPER_FOLLOWED_BY_LOWER_RE = re.compile("(.)([A-Z][a-z]+)")
LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE = re.compile("([a-z0-9])([A-Z])")


def create_from_directory(
k8s_client,
yaml_dir=None,
verbose=False,
namespace="default",
**kwargs):
k8s_client, yaml_dir=None, verbose=False, namespace="default", apply=False, **kwargs
):
"""
Perform an action from files from a directory. Pass True for verbose to
print confirmation information.
Expand All @@ -44,6 +41,7 @@ def create_from_directory(
the resource creation will fail. If the API object in
the yaml file already contains a namespace definition
this parameter has no effect.
apply: bool. If True, use server-side apply for creating resources.
Available parameters for creating <kind>:
:param async_req bool
Expand All @@ -65,27 +63,31 @@ def create_from_directory(
"""

if not yaml_dir:
raise ValueError(
'`yaml_dir` argument must be provided')
raise ValueError("`yaml_dir` argument must be provided")
elif not os.path.isdir(yaml_dir):
raise ValueError(
'`yaml_dir` argument must be a path to directory')
raise ValueError("`yaml_dir` argument must be a path to directory")

files = [os.path.join(yaml_dir, i) for i in os.listdir(yaml_dir)
if os.path.isfile(os.path.join(yaml_dir, i))]
files = [
os.path.join(yaml_dir, i)
for i in os.listdir(yaml_dir)
if os.path.isfile(os.path.join(yaml_dir, i))
]
if not files:
raise ValueError(
'`yaml_dir` contains no files')
raise ValueError("`yaml_dir` contains no files")

failures = []
k8s_objects_all = []

for file in files:
try:
k8s_objects = create_from_yaml(k8s_client, file,
verbose=verbose,
namespace=namespace,
**kwargs)
k8s_objects = create_from_yaml(
k8s_client,
file,
verbose=verbose,
namespace=namespace,
apply=apply,
**kwargs,
)
k8s_objects_all.append(k8s_objects)
except FailToCreateError as failure:
failures.extend(failure.api_exceptions)
Expand All @@ -95,12 +97,14 @@ def create_from_directory(


def create_from_yaml(
k8s_client,
yaml_file=None,
yaml_objects=None,
verbose=False,
namespace="default",
**kwargs):
k8s_client,
yaml_file=None,
yaml_objects=None,
verbose=False,
namespace="default",
apply=False,
**kwargs,
):
"""
Perform an action from a yaml file. Pass True for verbose to
print confirmation information.
Expand All @@ -116,6 +120,7 @@ def create_from_yaml(
the resource creation will fail. If the API object in
the yaml file already contains a namespace definition
this parameter has no effect.
apply: bool. If True, use server-side apply for creating resources.
Available parameters for creating <kind>:
:param async_req bool
Expand All @@ -136,16 +141,21 @@ def create_from_yaml(
instances for each object that failed to create.
"""

def create_with(objects):
def create_with(objects, apply=apply):
failures = []
k8s_objects = []
for yml_document in objects:
if yml_document is None:
continue
try:
created = create_from_dict(k8s_client, yml_document, verbose,
namespace=namespace,
**kwargs)
created = create_from_dict(
k8s_client,
yml_document,
verbose,
namespace=namespace,
apply=apply,
**kwargs,
)
k8s_objects.append(created)
except FailToCreateError as failure:
failures.extend(failure.api_exceptions)
Expand All @@ -164,14 +174,16 @@ class Loader(yaml.loader.SafeLoader):
elif yaml_file:
with open(os.path.abspath(yaml_file)) as f:
yml_document_all = yaml.load_all(f, Loader=Loader)
return create_with(yml_document_all)
return create_with(yml_document_all, apply)
else:
raise ValueError(
'One of `yaml_file` or `yaml_objects` arguments must be provided')
"One of `yaml_file` or `yaml_objects` arguments must be provided"
)


def create_from_dict(k8s_client, data, verbose=False, namespace='default',
**kwargs):
def create_from_dict(
k8s_client, data, verbose=False, namespace="default", apply=False, **kwargs
):
"""
Perform an action from a dictionary containing valid kubernetes
API object (i.e. List, Service, etc).
Expand All @@ -186,6 +198,7 @@ def create_from_dict(k8s_client, data, verbose=False, namespace='default',
the resource creation will fail. If the API object in
the yaml file already contains a namespace definition
this parameter has no effect.
apply: bool. If True, use server-side apply for creating resources.
Returns:
The created kubernetes API objects.
Expand All @@ -210,16 +223,22 @@ def create_from_dict(k8s_client, data, verbose=False, namespace='default',
yml_object["kind"] = kind
try:
created = create_from_yaml_single_item(
k8s_client, yml_object, verbose, namespace=namespace,
**kwargs)
k8s_client,
yml_object,
verbose,
namespace=namespace,
apply=apply,
**kwargs,
)
k8s_objects.append(created)
except client.rest.ApiException as api_exception:
api_exceptions.append(api_exception)
else:
# This is a single object. Call the single item method
try:
created = create_from_yaml_single_item(
k8s_client, data, verbose, namespace=namespace, **kwargs)
k8s_client, data, verbose, namespace=namespace, apply=apply, **kwargs
)
k8s_objects.append(created)
except client.rest.ApiException as api_exception:
api_exceptions.append(api_exception)
Expand All @@ -232,7 +251,23 @@ def create_from_dict(k8s_client, data, verbose=False, namespace='default',


def create_from_yaml_single_item(
k8s_client, yml_object, verbose=False, **kwargs):
k8s_client, yml_object, verbose=False, apply=False, **kwargs
):

kind = yml_object["kind"]
if apply is True:
apply_client = DynamicClient(k8s_client).resources.get(
api_version=yml_object["apiVersion"], kind=kind
)
resp = apply_client.server_side_apply(
body=yml_object, field_manager="python-client", **kwargs
)
if verbose:
msg = "{0} created.".format(kind)
if hasattr(resp, "status"):
msg += " status='{0}'".format(str(resp.status))
print(msg)
return resp
group, _, version = yml_object["apiVersion"].partition("/")
if version == "":
version = group
Expand All @@ -242,29 +277,30 @@ def create_from_yaml_single_item(
group = "".join(group.rsplit(".k8s.io", 1))
# convert group name from DNS subdomain format to
# python class name convention
group = "".join(word.capitalize() for word in group.split('.'))
group = "".join(word.capitalize() for word in group.split("."))
fcn_to_call = "{0}{1}Api".format(group, version.capitalize())
k8s_api = getattr(client, fcn_to_call)(k8s_client)
# Replace CamelCased action_type into snake_case
kind = yml_object["kind"]
kind = UPPER_FOLLOWED_BY_LOWER_RE.sub(r'\1_\2', kind)
kind = LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE.sub(r'\1_\2', kind).lower()
kind = UPPER_FOLLOWED_BY_LOWER_RE.sub(r"\1_\2", kind)
kind = LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE.sub(r"\1_\2", kind).lower()
# Expect the user to create namespaced objects more often
if hasattr(k8s_api, "create_namespaced_{0}".format(kind)):
# Decide which namespace we are going to put the object in,
# if any
if "namespace" in yml_object["metadata"]:
namespace = yml_object["metadata"]["namespace"]
kwargs['namespace'] = namespace
kwargs["namespace"] = namespace
resp = getattr(k8s_api, "create_namespaced_{0}".format(kind))(
body=yml_object, **kwargs)
body=yml_object, **kwargs
)
else:
kwargs.pop('namespace', None)
kwargs.pop("namespace", None)
resp = getattr(k8s_api, "create_{0}".format(kind))(
body=yml_object, **kwargs)
body=yml_object, **kwargs
)
if verbose:
msg = "{0} created.".format(kind)
if hasattr(resp, 'status'):
if hasattr(resp, "status"):
msg += " status='{0}'".format(str(resp.status))
print(msg)
return resp
Expand All @@ -283,5 +319,6 @@ def __str__(self):
msg = ""
for api_exception in self.api_exceptions:
msg += "Error from server ({0}): {1}".format(
api_exception.reason, api_exception.body)
api_exception.reason, api_exception.body
)
return msg

0 comments on commit b40eaf2

Please sign in to comment.