Skip to content

Commit 18fe668

Browse files
authored
Merge pull request #2252 from dcmcand/master
Add ability to use server_side_apply to utils.create_from_yaml
2 parents 0411039 + b40eaf2 commit 18fe668

File tree

2 files changed

+129
-50
lines changed

2 files changed

+129
-50
lines changed

kubernetes/e2e_test/test_utils.py

+42
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,48 @@ def test_create_apps_deployment_from_yaml(self):
7171
except ApiException:
7272
continue
7373

74+
def test_create_apps_deployment_from_yaml_with_apply_is_idempotent(self):
75+
"""
76+
Should be able to create an apps/v1 deployment.
77+
"""
78+
k8s_client = client.api_client.ApiClient(configuration=self.config)
79+
try:
80+
utils.create_from_yaml(
81+
k8s_client, self.path_prefix + "apps-deployment.yaml")
82+
app_api = client.AppsV1Api(k8s_client)
83+
dep = app_api.read_namespaced_deployment(name="nginx-app",
84+
namespace="default")
85+
self.assertIsNotNone(dep)
86+
self.assertEqual("nginx-app", dep.metadata.name)
87+
self.assertEqual("nginx:1.15.4", dep.spec.template.spec.containers[0].image)
88+
self.assertEqual(80, dep.spec.template.spec.containers[0].ports[0].container_port)
89+
self.assertEqual("nginx", dep.spec.template.spec.containers[0].name)
90+
self.assertEqual("nginx", dep.spec.template.metadata.labels["app"])
91+
self.assertEqual(3, dep.spec.replicas)
92+
93+
utils.create_from_yaml(
94+
k8s_client, self.path_prefix + "apps-deployment.yaml", apply=True)
95+
dep = app_api.read_namespaced_deployment(name="nginx-app",
96+
namespace="default")
97+
self.assertIsNotNone(dep)
98+
self.assertEqual("nginx-app", dep.metadata.name)
99+
self.assertEqual("nginx:1.15.4", dep.spec.template.spec.containers[0].image)
100+
self.assertEqual(80, dep.spec.template.spec.containers[0].ports[0].container_port)
101+
self.assertEqual("nginx", dep.spec.template.spec.containers[0].name)
102+
self.assertEqual("nginx", dep.spec.template.metadata.labels["app"])
103+
self.assertEqual(3, dep.spec.replicas)
104+
except Exception as e:
105+
self.fail(e)
106+
finally:
107+
while True:
108+
try:
109+
app_api.delete_namespaced_deployment(
110+
name="nginx-app", namespace="default",
111+
body={})
112+
break
113+
except ApiException:
114+
continue
115+
74116
def test_create_apps_deployment_from_yaml_object(self):
75117
"""
76118
Should be able to pass YAML objects directly to helper function.

kubernetes/utils/create_from_yaml.py

+87-50
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2018 The Kubernetes Authors.
1+
# Copyright 2019 The Kubernetes Authors.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -13,23 +13,20 @@
1313
# limitations under the License.
1414

1515

16-
import re
1716
import os
17+
import re
1818

1919
import yaml
20-
2120
from kubernetes import client
21+
from kubernetes.dynamic.client import DynamicClient
2222

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

2626

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

6765
if not yaml_dir:
68-
raise ValueError(
69-
'`yaml_dir` argument must be provided')
66+
raise ValueError("`yaml_dir` argument must be provided")
7067
elif not os.path.isdir(yaml_dir):
71-
raise ValueError(
72-
'`yaml_dir` argument must be a path to directory')
68+
raise ValueError("`yaml_dir` argument must be a path to directory")
7369

74-
files = [os.path.join(yaml_dir, i) for i in os.listdir(yaml_dir)
75-
if os.path.isfile(os.path.join(yaml_dir, i))]
70+
files = [
71+
os.path.join(yaml_dir, i)
72+
for i in os.listdir(yaml_dir)
73+
if os.path.isfile(os.path.join(yaml_dir, i))
74+
]
7675
if not files:
77-
raise ValueError(
78-
'`yaml_dir` contains no files')
76+
raise ValueError("`yaml_dir` contains no files")
7977

8078
failures = []
8179
k8s_objects_all = []
8280

8381
for file in files:
8482
try:
85-
k8s_objects = create_from_yaml(k8s_client, file,
86-
verbose=verbose,
87-
namespace=namespace,
88-
**kwargs)
83+
k8s_objects = create_from_yaml(
84+
k8s_client,
85+
file,
86+
verbose=verbose,
87+
namespace=namespace,
88+
apply=apply,
89+
**kwargs,
90+
)
8991
k8s_objects_all.append(k8s_objects)
9092
except FailToCreateError as failure:
9193
failures.extend(failure.api_exceptions)
@@ -95,12 +97,14 @@ def create_from_directory(
9597

9698

9799
def create_from_yaml(
98-
k8s_client,
99-
yaml_file=None,
100-
yaml_objects=None,
101-
verbose=False,
102-
namespace="default",
103-
**kwargs):
100+
k8s_client,
101+
yaml_file=None,
102+
yaml_objects=None,
103+
verbose=False,
104+
namespace="default",
105+
apply=False,
106+
**kwargs,
107+
):
104108
"""
105109
Perform an action from a yaml file. Pass True for verbose to
106110
print confirmation information.
@@ -116,6 +120,7 @@ def create_from_yaml(
116120
the resource creation will fail. If the API object in
117121
the yaml file already contains a namespace definition
118122
this parameter has no effect.
123+
apply: bool. If True, use server-side apply for creating resources.
119124
120125
Available parameters for creating <kind>:
121126
:param async_req bool
@@ -136,16 +141,21 @@ def create_from_yaml(
136141
instances for each object that failed to create.
137142
"""
138143

139-
def create_with(objects):
144+
def create_with(objects, apply=apply):
140145
failures = []
141146
k8s_objects = []
142147
for yml_document in objects:
143148
if yml_document is None:
144149
continue
145150
try:
146-
created = create_from_dict(k8s_client, yml_document, verbose,
147-
namespace=namespace,
148-
**kwargs)
151+
created = create_from_dict(
152+
k8s_client,
153+
yml_document,
154+
verbose,
155+
namespace=namespace,
156+
apply=apply,
157+
**kwargs,
158+
)
149159
k8s_objects.append(created)
150160
except FailToCreateError as failure:
151161
failures.extend(failure.api_exceptions)
@@ -164,14 +174,16 @@ class Loader(yaml.loader.SafeLoader):
164174
elif yaml_file:
165175
with open(os.path.abspath(yaml_file)) as f:
166176
yml_document_all = yaml.load_all(f, Loader=Loader)
167-
return create_with(yml_document_all)
177+
return create_with(yml_document_all, apply)
168178
else:
169179
raise ValueError(
170-
'One of `yaml_file` or `yaml_objects` arguments must be provided')
180+
"One of `yaml_file` or `yaml_objects` arguments must be provided"
181+
)
171182

172183

173-
def create_from_dict(k8s_client, data, verbose=False, namespace='default',
174-
**kwargs):
184+
def create_from_dict(
185+
k8s_client, data, verbose=False, namespace="default", apply=False, **kwargs
186+
):
175187
"""
176188
Perform an action from a dictionary containing valid kubernetes
177189
API object (i.e. List, Service, etc).
@@ -186,6 +198,7 @@ def create_from_dict(k8s_client, data, verbose=False, namespace='default',
186198
the resource creation will fail. If the API object in
187199
the yaml file already contains a namespace definition
188200
this parameter has no effect.
201+
apply: bool. If True, use server-side apply for creating resources.
189202
190203
Returns:
191204
The created kubernetes API objects.
@@ -210,16 +223,22 @@ def create_from_dict(k8s_client, data, verbose=False, namespace='default',
210223
yml_object["kind"] = kind
211224
try:
212225
created = create_from_yaml_single_item(
213-
k8s_client, yml_object, verbose, namespace=namespace,
214-
**kwargs)
226+
k8s_client,
227+
yml_object,
228+
verbose,
229+
namespace=namespace,
230+
apply=apply,
231+
**kwargs,
232+
)
215233
k8s_objects.append(created)
216234
except client.rest.ApiException as api_exception:
217235
api_exceptions.append(api_exception)
218236
else:
219237
# This is a single object. Call the single item method
220238
try:
221239
created = create_from_yaml_single_item(
222-
k8s_client, data, verbose, namespace=namespace, **kwargs)
240+
k8s_client, data, verbose, namespace=namespace, apply=apply, **kwargs
241+
)
223242
k8s_objects.append(created)
224243
except client.rest.ApiException as api_exception:
225244
api_exceptions.append(api_exception)
@@ -232,7 +251,23 @@ def create_from_dict(k8s_client, data, verbose=False, namespace='default',
232251

233252

234253
def create_from_yaml_single_item(
235-
k8s_client, yml_object, verbose=False, **kwargs):
254+
k8s_client, yml_object, verbose=False, apply=False, **kwargs
255+
):
256+
257+
kind = yml_object["kind"]
258+
if apply is True:
259+
apply_client = DynamicClient(k8s_client).resources.get(
260+
api_version=yml_object["apiVersion"], kind=kind
261+
)
262+
resp = apply_client.server_side_apply(
263+
body=yml_object, field_manager="python-client", **kwargs
264+
)
265+
if verbose:
266+
msg = "{0} created.".format(kind)
267+
if hasattr(resp, "status"):
268+
msg += " status='{0}'".format(str(resp.status))
269+
print(msg)
270+
return resp
236271
group, _, version = yml_object["apiVersion"].partition("/")
237272
if version == "":
238273
version = group
@@ -242,29 +277,30 @@ def create_from_yaml_single_item(
242277
group = "".join(group.rsplit(".k8s.io", 1))
243278
# convert group name from DNS subdomain format to
244279
# python class name convention
245-
group = "".join(word.capitalize() for word in group.split('.'))
280+
group = "".join(word.capitalize() for word in group.split("."))
246281
fcn_to_call = "{0}{1}Api".format(group, version.capitalize())
247282
k8s_api = getattr(client, fcn_to_call)(k8s_client)
248283
# Replace CamelCased action_type into snake_case
249-
kind = yml_object["kind"]
250-
kind = UPPER_FOLLOWED_BY_LOWER_RE.sub(r'\1_\2', kind)
251-
kind = LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE.sub(r'\1_\2', kind).lower()
284+
kind = UPPER_FOLLOWED_BY_LOWER_RE.sub(r"\1_\2", kind)
285+
kind = LOWER_OR_NUM_FOLLOWED_BY_UPPER_RE.sub(r"\1_\2", kind).lower()
252286
# Expect the user to create namespaced objects more often
253287
if hasattr(k8s_api, "create_namespaced_{0}".format(kind)):
254288
# Decide which namespace we are going to put the object in,
255289
# if any
256290
if "namespace" in yml_object["metadata"]:
257291
namespace = yml_object["metadata"]["namespace"]
258-
kwargs['namespace'] = namespace
292+
kwargs["namespace"] = namespace
259293
resp = getattr(k8s_api, "create_namespaced_{0}".format(kind))(
260-
body=yml_object, **kwargs)
294+
body=yml_object, **kwargs
295+
)
261296
else:
262-
kwargs.pop('namespace', None)
297+
kwargs.pop("namespace", None)
263298
resp = getattr(k8s_api, "create_{0}".format(kind))(
264-
body=yml_object, **kwargs)
299+
body=yml_object, **kwargs
300+
)
265301
if verbose:
266302
msg = "{0} created.".format(kind)
267-
if hasattr(resp, 'status'):
303+
if hasattr(resp, "status"):
268304
msg += " status='{0}'".format(str(resp.status))
269305
print(msg)
270306
return resp
@@ -283,5 +319,6 @@ def __str__(self):
283319
msg = ""
284320
for api_exception in self.api_exceptions:
285321
msg += "Error from server ({0}): {1}".format(
286-
api_exception.reason, api_exception.body)
322+
api_exception.reason, api_exception.body
323+
)
287324
return msg

0 commit comments

Comments
 (0)