tomplus / kubernetes_asyncio

Python asynchronous client library for Kubernetes http://kubernetes.io/
Apache License 2.0
359 stars 71 forks source link

`client.Configuration.proxy` doesn't work. #90

Open tatsuya0619 opened 4 years ago

tatsuya0619 commented 4 years ago

I was trying to use this library with http proxy.

However, if I set the 'proxy' configuration(like below script) and run the program, ka.client.CoreV1Api() raises TypeError.

This is the script(almost same as README.md sample):

import asyncio
import kubernetes_asyncio as ka

async def main():
    await ka.config.load_kube_config()

    # below 3 lines are main change from `README.md` sample.
    conf = ka.client.Configuration()
    conf.proxy = 'http://your_http_proxy'
    ka.client.Configuration.set_default(conf)

    v1 = ka.client.CoreV1Api() # This raises exception.

    print("Listing pods with their IPs:")
    ret = await v1.list_pod_for_all_namespaces()

    for i in ret.items:
        print(i.status.pod_ip, i.metadata.namespace, i.metadata.name)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()

The output is:

Exception ignored in: <bound method ClientSession.__del__ of <aiohttp.client.ClientSession object at 0x7fa97bb3fc88>>
Traceback (most recent call last):
  File "/home/ta-ono/.virtualenvs/dev-ka/lib/python3.6/site-packages/aiohttp/client.py", line 302, in __del__
    if not self.closed:
  File "/home/ta-ono/.virtualenvs/dev-ka/lib/python3.6/site-packages/aiohttp/client.py", line 916, in closed
    return self._connector is None or self._connector.closed
AttributeError: 'ClientSession' object has no attribute '_connector'
Traceback (most recent call last):
  File "test.py", line 26, in <module>
    loop.run_until_complete(main())
  File "/usr/lib64/python3.6/asyncio/base_events.py", line 484, in run_until_complete
    return future.result()
  File "test.py", line 16, in main
    v1 = ka.client.CoreV1Api()
  File "/home/ta-ono/programs/kubernetes_asyncio/kubernetes_asyncio/client/api/core_v1_api.py", line 32, in __init__
    api_client = ApiClient()
  File "/home/ta-ono/programs/kubernetes_asyncio/kubernetes_asyncio/client/api_client.py", line 72, in __init__
    self.rest_client = rest.RESTClientObject(configuration)
  File "/home/ta-ono/programs/kubernetes_asyncio/kubernetes_asyncio/client/rest.py", line 76, in __init__
    proxy=configuration.proxy
TypeError: __init__() got an unexpected keyword argument 'proxy'
Exception ignored in: <bound method RESTClientObject.__del__ of <kubernetes_asyncio.client.rest.RESTClientObject object at 0x7fa97bb3f470>>
Traceback (most recent call last):
  File "/home/ta-ono/programs/kubernetes_asyncio/kubernetes_asyncio/client/rest.py", line 84, in __del__
AttributeError: 'RESTClientObject' object has no attribute 'pool_manager'

I think proxy argument should be passed to aiohttp.ClientSession.request instead of aiohttp.ClientSession.

I could run the above script by changing kubernetes_asyncio.client.rest.RESTClientObject. The change is:

(dev-ka) [ta-ono@kubernetes_asyncio]$ git diff
diff --git a/kubernetes_asyncio/client/rest.py b/kubernetes_asyncio/client/rest.py
index 69bba34a..5a71b894 100644
--- a/kubernetes_asyncio/client/rest.py
+++ b/kubernetes_asyncio/client/rest.py
@@ -69,16 +69,12 @@ class RESTClientObject(object):
             ssl_context=ssl_context
         )

+        self.proxy = configuration.proxy
+
         # https pool manager
-        if configuration.proxy:
-            self.pool_manager = aiohttp.ClientSession(
-                connector=connector,
-                proxy=configuration.proxy
-            )
-        else:
-            self.pool_manager = aiohttp.ClientSession(
-                connector=connector
-            )
+        self.pool_manager = aiohttp.ClientSession(
+            connector=connector
+        )

     def __del__(self):
         asyncio.ensure_future(self.pool_manager.close())
