Repository: tomplus/kubernetes_asyncio
Pull request #76 (2019-07-04 08:06:22)

Add create_form_yaml() functionality

codecov-io: # [Codecov](https://codecov.io/gh/tomplus/kubernetes_asyncio/pull/76?src=pr&el=h1) Report
> Merging [#76](https://codecov.io/gh/tomplus/kubernetes_asyncio/pull/76?src=pr&el=desc) into [master](https://codecov.io/gh/tomplus/kubernetes_asyncio/commit/594a639e2e202f20fb3700d014593bde26cdd15f?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/tomplus/kubernetes_asyncio/pull/76/graphs/tree.svg?width=650&token=0Dvm4fqopq&height=150&src=pr)](https://codecov.io/gh/tomplus/kubernetes_asyncio/pull/76?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master      #76   +/-   ##
=======================================
  Coverage   94.78%   94.78%           
=======================================
  Files          20       20           
  Lines        1458     1458           
=======================================
  Hits         1382     1382           
  Misses         76       76
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/tomplus/kubernetes_asyncio/pull/76?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tomplus/kubernetes_asyncio/pull/76?src=pr&el=footer). Last update [594a639...a89a621](https://codecov.io/gh/tomplus/kubernetes_asyncio/pull/76?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

PidgeyBE: I'm not sure how to write unit tests... Cause it's simply about deploying a yaml file to a k8s cluster...
tomplus: @PidgeyBE Good job, thanks!

Patch:
diff --git a/examples/create_deployment_from_yaml.py b/examples/create_deployment_from_yaml.py
new file mode 100644
index 00000000..23d2c865
--- /dev/null
+++ b/examples/create_deployment_from_yaml.py
@@ -0,0 +1,34 @@
+# Copyright 2018 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.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path
+
+from kubernetes_asyncio import client, config, utils
+
+
+def main():
+    # Configs can be set in Configuration class directly or using helper
+    # utility. If no argument provided, the config will be loaded from
+    # default location.
+    await config.load_kube_config()
+    k8s_client = client.ApiClient()
+    await utils.create_from_yaml(k8s_client, "nginx-deployment.yaml")
+    k8s_api = client.ExtensionsV1beta1Api(k8s_client)
+    deps = await k8s_api.read_namespaced_deployment("nginx-deployment",
+                                                    "default")
+    print("Deployment {0} created".format(deps.metadata.name))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/examples/nginx-deployment.yaml b/examples/nginx-deployment.yaml
new file mode 100644
index 00000000..7a0dd431
--- /dev/null
+++ b/examples/nginx-deployment.yaml
@@ -0,0 +1,16 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+      - name: nginx
+        image: nginx:1.7.9
+        ports:
+        - containerPort: 80
diff --git a/kubernetes_asyncio/__init__.py b/kubernetes_asyncio/__init__.py
index ac4c6209..cc37d5a2 100644
--- a/kubernetes_asyncio/__init__.py
+++ b/kubernetes_asyncio/__init__.py
@@ -20,3 +20,4 @@ import kubernetes_asyncio.client
 import kubernetes_asyncio.config
 import kubernetes_asyncio.watch
 import kubernetes_asyncio.stream
+import kubernetes_asyncio.utils
diff --git a/kubernetes_asyncio/utils/__init__.py b/kubernetes_asyncio/utils/__init__.py
new file mode 100644
index 00000000..2b8597c2
--- /dev/null
+++ b/kubernetes_asyncio/utils/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2018 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.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+from .create_from_yaml import FailToCreateError, create_from_yaml
diff --git a/kubernetes_asyncio/utils/create_from_yaml.py b/kubernetes_asyncio/utils/create_from_yaml.py
new file mode 100644
index 00000000..2c0995d4
--- /dev/null
+++ b/kubernetes_asyncio/utils/create_from_yaml.py
@@ -0,0 +1,145 @@
+# Copyright 2018 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.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import re
+from os import path
+
+import yaml
+
+from kubernetes_asyncio import client
+
+
+async def create_from_yaml(
+        k8s_client,
+        yaml_file,
+        verbose=False,
+        namespace="default",
+        **kwargs):
+    """
+    Perform an action from a yaml file. Pass True for verbose to
+    print confirmation information.
+    Input:
+    yaml_file: string. Contains the path to yaml file.
+    k8s_client: an ApiClient object, initialized with the client args.
+    verbose: If True, print confirmation from the create action.
+        Default is False.
+    namespace: string. Contains the namespace to create all
+        resources inside. The namespace must preexist otherwise
+        the resource creation will fail. If the API object in
+        the yaml file already contains a namespace definition
+        this parameter has no effect.
+    Returns:
+    An k8s api object or list of apis objects created from YAML.
+    When a single object is generated, return type is dependent
+    on output_list.
+    Throws a FailToCreateError exception if creation of any object
+    fails with helpful messages from the server.
+    Available parameters for creating <kind>:
+    :param async_req bool
+    :param bool include_uninitialized: If true, partially initialized
+        resources are included in the response.
+    :param str pretty: If 'true', then the output is pretty printed.
+    :param str dry_run: When present, indicates that modifications
+        should not be persisted. An invalid or unrecognized dryRun
+        directive will result in an error response and no further
+        processing of the request.
+        Valid values are: - All: all dry run stages will be processed
+    """
+
+    with open(path.abspath(yaml_file)) as f:
+        yml_document_all = yaml.safe_load_all(f)
+        api_exceptions = []
+        # Load all documents from a single YAML file
+        for yml_document in yml_document_all:
+            # If it is a list type, will need to iterate its items
+            if "List" in yml_document["kind"]:
+                # Could be "List" or "Pod/Service/...List"
+                # This is a list type. iterate within its items
+                kind = yml_document["kind"].replace("List", "")
+                for yml_object in yml_document["items"]:
+                    # Mitigate cases when server returns a xxxList object
+                    # See kubernetes-client/python#586
+                    if kind is not "":
+                        yml_object["apiVersion"] = yml_document["apiVersion"]
+                        yml_object["kind"] = kind
+                    try:
+                        await create_from_yaml_single_item(
+                            k8s_client, yml_object, verbose, namespace, **kwargs)
+                    except client.rest.ApiException as api_exception:
+                        api_exceptions.append(api_exception)
+            else:
+                # This is a single object. Call the single item method
+                try:
+                    await create_from_yaml_single_item(
+                        k8s_client, yml_document, verbose, namespace, **kwargs)
+                except client.rest.ApiException as api_exception:
+                    api_exceptions.append(api_exception)
+    # In case we have exceptions waiting for us, raise them
+    if api_exceptions:
+        raise FailToCreateError(api_exceptions)
+
+
+async def create_from_yaml_single_item(
+        k8s_client,
+        yml_object,
+        verbose=False,
+        namespace="default",
+        **kwargs):
+    group, _, version = yml_object["apiVersion"].partition("/")
+    if version == "":
+        version = group
+        group = "core"
+    # Take care for the case e.g. api_type is "apiextensions.k8s.io"
+    # Only replace the last instance
+    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('.'))
+    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 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', kind)
+    kind = re.sub('([a-z0-9])([A-Z])', r'\1_\2', kind).lower()
+    # Decide which namespace we are going to put the object in,
+    # if any
+    if "namespace" in yml_object["metadata"]:
+        namespace = yml_object["metadata"]["namespace"]
+    # Expect the user to create namespaced objects more often
+    if hasattr(k8s_api, "create_namespaced_{0}".format(kind)):
+        resp = await getattr(k8s_api, "create_namespaced_{0}".format(kind))(
+            body=yml_object, namespace=namespace, **kwargs)
+    else:
+        resp = await getattr(k8s_api, "create_{0}".format(kind))(
+            body=yml_object, **kwargs)
+    if verbose:
+        print("{0} created. status='{1}'".format(kind, str(resp.status)))
+
+
+class FailToCreateError(Exception):
+    """
+    An exception class for handling error if an error occurred when
+    handling a yaml file.
+    """
+
+    def __init__(self, api_exceptions):
+        self.api_exceptions = api_exceptions
+
+    def __str__(self):
+        msg = ""
+        for api_exception in self.api_exceptions:
+            msg += "Error from server ({0}): {1}".format(
+                api_exception.reason, api_exception.body)
+        return msg
diff --git a/setup.py b/setup.py
index e8235532..64cfb4aa 100644
--- a/setup.py
+++ b/setup.py
@@ -54,7 +54,8 @@ setup(
         'kubernetes_asyncio.watch',
         'kubernetes_asyncio.client.api',
         'kubernetes_asyncio.stream',
-        'kubernetes_asyncio.client.models'],
+        'kubernetes_asyncio.client.models',
+        'kubernetes_asyncio.utils'],
     include_package_data=True,
     long_description="""\
     Python client for kubernetes http://kubernetes.io/
diff --git a/tox.ini b/tox.ini
index acc74a1d..88ff4a02 100644
--- a/tox.ini
+++ b/tox.ini
@@ -37,7 +37,7 @@ commands =
 [testenv:coverage]
 commands =
    python -V
-   nosetests --with-coverage --cover-package=kubernetes_asyncio.config,kubernetes_asyncio.watch,kubernetes_asyncio.stream --cover-tests
+   nosetests --with-coverage --cover-package=kubernetes_asyncio.config,kubernetes_asyncio.watch,kubernetes_asyncio.stream,kubernetes_asyncio.utils --cover-tests
 
 [testenv:codecov]
 commands =


Test patch:
diff --git a/kubernetes_asyncio/e2e_test/test_extensions.py b/kubernetes_asyncio/e2e_test/test_extensions.py
index 743ad08d..3f60bd52 100644
--- a/kubernetes_asyncio/e2e_test/test_extensions.py
+++ b/kubernetes_asyncio/e2e_test/test_extensions.py
@@ -12,12 +12,15 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import os
+
 import uuid
 
 import asynctest
 import yaml
 
 from kubernetes_asyncio.client import api_client
+from kubernetes_asyncio import utils
 from kubernetes_asyncio.client.api import extensions_v1beta1_api
 from kubernetes_asyncio.client.models import v1_delete_options
 from kubernetes_asyncio.e2e_test import base
@@ -59,6 +62,38 @@ spec:
         options = v1_delete_options.V1DeleteOptions()
         resp = await api.delete_namespaced_deployment(name, 'default', body=options)
 
+    async def test_create_deployment_from_yaml_file(self):
+        client = api_client.ApiClient(configuration=self.config)
+        api = extensions_v1beta1_api.ExtensionsV1beta1Api(client)
+        name = 'nginx-deployment-' + str(uuid.uuid4())
+        tempfile = 'temp.yaml'
+        deployment = '''apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: %s
+spec:
+  replicas: 3
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+      - name: nginx
+        image: nginx:1.7.9
+        ports:
+        - containerPort: 80
+'''
+        with open(tempfile, 'w') as f:
+            f.write(deployment % name)
+        resp = await utils.create_from_yaml(client, tempfile)
+        os.remove(tempfile)
+        resp = await api.read_namespaced_deployment(name, 'default')
+        self.assertIsNotNone(resp)
+
+        options = v1_delete_options.V1DeleteOptions()
+        resp = await api.delete_namespaced_deployment(name, 'default', body=options)
+
     async def test_create_daemonset(self):
         client = api_client.ApiClient(configuration=self.config)
         api = extensions_v1beta1_api.ExtensionsV1beta1Api(client)
diff --git a/kubernetes_asyncio/test/test_utils_create_from_yaml.py b/kubernetes_asyncio/test/test_utils_create_from_yaml.py
new file mode 100644
index 00000000..20776b23
--- /dev/null
+++ b/kubernetes_asyncio/test/test_utils_create_from_yaml.py
@@ -0,0 +1,30 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+
+import unittest
+
+from kubernetes_asyncio.utils import create_from_yaml
+
+import kubernetes_asyncio.client
+from kubernetes_asyncio.client.rest import ApiException
+
+
+class TestCreateFromYAML(unittest.TestCase):
+    """ExtensionsV1beta1IDRange unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testCreateFromYAML(self):
+        """Test testCreateFromYAML"""
+        # The only function there is will deploy a yaml straight to a kubernetes cluster
+        # So there is not much to unit test... There is a e2e test doing this.
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()