Skip to content

Commit 3e56bdf

Browse files
PidgeyBEtomplus
authored andcommitted
Add create_form_yaml() functionality (#76)
1 parent 594a639 commit 3e56bdf

File tree

9 files changed

+281
-2
lines changed

9 files changed

+281
-2
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 os import path
16+
17+
from kubernetes_asyncio import client, config, utils
18+
19+
20+
def main():
21+
# Configs can be set in Configuration class directly or using helper
22+
# utility. If no argument provided, the config will be loaded from
23+
# default location.
24+
await config.load_kube_config()
25+
k8s_client = client.ApiClient()
26+
await utils.create_from_yaml(k8s_client, "nginx-deployment.yaml")
27+
k8s_api = client.ExtensionsV1beta1Api(k8s_client)
28+
deps = await k8s_api.read_namespaced_deployment("nginx-deployment",
29+
"default")
30+
print("Deployment {0} created".format(deps.metadata.name))
31+
32+
33+
if __name__ == '__main__':
34+
main()

examples/nginx-deployment.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: extensions/v1beta1
2+
kind: Deployment
3+
metadata:
4+
name: nginx-deployment
5+
spec:
6+
replicas: 3
7+
template:
8+
metadata:
9+
labels:
10+
app: nginx
11+
spec:
12+
containers:
13+
- name: nginx
14+
image: nginx:1.7.9
15+
ports:
16+
- containerPort: 80

kubernetes_asyncio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
import kubernetes_asyncio.config
2121
import kubernetes_asyncio.watch
2222
import kubernetes_asyncio.stream
23+
import kubernetes_asyncio.utils

kubernetes_asyncio/e2e_test/test_extensions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
import os
16+
1517
import uuid
1618

1719
import asynctest
1820
import yaml
1921

2022
from kubernetes_asyncio.client import api_client
23+
from kubernetes_asyncio import utils
2124
from kubernetes_asyncio.client.api import extensions_v1beta1_api
2225
from kubernetes_asyncio.client.models import v1_delete_options
2326
from kubernetes_asyncio.e2e_test import base
@@ -59,6 +62,38 @@ async def test_create_deployment(self):
5962
options = v1_delete_options.V1DeleteOptions()
6063
resp = await api.delete_namespaced_deployment(name, 'default', body=options)
6164

65+
async def test_create_deployment_from_yaml_file(self):
66+
client = api_client.ApiClient(configuration=self.config)
67+
api = extensions_v1beta1_api.ExtensionsV1beta1Api(client)
68+
name = 'nginx-deployment-' + str(uuid.uuid4())
69+
tempfile = 'temp.yaml'
70+
deployment = '''apiVersion: extensions/v1beta1
71+
kind: Deployment
72+
metadata:
73+
name: %s
74+
spec:
75+
replicas: 3
76+
template:
77+
metadata:
78+
labels:
79+
app: nginx
80+
spec:
81+
containers:
82+
- name: nginx
83+
image: nginx:1.7.9
84+
ports:
85+
- containerPort: 80
86+
'''
87+
with open(tempfile, 'w') as f:
88+
f.write(deployment % name)
89+
resp = await utils.create_from_yaml(client, tempfile)
90+
os.remove(tempfile)
91+
resp = await api.read_namespaced_deployment(name, 'default')
92+
self.assertIsNotNone(resp)
93+
94+
options = v1_delete_options.V1DeleteOptions()
95+
resp = await api.delete_namespaced_deployment(name, 'default', body=options)
96+
6297
async def test_create_daemonset(self):
6398
client = api_client.ApiClient(configuration=self.config)
6499
api = extensions_v1beta1_api.ExtensionsV1beta1Api(client)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# coding: utf-8
2+
3+
from __future__ import absolute_import
4+
5+
import unittest
6+
7+
from kubernetes_asyncio.utils import create_from_yaml
8+
9+
import kubernetes_asyncio.client
10+
from kubernetes_asyncio.client.rest import ApiException
11+
12+
13+
class TestCreateFromYAML(unittest.TestCase):
14+
"""ExtensionsV1beta1IDRange unit test stubs"""
15+
16+
def setUp(self):
17+
pass
18+
19+
def tearDown(self):
20+
pass
21+
22+
def testCreateFromYAML(self):
23+
"""Test testCreateFromYAML"""
24+
# The only function there is will deploy a yaml straight to a kubernetes cluster
25+
# So there is not much to unit test... There is a e2e test doing this.
26+
pass
27+
28+
29+
if __name__ == '__main__':
30+
unittest.main()

kubernetes_asyncio/utils/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 FailToCreateError, create_from_yaml
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
# Load all documents from a single YAML file
65+
for yml_document in yml_document_all:
66+
# If it is a list type, will need to iterate its items
67+
if "List" in yml_document["kind"]:
68+
# Could be "List" or "Pod/Service/...List"
69+
# This is a list type. iterate within its items
70+
kind = yml_document["kind"].replace("List", "")
71+
for yml_object in yml_document["items"]:
72+
# Mitigate cases when server returns a xxxList object
73+
# See kubernetes-client/python#586
74+
if kind is not "":
75+
yml_object["apiVersion"] = yml_document["apiVersion"]
76+
yml_object["kind"] = kind
77+
try:
78+
await create_from_yaml_single_item(
79+
k8s_client, yml_object, verbose, namespace, **kwargs)
80+
except client.rest.ApiException as api_exception:
81+
api_exceptions.append(api_exception)
82+
else:
83+
# This is a single object. Call the single item method
84+
try:
85+
await create_from_yaml_single_item(
86+
k8s_client, yml_document, verbose, namespace, **kwargs)
87+
except client.rest.ApiException as api_exception:
88+
api_exceptions.append(api_exception)
89+
# In case we have exceptions waiting for us, raise them
90+
if api_exceptions:
91+
raise FailToCreateError(api_exceptions)
92+
93+
94+
async def create_from_yaml_single_item(
95+
k8s_client,
96+
yml_object,
97+
verbose=False,
98+
namespace="default",
99+
**kwargs):
100+
group, _, version = yml_object["apiVersion"].partition("/")
101+
if version == "":
102+
version = group
103+
group = "core"
104+
# Take care for the case e.g. api_type is "apiextensions.k8s.io"
105+
# Only replace the last instance
106+
group = "".join(group.rsplit(".k8s.io", 1))
107+
# convert group name from DNS subdomain format to
108+
# python class name convention
109+
group = "".join(word.capitalize() for word in group.split('.'))
110+
fcn_to_call = "{0}{1}Api".format(group, version.capitalize())
111+
k8s_api = getattr(client, fcn_to_call)(k8s_client)
112+
# Replace CamelCased action_type into snake_case
113+
kind = yml_object["kind"]
114+
kind = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', kind)
115+
kind = re.sub('([a-z0-9])([A-Z])', r'\1_\2', kind).lower()
116+
# Decide which namespace we are going to put the object in,
117+
# if any
118+
if "namespace" in yml_object["metadata"]:
119+
namespace = yml_object["metadata"]["namespace"]
120+
# Expect the user to create namespaced objects more often
121+
if hasattr(k8s_api, "create_namespaced_{0}".format(kind)):
122+
resp = await getattr(k8s_api, "create_namespaced_{0}".format(kind))(
123+
body=yml_object, namespace=namespace, **kwargs)
124+
else:
125+
resp = await getattr(k8s_api, "create_{0}".format(kind))(
126+
body=yml_object, **kwargs)
127+
if verbose:
128+
print("{0} created. status='{1}'".format(kind, str(resp.status)))
129+
130+
131+
class FailToCreateError(Exception):
132+
"""
133+
An exception class for handling error if an error occurred when
134+
handling a yaml file.
135+
"""
136+
137+
def __init__(self, api_exceptions):
138+
self.api_exceptions = api_exceptions
139+
140+
def __str__(self):
141+
msg = ""
142+
for api_exception in self.api_exceptions:
143+
msg += "Error from server ({0}): {1}".format(
144+
api_exception.reason, api_exception.body)
145+
return msg

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
'kubernetes_asyncio.watch',
5555
'kubernetes_asyncio.client.api',
5656
'kubernetes_asyncio.stream',
57-
'kubernetes_asyncio.client.models'],
57+
'kubernetes_asyncio.client.models',
58+
'kubernetes_asyncio.utils'],
5859
include_package_data=True,
5960
long_description="""\
6061
Python client for kubernetes http://kubernetes.io/

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ commands =
3737
[testenv:coverage]
3838
commands =
3939
python -V
40-
nosetests --with-coverage --cover-package=kubernetes_asyncio.config,kubernetes_asyncio.watch,kubernetes_asyncio.stream --cover-tests
40+
nosetests --with-coverage --cover-package=kubernetes_asyncio.config,kubernetes_asyncio.watch,kubernetes_asyncio.stream,kubernetes_asyncio.utils --cover-tests
4141

4242
[testenv:codecov]
4343
commands =

0 commit comments

Comments
 (0)