From 28a37d41bd40336c570fcf28c2f8eac4c9e48c2f Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Thu, 5 Dec 2019 18:13:14 +0200 Subject: [PATCH] Add http.login function accepts optional path to kubeconfig file and optional name of a context in the kubeconfig file. If kubeconfig not specified, first tries to load kubeconfig from service account. If that is not available too, tries to load configuration from default local source. Returns HTTPClient instance configured with loaded config. Issue #24 --- pykube/__init__.py | 2 +- pykube/console.py | 8 ++------ pykube/http.py | 28 ++++++++++++++++++++++++++++ tests/test_login.py | 41 +++++++++++++++++++++++++++++++++++++++++ tox.ini | 9 +++++---- 5 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 tests/test_login.py diff --git a/pykube/__init__.py b/pykube/__init__.py index 012592d..6d9fe4a 100644 --- a/pykube/__init__.py +++ b/pykube/__init__.py @@ -6,7 +6,7 @@ from .config import KubeConfig # noqa from .exceptions import KubernetesError, PyKubeError, ObjectDoesNotExist # noqa -from .http import HTTPClient # noqa +from .http import HTTPClient, login # noqa from .objects import ( # noqa object_factory, ConfigMap, diff --git a/pykube/console.py b/pykube/console.py index 2ea46a6..1247b0b 100644 --- a/pykube/console.py +++ b/pykube/console.py @@ -20,12 +20,8 @@ def main(argv=None): parser.add_argument('-c', help='Python program passed in as string', metavar='SCRIPT') args = parser.parse_args(argv) - config = pykube.KubeConfig.from_file(args.kubeconfig) - - if args.context: - config.set_current_context(args.context) - - api = pykube.HTTPClient(config) + api = pykube.login(kubeconfig=args.kubeconfig, kubecontext=args.context) + config = api.config context = { '__name__': '__console__', diff --git a/pykube/http.py b/pykube/http.py index ccb5004..fdcee52 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -7,6 +7,7 @@ import posixpath import shlex import subprocess +from typing import Optional try: import google.auth @@ -342,3 +343,30 @@ def delete(self, *args, **kwargs): - `kwargs`: Keyword arguments """ return self.session.delete(*args, **self.get_kwargs(**kwargs)) + + +def login( + kubeconfig: Optional[str] = None, + kubecontext: Optional[str] = None, +) -> HTTPClient: + """Make Kubernetes API client. + + :param kubeconfig: (optional) path to kubeconfig, + if not passed will use in-cluster config or + default kubectl ones (in that order of preference) + :param kubecontext: (optional) name of the context in the kubeconfig file + :return: pykube.HTTPClient + """ + if kubeconfig: + # if passed kubeconfig file explicitly - use it + config = KubeConfig.from_file(kubeconfig) + else: + try: + # running in cluster + config = KubeConfig.from_service_account() + except FileNotFoundError: + # not running in cluster => load default local KUBECONFIG + config = KubeConfig.from_file() + if kubecontext: + config.set_current_context(kubecontext) + return HTTPClient(config) diff --git a/tests/test_login.py b/tests/test_login.py new file mode 100644 index 0000000..b44c65c --- /dev/null +++ b/tests/test_login.py @@ -0,0 +1,41 @@ +from unittest import mock + +from pykube.http import login +from . import TestCase + + +@mock.patch('pykube.http.KubeConfig') +@mock.patch('pykube.http.HTTPClient') +class TestLogin(TestCase): + def test_login_with_config(self, client, config): + api = login(kubeconfig='/path/to/file') + config.from_file.assert_called_once_with('/path/to/file') + config.from_service_account.assert_not_called() + config.from_file.return_value.set_current_context.assert_not_called() + client.assert_called_once_with(config.from_file.return_value) + assert api == client.return_value + + def test_login_with_config_and_context(self, client, config): + api = login(kubeconfig='/path/to/file', kubecontext='fake') + config.from_file.assert_called_once_with('/path/to/file') + config.from_service_account.assert_not_called() + config.from_file.return_value.set_current_context.assert_called_once_with('fake') + client.assert_called_once_with(config.from_file.return_value) + assert api == client.return_value + + def test_login_with_incluster(self, client, config): + api = login() + config.from_service_account.assert_called_once_with() + config.from_file.assert_not_called() + config.from_service_account.return_value.set_current_context.assert_not_called() + client.assert_called_once_with(config.from_service_account.return_value) + assert api == client.return_value + + def test_login_with_default_config(self, client, config): + config.from_service_account.side_effect = FileNotFoundError + api = login() + config.from_service_account.assert_called_once_with() + config.from_file.assert_called_once_with() + config.from_file.return_value.set_current_context.assert_not_called() + client.assert_called_once_with(config.from_file.return_value) + assert api == client.return_value diff --git a/tox.ini b/tox.ini index 1cfaf28..0936372 100644 --- a/tox.ini +++ b/tox.ini @@ -8,10 +8,11 @@ envlist = [testenv] deps = - coverage == 4.0.3 - flake8 == 2.5.4 - pytest == 2.9.0 - pytest-cov == 2.2.1 + coverage == 4.5.4 + flake8 == 3.7.8 + pytest == 5.2.1 + pytest-cov == 2.8.1 + responses == 0.10.6 usedevelop = True setenv = LANG=en_US.UTF-8