@@ -168,6 +164,7 @@ class RESTClientObject(object):
                          declared content type."""
                 raise ApiException(status=0, reason=msg)

+        args['proxy'] = self.proxy
         r = await self.pool_manager.request(**args)
         if _preload_content:

However this change doesn't pass pytest via http proxy. So I'm not sure this is correct.

Thanks.

tomplus commented 4 years ago

@tatsuya0619 Thanks. It looks good to me but it has to be fixed in the generator template:

https://github.com/OpenAPITools/openapi-generator/blob/50f7e14a9974cce15aaaaf5b3c4c48f079613b38/modules/openapi-generator/src/main/resources/python/asyncio/rest.mustache#L70

Could you create a PR to the openapi-generator? I can help with tests, what kind of errors have you got?

tatsuya0619 commented 4 years ago

I'm sorry, but I don't have time now to create a PR to the openapi-generator repo.

And about the pytest error, my words are incorret. Surely, pytest passed when I don't change the test code. However, modified pytest fails at kubernetes_asyncio/e2e_test/test_client.py.

I modified pytest code like below to communicate with Kubernetes cluster via http proxy:

(dev-ka) [ta-ono@kubernetes_asyncio]$ git diff
diff --git a/kubernetes_asyncio/e2e_test/base.py b/kubernetes_asyncio/e2e_test/base.py
index e8f1bf96..f22a0dc2 100644
--- a/kubernetes_asyncio/e2e_test/base.py
+++ b/kubernetes_asyncio/e2e_test/base.py
@@ -23,6 +23,7 @@ DEFAULT_E2E_HOST = '127.0.0.1'

 def get_e2e_configuration():
     config = Configuration()
+    config.proxy = 'http://my_http_proxy:9999'
     config.host = None
     if os.path.exists(
             os.path.expanduser(kube_config.KUBE_CONFIG_DEFAULT_LOCATION)):

I run pytest on GKE via http proxy. And when I execute pytest, It stopped at kubernetes_asyncio/e2e_test/test_client.py. And after a while, It shows too many errors to paste it. The output of $ pytest kubernetes_asyncio/e2e_test/test_client.py with modified pytest is:

(dev-ka) [ta-ono@kubernetes_asyncio]$ pytest kubernetes_asyncio/e2e_test/test_client.py
========================================= test session starts ==========================================
platform linux -- Python 3.6.8, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: /home/ta-ono/programs/kubernetes_asyncio
plugins: forked-1.1.3, xdist-1.31.0
collected 5 items

kubernetes_asyncio/e2e_test/test_client.py ..Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f242e608c88>
Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7f242e60d0b0>, 32749.447576531)]']
connector: <aiohttp.connector.TCPConnector object at 0x7f242e608ba8>
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f242e608438>
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f242e333780>
Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7f242e31c4c0>, 32749.591333565)]']
connector: <aiohttp.connector.TCPConnector object at 0x7f242e333748>
F                                                 [100%]

=============================================== FAILURES ===============================================
_______________________________________ TestClient.test_pod_apis _______________________________________

self = <kubernetes_asyncio.e2e_test.test_client.TestClient testMethod=test_pod_apis>

    async def test_pod_apis(self):
        client = api_client.ApiClient(configuration=self.config)
        client_ws = WsApiClient(configuration=self.config)

        api = core_v1_api.CoreV1Api(client)
        api_ws = core_v1_api.CoreV1Api(client_ws)

        name = 'busybox-test-' + short_uuid()
        pod_manifest = {
            'apiVersion': 'v1',
            'kind': 'Pod',
            'metadata': {
                'name': name
            },
            'spec': {
                'containers': [{
                    'image': 'busybox',
                    'name': 'sleep',
                    "args": [
                        "/bin/sh",
                        "-c",
                        "while true;do date;sleep 5; done"
                    ]
                }]
            }
        }

        resp = await api.create_namespaced_pod(body=pod_manifest, namespace='default')
        self.assertEqual(name, resp.metadata.name)
        self.assertTrue(resp.status.phase)

        while True:
>           resp = await api.read_namespaced_pod(name=name, namespace='default')

kubernetes_asyncio/e2e_test/test_client.py:69:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
kubernetes_asyncio/client/api_client.py:166: in __call_api
    _request_timeout=_request_timeout)
kubernetes_asyncio/client/rest.py:187: in GET
    query_params=query_params))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <kubernetes_asyncio.client.rest.RESTClientObject object at 0x7f242e608be0>, method = 'GET'
url = 'https://34.85.96.57/api/v1/namespaces/default/pods/busybox-test-596b7ff99dc4', query_params = []
headers = {'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'OpenAPI-Generator/10.0.1-snapshot/py...QoqMUS98o4L9thvCD8wREWzrYnx3bZ8sKLXhqv1iR-V9iTlEkuCT7AoYQJH7vDpFHX__30phT8WuVO_7xE5tloIc0BWzAZZrYQyWYXN-coLP9BCmS3gMo'}
body = None, post_params = {}, _preload_content = True, _request_timeout = None

    async def request(self, method, url, query_params=None, headers=None,
                      body=None, post_params=None, _preload_content=True,
                      _request_timeout=None):
        """Execute request

        :param method: http request method
        :param url: http request url
        :param query_params: query parameters in the url
        :param headers: http request headers
        :param body: request json body, for `application/json`
        :param post_params: request post parameters,
                            `application/x-www-form-urlencoded`
                            and `multipart/form-data`
        :param _preload_content: this is a non-applicable field for
                                 the AiohttpClient.
        :param _request_timeout: timeout setting for this request. If one
                                 number provided, it will be total request
                                 timeout. It can also be a pair (tuple) of
                                 (connection, read) timeouts.
        """
        method = method.upper()
        assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
                          'PATCH', 'OPTIONS']

        if post_params and body:
            raise ValueError(
                "body parameter cannot be used with post_params parameter."
            )

        post_params = post_params or {}
        headers = headers or {}
        timeout = _request_timeout or 5 * 60

        if 'Content-Type' not in headers:
            headers['Content-Type'] = 'application/json'

        args = {
            "method": method,
            "url": url,
            "timeout": timeout,
            "headers": headers
        }

        if query_params:
            args["url"] += '?' + urlencode(query_params)

        # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
        if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
            if re.search('json', headers['Content-Type'], re.IGNORECASE):
                if headers['Content-Type'] == 'application/json-patch+json':
                    if not isinstance(body, list):
                        headers['Content-Type'] = 'application/strategic-merge-patch+json'
                if body is not None:
                    body = json.dumps(body)
                args["data"] = body
            elif headers['Content-Type'] == 'application/x-www-form-urlencoded':  # noqa: E501
                args["data"] = aiohttp.FormData(post_params)
            elif headers['Content-Type'] == 'multipart/form-data':
                # must del headers['Content-Type'], or the correct
                # Content-Type which generated by aiohttp
                del headers['Content-Type']
                data = aiohttp.FormData()
                for param in post_params:
                    k, v = param
                    if isinstance(v, tuple) and len(v) == 3:
                        data.add_field(k,
                                       value=v[1],
                                       filename=v[0],
                                       content_type=v[2])
                    else:
                        data.add_field(k, v)
                args["data"] = data

            # Pass a `bytes` parameter directly in the body to support
            # other content types than Json when `body` argument is provided
            # in serialized form
            elif isinstance(body, bytes):
                args["data"] = body
            else:
                # Cannot generate the request from given parameters
                msg = """Cannot prepare a request message for provided
                         arguments. Please check that your arguments match
                         declared content type."""
                raise ApiException(status=0, reason=msg)
        args['proxy'] = self.proxy
        r = await self.pool_manager.request(**args)
        if _preload_content:

            data = await r.text()
            r = RESTResponse(r, data)

            # log response body
            logger.debug("response body: %s", r.data)

            if not 200 <= r.status <= 299:
>               raise ApiException(http_resp=r)
E               kubernetes_asyncio.client.rest.ApiException: (401)
E               Reason: Unauthorized
E               HTTP response headers: <CIMultiDictProxy('Audit-Id': '0d7f9c82-56c4-4a7e-8d19-e0319261dc48', 'Content-Type': 'application/json', 'Date': 'Fri, 17 Jan 2020 08:57:27 GMT', 'Content-Length': '129')>
E               HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}

kubernetes_asyncio/client/rest.py:177: ApiException
=========================================== warnings summary ===========================================
kubernetes_asyncio/e2e_test/test_client.py::TestClient::test_configmap_apis
kubernetes_asyncio/e2e_test/test_client.py::TestClient::test_node_apis
kubernetes_asyncio/e2e_test/test_client.py::TestClient::test_pod_apis
kubernetes_asyncio/e2e_test/test_client.py::TestClient::test_pod_apis
kubernetes_asyncio/e2e_test/test_client.py::TestClient::test_replication_controller_apis
kubernetes_asyncio/e2e_test/test_client.py::TestClient::test_service_apis
  /home/ta-ono/programs/kubernetes_asyncio/kubernetes_asyncio/client/rest.py:69: DeprecationWarning: ssl
_context is deprecated, use ssl=context instead
    ssl_context=ssl_context

-- Docs: https://docs.pytest.org/en/latest/warnings.html
========================= 3 failed, 2 passed, 6 warnings in 316.81s (0:05:16) ==========================
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f242e367198>
Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7f242e31cce0>, 32749.694242211)]']
connector: <aiohttp.connector.TCPConnector object at 0x7f242e367fd0>
(dev-ka) [ta-ono@kubernetes_asyncio]$

I couldn't spend time to research this error. So maybe the change is wrong.

Thanks.

ntextreme3 commented 4 years ago

Also just noticed this, opened https://github.com/swagger-api/swagger-codegen/issues/9995 on generator repo.

tomplus commented 4 years ago

Thanks for describing the issue. I'll work on it later.