Skip to content

Commit c61c45e

Browse files
committed
1 parent 7d6a3b5 commit c61c45e

4 files changed

Lines changed: 301 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2018 The Kubernetes Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import absolute_import
16+
17+
from .create_from_yaml import (
18+
FailToCreateError, create_from_dict, create_from_yaml,
19+
create_from_yaml_single_item,
20+
)
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Copyright 2018 The Kubernetes Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import re
17+
from os import path
18+
19+
import yaml
20+
21+
from kubernetes_asyncio import client
22+
23+
24+
async def create_from_yaml(
25+
k8s_client,
26+
yaml_file,
27+
verbose=False,
28+
namespace="default",
29+
**kwargs):
30+
"""
31+
Perform an action from a yaml file. Pass True for verbose to
32+
print confirmation information.
33+
Input:
34+
yaml_file: string. Contains the path to yaml file.
35+
k8s_client: an ApiClient object, initialized with the client args.
36+
verbose: If True, print confirmation from the create action.
37+
Default is False.
38+
namespace: string. Contains the namespace to create all
39+
resources inside. The namespace must preexist otherwise
40+
the resource creation will fail. If the API object in
41+
the yaml file already contains a namespace definition
42+
this parameter has no effect.
43+
Returns:
44+
An k8s api object or list of apis objects created from YAML.
45+
When a single object is generated, return type is dependent
46+
on output_list.
47+
Throws a FailToCreateError exception if creation of any object
48+
fails with helpful messages from the server.
49+
Available parameters for creating <kind>:
50+
:param async_req bool
51+
:param bool include_uninitialized: If true, partially initialized
52+
resources are included in the response.
53+
:param str pretty: If 'true', then the output is pretty printed.
54+
:param str dry_run: When present, indicates that modifications
55+
should not be persisted. An invalid or unrecognized dryRun
56+
directive will result in an error response and no further
57+
processing of the request.
58+
Valid values are: - All: all dry run stages will be processed
59+
"""
60+
61+
with open(path.abspath(yaml_file)) as f:
62+
yml_document_all = yaml.safe_load_all(f)
63+
api_exceptions = []
64+
k8s_objects = []
65+
# Load all documents from a single YAML file
66+
for yml_document in yml_document_all:
67+
try:
68+
created = await create_from_dict(k8s_client, yml_document,
69+
verbose, namespace=namespace,
70+
**kwargs)
71+
k8s_objects.append(created)
72+
except FailToCreateError as failure:
73+
api_exceptions.append(failure)
74+
75+
# In case we have exceptions waiting for us, raise them
76+
if api_exceptions:
77+
raise FailToCreateError(api_exceptions)
78+
79+
return k8s_objects
80+
81+
82+
async def create_from_dict(
83+
k8s_client,
84+
data,
85+
verbose=False,
86+
namespace="default",
87+
**kwargs):
88+
"""
89+
Perform an action from a yaml file. Pass True for verbose to
90+
print confirmation information.
91+
Input:
92+
yaml_file: string. Contains the path to yaml file.
93+
data: a dictionary holding valid kubernetes objects
94+
verbose: If True, print confirmation from the create action.
95+
Default is False.
96+
namespace: string. Contains the namespace to create all
97+
resources inside. The namespace must preexist otherwise
98+
the resource creation will fail. If the API object in
99+
the yaml file already contains a namespace definition
100+
this parameter has no effect.
101+
Returns:
102+
An k8s api object or list of apis objects created from dict.
103+
When a single object is generated, return type is dependent
104+
on output_list.
105+
Throws a FailToCreateError exception if creation of any object
106+
fails with helpful messages from the server.
107+
Available parameters for creating <kind>:
108+
:param async_req bool
109+
:param bool include_uninitialized: If true, partially initialized
110+
resources are included in the response.
111+
:param str pretty: If 'true', then the output is pretty printed.
112+
:param str dry_run: When present, indicates that modifications
113+
should not be persisted. An invalid or unrecognized dryRun
114+
directive will result in an error response and no further
115+
processing of the request.
116+
Valid values are: - All: all dry run stages will be processed
117+
"""
118+
api_exceptions = []
119+
k8s_objects = []
120+
121+
# If it is a list type, will need to iterate its items
122+
if "List" in data["kind"]:
123+
# Could be "List" or "Pod/Service/...List"
124+
# This is a list type. iterate within its items
125+
kind = data["kind"].replace("List", "")
126+
for yml_object in data["items"]:
127+
# Mitigate cases when server returns a xxxList object
128+
# See kubernetes-client/python#586
129+
if kind != "":
130+
yml_object["apiVersion"] = data["apiVersion"]
131+
yml_object["kind"] = kind
132+
try:
133+
created = await create_from_yaml_single_item(
134+
k8s_client, yml_object, verbose, namespace, **kwargs)
135+
k8s_objects.append(created)
136+
except client.rest.ApiException as api_exception:
137+
api_exceptions.append(api_exception)
138+
else:
139+
# This is a single object. Call the single item method
140+
try:
141+
created = await create_from_yaml_single_item(
142+
k8s_client, data, verbose, namespace, **kwargs)
143+
k8s_objects.append(created)
144+
except client.rest.ApiException as api_exception:
145+
api_exceptions.append(api_exception)
146+
147+
if api_exceptions:
148+
raise FailToCreateError(api_exceptions)
149+
150+
return k8s_objects
151+
152+
153+
async def create_from_yaml_single_item(
154+
k8s_client,
155+
yml_object,
156+
verbose=False,
157+
namespace="default",
158+
**kwargs):
159+
group, _, version = yml_object["apiVersion"].partition("/")
160+
if version == "":
161+
version = group
162+
group = "core"
163+
# Take care for the case e.g. api_type is "apiextensions.k8s.io"
164+
# Only replace the last instance
165+
group = "".join(group.rsplit(".k8s.io", 1))
166+
# convert group name from DNS subdomain format to
167+
# python class name convention
168+
group = "".join(word.capitalize() for word in group.split('.'))
169+
fcn_to_call = "{0}{1}Api".format(group, version.capitalize())
170+
k8s_api = getattr(client, fcn_to_call)(k8s_client)
171+
# Replace CamelCased action_type into snake_case
172+
kind = yml_object["kind"]
173+
kind = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', kind)
174+
kind = re.sub('([a-z0-9])([A-Z])', r'\1_\2', kind).lower()
175+
# Decide which namespace we are going to put the object in,
176+
# if any
177+
if "namespace" in yml_object["metadata"]:
178+
namespace = yml_object["metadata"]["namespace"]
179+
# Expect the user to create namespaced objects more often
180+
if hasattr(k8s_api, "create_namespaced_{0}".format(kind)):
181+
resp = await getattr(k8s_api, "create_namespaced_{0}".format(kind))(
182+
body=yml_object, namespace=namespace, **kwargs)
183+
else:
184+
resp = await getattr(k8s_api, "create_{0}".format(kind))(
185+
body=yml_object, **kwargs)
186+
if verbose:
187+
print("{0} created. status='{1}'".format(kind, str(resp.status)))
188+
return resp
189+
190+
191+
class FailToCreateError(Exception):
192+
"""
193+
An exception class for handling error if an error occurred when
194+
handling a yaml file.
195+
"""
196+
197+
def __init__(self, api_exceptions):
198+
self.api_exceptions = api_exceptions
199+
200+
def __str__(self):
201+
msg = ""
202+
for api_exception in self.api_exceptions:
203+
msg += "Error from server ({0}): {1}".format(
204+
api_exception.reason, api_exception.body)
205+
return msg
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2019 The Kubernetes Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest import IsolatedAsyncioTestCase
16+
from unittest.mock import AsyncMock
17+
18+
from kubernetes_asyncio.utils import create_from_dict, create_from_yaml
19+
20+
21+
class CreateFromYamlTest(IsolatedAsyncioTestCase):
22+
23+
async def test_create_from_yaml(self):
24+
api_client = AsyncMock()
25+
api_client.call_api = AsyncMock()
26+
api_client.call_api.return_value = 'mock-value'
27+
28+
created = await create_from_yaml(api_client, 'kubernetes_asyncio/utils/nginx-deployment.yaml')
29+
30+
# simple check for api call
31+
self.assertEqual(api_client.call_api.call_args[0][0],
32+
'/apis/apps/v1/namespaces/{namespace}/deployments')
33+
34+
# returned values
35+
self.assertEqual(created, [['mock-value']])
36+
37+
async def test_create_from_dict(self):
38+
api_client = AsyncMock()
39+
api_client.call_api = AsyncMock()
40+
api_client.call_api.return_value = 'mock-value'
41+
42+
created = await create_from_dict(api_client, {
43+
'apiVersion': 'apps/v1',
44+
'kind': 'Deployment',
45+
'metadata': {
46+
'name': 'nginx-deployment'},
47+
'spec': {
48+
'replicas': 3,
49+
}
50+
})
51+
52+
# simple check for api call
53+
self.assertEqual(api_client.call_api.call_args[0][0],
54+
'/apis/apps/v1/namespaces/{namespace}/deployments')
55+
56+
# returned values
57+
self.assertEqual(created, ['mock-value'])
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: nginx-deployment
5+
spec:
6+
replicas: 3
7+
selector:
8+
matchLabels:
9+
app: nginx
10+
template:
11+
metadata:
12+
labels:
13+
app: nginx
14+
spec:
15+
containers:
16+
- name: nginx
17+
image: nginx:1.7.9
18+
ports:
19+
- containerPort: 80

0 commit comments

Comments
 (0)