From 6ebb5691acd6d9b8e54a8a258c1ccd82cdc03703 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 10:27:26 +0100 Subject: [PATCH 01/22] mark as fork --- .travis.yml | 4 +--- README.rst | 21 ++++++++++----------- setup.py | 6 +++--- tox.ini | 4 +--- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 12a6cc1..15613d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ language: python python: - "2.7" - - "3.3" - - "3.4" - - "3.5" + - "3.6" install: - pip install tox coveralls script: diff --git a/README.rst b/README.rst index 5d0d95b..df245d4 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,19 @@ pykube ====== -.. image:: http://slack.kelproject.com/badge.svg - :target: http://slack.kelproject.com/ +.. image:: https://img.shields.io/travis/hjacobs/pykube.svg + :target: https://travis-ci.org/hjacobs/pykube -.. image:: https://img.shields.io/travis/kelproject/pykube.svg - :target: https://travis-ci.org/kelproject/pykube +.. image:: https://img.shields.io/pypi/dm/pykube-ng.svg + :target: https://pypi.python.org/pypi/pykube-ng/ -.. image:: https://img.shields.io/pypi/dm/pykube.svg - :target: https://pypi.python.org/pypi/pykube/ - -.. image:: https://img.shields.io/pypi/v/pykube.svg - :target: https://pypi.python.org/pypi/pykube/ +.. image:: https://img.shields.io/pypi/v/pykube-ng.svg + :target: https://pypi.python.org/pypi/pykube-ng/ .. image:: https://img.shields.io/badge/license-apache-blue.svg - :target: https://pypi.python.org/pypi/pykube/ + :target: https://pypi.python.org/pypi/pykube-ng/ + +**IMPORTANT**: This is a fork of `kelproject/pykube `_ which is no longer maintained (archived)! Python client library for Kubernetes @@ -46,7 +45,7 @@ Installation To install pykube, use pip:: - pip install pykube + pip install pykube-ng Usage ----- diff --git a/setup.py b/setup.py index 6b314ca..81fd577 100644 --- a/setup.py +++ b/setup.py @@ -19,14 +19,14 @@ ]) setup( - name="pykube", - version="0.16a1", + name="pykube-ng", + version="0.17a1", description="Python client library for Kubernetes", long_description=long_description, author="Eldarion, Inc.", author_email="development@eldarion.com", license="Apache", - url="https://github.com/kelproject/pykube", + url="https://github.com/hjacobs/pykube", classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Web Environment", diff --git a/tox.ini b/tox.ini index 808f169..817b8dd 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,7 @@ ignore = E265,E501 [tox] envlist = py27, - py33, - py34, - py35 + py36 [testenv] deps = From eb816b8f7c3af2109a07000a0f24e66e80f262b4 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 11:29:06 +0100 Subject: [PATCH 02/22] skip tests, clean up build --- .gitignore | 3 +++ .travis.yml | 4 +++- README.rst | 43 ++++++++++++++++++++++++++++++++--------- pykube/http.py | 17 ++++++---------- test/test_config.py | 1 + test/test_http.py | 25 ++++++++++++------------ test/test_httpclient.py | 5 +++-- test/test_session.py | 13 ++++++------- tox.ini | 3 ++- 9 files changed, 71 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 26e63b2..1a359cb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ __pycache__ *.pyo build/ dist/ +.cache/ +.pytest_cache/ +.tox/ diff --git a/.travis.yml b/.travis.yml index 15613d8..3c63002 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,11 @@ +dist: xenial language: python python: - "2.7" - "3.6" install: - - pip install tox coveralls + - pip install pipenv + - pipenv install --dev script: - tox -e py${TRAVIS_PYTHON_VERSION//[.]/} after_success: diff --git a/README.rst b/README.rst index df245d4..7256881 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,10 @@ pykube .. image:: https://img.shields.io/travis/hjacobs/pykube.svg :target: https://travis-ci.org/hjacobs/pykube +.. image:: https://coveralls.io/repos/github/hjacobs/pykube/badge.svg?branch=master;_=1 + :target: https://coveralls.io/github/hjacobs/pykube?branch=master + :alt: Code Coverage + .. image:: https://img.shields.io/pypi/dm/pykube-ng.svg :target: https://pypi.python.org/pypi/pykube-ng/ @@ -151,6 +155,7 @@ Check server version: api = pykube.HTTPClient(pykube.KubeConfig.from_file("/Users//.kube/config")) api.version + HTTPie ------ @@ -164,13 +169,36 @@ pykube can be used together with HTTPie for Kubernetes command line querying goo The above example will construct an HTTP request to the cluster behind the ``minikube`` context and show you the response containing all services. + Requirements ------------ -* Python 2.7 or 3.3+ +* Python 3.6+ * requests (included in ``install_requires``) * PyYAML (included in ``install_requires``) + +Local Development +----------------- + +You can run pykube against your current kubeconfig context, e.g. local Minikube_: + +.. code-block:: bash + + $ pipenv install --dev + $ pipenv run python3 + >>> import pykube, os + >>> config = pykube.KubeConfig.from_file(os.path.expanduser('~/.kube/config')) + >>> api = pykube.HTTPClient(config) + >>> list(pykube.Deployment.objects(api)) + +To run PEP8 (flake8) checks and unit tests including coverage report: + +.. code-block:: bash + + $ make test + + License ------- @@ -181,10 +209,10 @@ The code in this project is licensed under the Apache License, version 2.0 Contributing ------------ -By making a contribution to this project, you are agreeing to the `Developer -Certificate of Origin v1.1`_ (also included in this repository under DCO.txt). +Easiest way to contribute is to provide feedback! We would love to hear what you like and what you think is missing. +Create an issue or `ping try_except_ on Twitter`_. -.. _Developer Certificate of Origin v1.1: http://developercertificate.org +PRs are welcome. Please also have a look at `issues labeled with "help wanted"`_. Code of Conduct @@ -196,8 +224,5 @@ Project follows the `Contributor Covenant Code of Conduct`_. .. _Contributor Covenant Code of Conduct: http://contributor-covenant.org/version/1/4/ -Commercial Support ------------------- - -Commercial support for Kel is available through Eldarion, please contact -info@eldarion.com. +.. _ping try_except_ on Twitter: https://twitter.com/try_except_ +.. _issues labeled with "help wanted": https://github.com/hjacobs/pykube/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 diff --git a/pykube/http.py b/pykube/http.py index 8cdfdb6..41ab4ac 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -28,7 +28,11 @@ _ipv4_re = re.compile(r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") -class KubernetesHTTPAdapterSendMixin(object): +class KubernetesHTTPAdapter(requests.adapters.HTTPAdapter): + + def __init__(self, kube_config, **kwargs): + self.kube_config = kube_config + super(KubernetesHTTPAdapter, self).__init__(**kwargs) def _persist_credentials(self, config, token, expiry): user_name = config.contexts[config.current_context]["user"] @@ -120,7 +124,7 @@ def send(self, request, **kwargs): elif "insecure-skip-tls-verify" in config.cluster: kwargs["verify"] = not config.cluster["insecure-skip-tls-verify"] - send = super(KubernetesHTTPAdapterSendMixin, self).send + send = super(KubernetesHTTPAdapter, self).send response = send(request, **kwargs) _retry_status_codes = {http_client.UNAUTHORIZED} @@ -136,20 +140,11 @@ def send(self, request, **kwargs): return response -class KubernetesHTTPAdapter(KubernetesHTTPAdapterSendMixin, requests.adapters.HTTPAdapter): - - def __init__(self, kube_config, **kwargs): - self.kube_config = kube_config - super(KubernetesHTTPAdapter, self).__init__(**kwargs) - - class HTTPClient(object): """ Client for interfacing with the Kubernetes API. """ - _session = None - def __init__(self, config): """ Creates a new instance of the HTTPClient. diff --git a/test/test_config.py b/test/test_config.py index dc519d1..7aa5985 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -12,6 +12,7 @@ GOOD_CONFIG_FILE_PATH = os.path.sep.join(["test", "test_config.yaml"]) DEFAULTUSER_CONFIG_FILE_PATH = os.path.sep.join(["test", "test_config_default_user.yaml"]) + class TestConfig(TestCase): def setUp(self): diff --git a/test/test_http.py b/test/test_http.py index c53170a..75d54f6 100644 --- a/test/test_http.py +++ b/test/test_http.py @@ -3,25 +3,26 @@ """ import os +import pytest + +from unittest.mock import MagicMock from pykube.http import HTTPClient from pykube.config import KubeConfig -from . import TestCase - GOOD_CONFIG_FILE_PATH = os.path.sep.join(["test", "test_config_with_context.yaml"]) -class TestHttp(TestCase): +def test_http(monkeypatch): + cfg = KubeConfig.from_file(GOOD_CONFIG_FILE_PATH) + session = HTTPClient(cfg).session - def setUp(self): - self.cfg = KubeConfig.from_file(GOOD_CONFIG_FILE_PATH) + mock_send = MagicMock() + mock_send.side_effect = Exception('MOCK HTTP') + monkeypatch.setattr('requests.adapters.HTTPAdapter.send', mock_send) - def tearDown(self): - self.cfg = None + with pytest.raises(Exception): + session.get('http://localhost:9090/test') - def test_build_session_basic(self): - """ - """ - session = HTTPClient(self.cfg).session - self.assertEqual(session.auth, ('adm', 'somepassword')) + mock_send.assert_called_once() + assert mock_send.call_args[0][0].headers['Authorization'] == 'Basic YWRtOnNvbWVwYXNzd29yZA==' diff --git a/test/test_httpclient.py b/test/test_httpclient.py index 18e3b0d..a10dbee 100644 --- a/test/test_httpclient.py +++ b/test/test_httpclient.py @@ -119,5 +119,6 @@ def test_build_session_bearer_token(self): client = pykube.HTTPClient(pykube.KubeConfig(doc=self.config)) _log.debug('Checking headers %s', client.session.headers) - self.assertIn('Authorization', client.session.headers) - self.assertEqual(client.session.headers['Authorization'], 'Bearer test') + # TODO: session.headers is no long filled due to KubernetesHTTPAdapter! + #self.assertIn('Authorization', client.session.headers) + #self.assertEqual(client.session.headers['Authorization'], 'Bearer test') diff --git a/test/test_session.py b/test/test_session.py index ad85381..daf4e61 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -6,8 +6,6 @@ import logging import tempfile -import pykube - from . import TestCase BASE_CONFIG = { @@ -82,11 +80,12 @@ def test_build_session_auth_provider(self): with open(tmp, 'w') as f: f.write(gcloud_content) - session = pykube.session.GCPSession(pykube.KubeConfig(doc=self.config), tmp) - self.assertEquals(session.oauth.token['access_token'], 'abc') - self.assertEquals(session.oauth.token['refresh_token'], 'myrefreshtoken') - self.assertEquals(session.credentials.get('client_id'), 'myclientid') - self.assertEquals(session.credentials.get('client_secret'), 'myclientsecret') + # TODO: this no longer works due to refactoring, GCP session handling is now done in KubernetesHTTPAdapter + #session = pykube.session.GCPSession(pykube.KubeConfig(doc=self.config), tmp) + #self.assertEquals(session.oauth.token['access_token'], 'abc') + #self.assertEquals(session.oauth.token['refresh_token'], 'myrefreshtoken') + #self.assertEquals(session.credentials.get('client_id'), 'myclientid') + #self.assertEquals(session.credentials.get('client_secret'), 'myclientsecret') finally: if os.path.exists(tmp): os.remove(tmp) diff --git a/tox.ini b/tox.ini index 817b8dd..5ca52dc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [flake8] -ignore = E265,E501 +ignore = E265,E501,W504 +max-line-length=160 [tox] envlist = From 9cda79bdd14befc4238b2092749ef594807970f8 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 11:35:45 +0100 Subject: [PATCH 03/22] #1 update Kubernetes api versions --- pykube/__init__.py | 1 - pykube/objects.py | 25 +++++++++---------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/pykube/__init__.py b/pykube/__init__.py index 52075b8..cee5de5 100644 --- a/pykube/__init__.py +++ b/pykube/__init__.py @@ -21,7 +21,6 @@ Node, PersistentVolume, PersistentVolumeClaim, - PetSet, Pod, PodSecurityPolicy, ReplicationController, diff --git a/pykube/objects.py b/pykube/objects.py index e24f1b7..59fbde9 100644 --- a/pykube/objects.py +++ b/pykube/objects.py @@ -169,21 +169,21 @@ class ConfigMap(NamespacedAPIObject): class CronJob(NamespacedAPIObject): - version = "batch/v2alpha1" + version = "batch/v1beta1" endpoint = "cronjobs" kind = "CronJob" class DaemonSet(NamespacedAPIObject): - version = "extensions/v1beta1" + version = "apps/v1" endpoint = "daemonsets" kind = "DaemonSet" class Deployment(NamespacedAPIObject, ReplicatedMixin, ScalableMixin): - version = "extensions/v1beta1" + version = "apps/v1" endpoint = "deployments" kind = "Deployment" @@ -386,7 +386,7 @@ def ready(self): class ReplicaSet(NamespacedAPIObject, ReplicatedMixin, ScalableMixin): - version = "extensions/v1beta1" + version = "apps/v1" endpoint = "replicasets" kind = "ReplicaSet" @@ -426,44 +426,37 @@ class HorizontalPodAutoscaler(NamespacedAPIObject): kind = "HorizontalPodAutoscaler" -class PetSet(NamespacedAPIObject): - - version = "apps/v1alpha1" - endpoint = "petsets" - kind = "PetSet" - - class StatefulSet(NamespacedAPIObject, ReplicatedMixin, ScalableMixin): - version = "apps/v1beta1" + version = "apps/v1" endpoint = "statefulsets" kind = "StatefulSet" class Role(NamespacedAPIObject): - version = "rbac.authorization.k8s.io/v1alpha1" + version = "rbac.authorization.k8s.io/v1" endpoint = "roles" kind = "Role" class RoleBinding(NamespacedAPIObject): - version = "rbac.authorization.k8s.io/v1alpha1" + version = "rbac.authorization.k8s.io/v1" endpoint = "rolebindings" kind = "RoleBinding" class ClusterRole(APIObject): - version = "rbac.authorization.k8s.io/v1alpha1" + version = "rbac.authorization.k8s.io/v1" endpoint = "clusterroles" kind = "ClusterRole" class ClusterRoleBinding(APIObject): - version = "rbac.authorization.k8s.io/v1alpha1" + version = "rbac.authorization.k8s.io/v1" endpoint = "clusterrolebindings" kind = "ClusterRoleBinding" From d2b3830258d0dcab60dfd42384d15807093f2595 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 11:41:59 +0100 Subject: [PATCH 04/22] drop Python 2.7 support --- .travis.yml | 3 +-- README.rst | 3 +++ setup.py | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3c63002..e969e70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,11 @@ dist: xenial language: python python: - - "2.7" - "3.6" install: - pip install pipenv - pipenv install --dev script: - - tox -e py${TRAVIS_PYTHON_VERSION//[.]/} + - make test after_success: - coveralls diff --git a/README.rst b/README.rst index 7256881..7557c39 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,9 @@ pykube .. image:: https://img.shields.io/pypi/v/pykube-ng.svg :target: https://pypi.python.org/pypi/pykube-ng/ +.. image:: https://img.shields.io/pypi/pyversions/pykube-ng.svg + :target: https://pypi.python.org/pypi/pykube-ng/ + .. image:: https://img.shields.io/badge/license-apache-blue.svg :target: https://pypi.python.org/pypi/pykube-ng/ diff --git a/setup.py b/setup.py index 81fd577..fe9c248 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,9 @@ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3 :: Only", ], zip_safe=False, packages=find_packages(), From 7d78f3a0646912f8bd7d28df9050a3448904d1f8 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 11:45:10 +0100 Subject: [PATCH 05/22] fix twine package check --- .travis.yml | 2 +- README.rst | 1 + setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e969e70..6c7bd80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,6 @@ install: - pip install pipenv - pipenv install --dev script: - - make test + - make test package after_success: - coveralls diff --git a/README.rst b/README.rst index 7557c39..9f280bc 100644 --- a/README.rst +++ b/README.rst @@ -229,3 +229,4 @@ Project follows the `Contributor Covenant Code of Conduct`_. .. _ping try_except_ on Twitter: https://twitter.com/try_except_ .. _issues labeled with "help wanted": https://github.com/hjacobs/pykube/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 +.. _Minikube: https://github.com/kubernetes/minikube diff --git a/setup.py b/setup.py index fe9c248..ebe5d45 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ version="0.17a1", description="Python client library for Kubernetes", long_description=long_description, + long_description_content_type='text/x-rst', author="Eldarion, Inc.", author_email="development@eldarion.com", license="Apache", From 1cf641f76a6f8c3b849973e160867df03c413ef3 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 11:46:56 +0100 Subject: [PATCH 06/22] add Makefile --- Makefile | 17 +++ Pipfile | 18 +++ Pipfile.lock | 382 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 Makefile create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..365304c --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: test package upload + +VERSION ?= $(shell git describe --tags --always --dirty) + +default: package + +test: + pipenv run flake8 + pipenv run coverage run --source=pykube -m py.test + pipenv run coverage report + +package: test + pipenv run python3 setup.py sdist bdist_wheel + pipenv run twine check dist/pykube* + +upload: package + pipenv run twine upload dist/pykube* diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..9d49bcb --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"pykube-ng" = {path = ".", editable = true} + +[dev-packages] +"flake8" = "*" +pytest-cov = "*" +coveralls = "*" +tox = "*" +coverage = "*" +twine = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..cbe266a --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,382 @@ +{ + "_meta": { + "hash": { + "sha256": "6529102b414363c77a982a776dfede3d28f951068867b99cfbb3f8afd2832e15" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "pykube-ng": { + "editable": true, + "path": "." + }, + "pytz": { + "hashes": [ + "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", + "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" + ], + "version": "==2018.9" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "tzlocal": { + "hashes": [ + "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e" + ], + "version": "==1.5.1" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "bleach": { + "hashes": [ + "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", + "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" + ], + "version": "==3.1.0" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:06123b58a1410873e22134ca2d88bd36680479fe354955b3579fb8ff150e4d27", + "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", + "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", + "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", + "sha256:0d34245f824cc3140150ab7848d08b7e2ba67ada959d77619c986f2062e1f0e8", + "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", + "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", + "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", + "sha256:258b21c5cafb0c3768861a6df3ab0cfb4d8b495eee5ec660e16f928bf7385390", + "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", + "sha256:3ad59c84c502cd134b0088ca9038d100e8fb5081bbd5ccca4863f3804d81f61d", + "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", + "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", + "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", + "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", + "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", + "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", + "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", + "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", + "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", + "sha256:71afc1f5cd72ab97330126b566bbf4e8661aab7449f08895d21a5d08c6b051ff", + "sha256:7349c27128334f787ae63ab49d90bf6d47c7288c63a0a5dfaa319d4b4541dd2c", + "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", + "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", + "sha256:859714036274a75e6e57c7bab0c47a4602d2a8cfaaa33bbdb68c8359b2ed4f5c", + "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", + "sha256:869ef4a19f6e4c6987e18b315721b8b971f7048e6eaea29c066854242b4e98d9", + "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", + "sha256:977e2d9a646773cc7428cdd9a34b069d6ee254fadfb4d09b3f430e95472f3cf3", + "sha256:99bd767c49c775b79fdcd2eabff405f1063d9d959039c0bdd720527a7738748a", + "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", + "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", + "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", + "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", + "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", + "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", + "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", + "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", + "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", + "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", + "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + ], + "index": "pypi", + "version": "==4.5.2" + }, + "coveralls": { + "hashes": [ + "sha256:6f213e461390973f4a97fb9e9d4ebd4956af296ff0a4d868e622108145835cb7", + "sha256:a7d0078c9e9b5692c03dcd3884647e837836c265c01e98094632feadef767d36" + ], + "index": "pypi", + "version": "==1.6.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "filelock": { + "hashes": [ + "sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", + "sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6" + ], + "version": "==3.0.10" + }, + "flake8": { + "hashes": [ + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + ], + "index": "pypi", + "version": "==3.7.7" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + ], + "markers": "python_version > '2.7'", + "version": "==6.0.0" + }, + "pkginfo": { + "hashes": [ + "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", + "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + ], + "version": "==1.5.0.1" + }, + "pluggy": { + "hashes": [ + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + ], + "version": "==0.9.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pygments": { + "hashes": [ + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + ], + "version": "==2.3.1" + }, + "pytest": { + "hashes": [ + "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", + "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4" + ], + "version": "==4.3.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", + "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" + ], + "index": "pypi", + "version": "==2.6.1" + }, + "readme-renderer": { + "hashes": [ + "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", + "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" + ], + "version": "==24.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", + "sha256:25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc" + ], + "index": "pypi", + "version": "==3.7.0" + }, + "tqdm": { + "hashes": [ + "sha256:d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021", + "sha256:e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05" + ], + "version": "==4.31.1" + }, + "twine": { + "hashes": [ + "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", + "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc" + ], + "index": "pypi", + "version": "==1.13.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, + "virtualenv": { + "hashes": [ + "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", + "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + ], + "version": "==16.4.3" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + } + } +} From 396d379429db8bfb5eb295295a0497138b287002 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 11:54:07 +0100 Subject: [PATCH 07/22] KubeConfig.from_file expands ~ automatically --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9f280bc..770daa2 100644 --- a/README.rst +++ b/README.rst @@ -190,8 +190,8 @@ You can run pykube against your current kubeconfig context, e.g. local Minikube_ $ pipenv install --dev $ pipenv run python3 - >>> import pykube, os - >>> config = pykube.KubeConfig.from_file(os.path.expanduser('~/.kube/config')) + >>> import pykube + >>> config = pykube.KubeConfig.from_file('~/.kube/config') >>> api = pykube.HTTPClient(config) >>> list(pykube.Deployment.objects(api)) From 67528f72c9d934b3e60dd06d274dc753894389b7 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 11:56:17 +0100 Subject: [PATCH 08/22] test -> tests --- {test => tests}/__init__.py | 0 {test => tests}/test_config.py | 4 ++-- {test => tests}/test_config.yaml | 0 {test => tests}/test_config_default_user.yaml | 0 {test => tests}/test_config_with_context.yaml | 0 {test => tests}/test_http.py | 2 +- {test => tests}/test_httpclient.py | 0 {test => tests}/test_session.py | 0 8 files changed, 3 insertions(+), 3 deletions(-) rename {test => tests}/__init__.py (100%) rename {test => tests}/test_config.py (95%) rename {test => tests}/test_config.yaml (100%) rename {test => tests}/test_config_default_user.yaml (100%) rename {test => tests}/test_config_with_context.yaml (100%) rename {test => tests}/test_http.py (88%) rename {test => tests}/test_httpclient.py (100%) rename {test => tests}/test_session.py (100%) diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/test/test_config.py b/tests/test_config.py similarity index 95% rename from test/test_config.py rename to tests/test_config.py index 7aa5985..d79b760 100644 --- a/test/test_config.py +++ b/tests/test_config.py @@ -9,8 +9,8 @@ from . import TestCase -GOOD_CONFIG_FILE_PATH = os.path.sep.join(["test", "test_config.yaml"]) -DEFAULTUSER_CONFIG_FILE_PATH = os.path.sep.join(["test", "test_config_default_user.yaml"]) +GOOD_CONFIG_FILE_PATH = os.path.sep.join(["tests", "test_config.yaml"]) +DEFAULTUSER_CONFIG_FILE_PATH = os.path.sep.join(["tests", "test_config_default_user.yaml"]) class TestConfig(TestCase): diff --git a/test/test_config.yaml b/tests/test_config.yaml similarity index 100% rename from test/test_config.yaml rename to tests/test_config.yaml diff --git a/test/test_config_default_user.yaml b/tests/test_config_default_user.yaml similarity index 100% rename from test/test_config_default_user.yaml rename to tests/test_config_default_user.yaml diff --git a/test/test_config_with_context.yaml b/tests/test_config_with_context.yaml similarity index 100% rename from test/test_config_with_context.yaml rename to tests/test_config_with_context.yaml diff --git a/test/test_http.py b/tests/test_http.py similarity index 88% rename from test/test_http.py rename to tests/test_http.py index 75d54f6..47e9113 100644 --- a/test/test_http.py +++ b/tests/test_http.py @@ -10,7 +10,7 @@ from pykube.http import HTTPClient from pykube.config import KubeConfig -GOOD_CONFIG_FILE_PATH = os.path.sep.join(["test", "test_config_with_context.yaml"]) +GOOD_CONFIG_FILE_PATH = os.path.sep.join(["tests", "test_config_with_context.yaml"]) def test_http(monkeypatch): diff --git a/test/test_httpclient.py b/tests/test_httpclient.py similarity index 100% rename from test/test_httpclient.py rename to tests/test_httpclient.py diff --git a/test/test_session.py b/tests/test_session.py similarity index 100% rename from test/test_session.py rename to tests/test_session.py From c6479352941c44f3be0f8543fd70f15a2c67ca7e Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 12:38:39 +0100 Subject: [PATCH 09/22] test API calls --- Pipfile | 1 + Pipfile.lock | 10 ++++++- pykube/http.py | 10 +++++-- tests/test_api.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_http.py | 2 +- 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 tests/test_api.py diff --git a/Pipfile b/Pipfile index 9d49bcb..099c57a 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,7 @@ coveralls = "*" tox = "*" coverage = "*" twine = "*" +responses = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index cbe266a..e49761b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6529102b414363c77a982a776dfede3d28f951068867b99cfbb3f8afd2832e15" + "sha256": "fa3c25be3741db0d58d003802ae34a8f5ee3dcb71275bf08e43883dd34e67902" }, "pipfile-spec": 6, "requires": { @@ -319,6 +319,14 @@ ], "version": "==0.9.1" }, + "responses": { + "hashes": [ + "sha256:c85882d2dc608ce6b5713a4e1534120f4a0dc6ec79d1366570d2b0c909a50c87", + "sha256:ea5a14f9aea173e3b786ff04cf03133c2dabd4103dbaef1028742fd71a6c2ad3" + ], + "index": "pypi", + "version": "==0.10.5" + }, "six": { "hashes": [ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", diff --git a/pykube/http.py b/pykube/http.py index 41ab4ac..7fbbd8e 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -30,9 +30,14 @@ class KubernetesHTTPAdapter(requests.adapters.HTTPAdapter): + # _do_send: the actual send method of HTTPAdapter + # it can be overwritten in unit tests to mock the actual HTTP calls + _do_send = requests.adapters.HTTPAdapter.send + def __init__(self, kube_config, **kwargs): self.kube_config = kube_config - super(KubernetesHTTPAdapter, self).__init__(**kwargs) + + super().__init__(**kwargs) def _persist_credentials(self, config, token, expiry): user_name = config.contexts[config.current_context]["user"] @@ -124,8 +129,7 @@ def send(self, request, **kwargs): elif "insecure-skip-tls-verify" in config.cluster: kwargs["verify"] = not config.cluster["insecure-skip-tls-verify"] - send = super(KubernetesHTTPAdapter, self).send - response = send(request, **kwargs) + response = self._do_send(request, **kwargs) _retry_status_codes = {http_client.UNAUTHORIZED} diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..fd867ee --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,71 @@ +import json +import pytest +import responses + +from pykube import KubeConfig, HTTPClient, Deployment + + +@pytest.fixture +def kubeconfig(tmpdir): + kubeconfig = tmpdir.join('kubeconfig') + kubeconfig.write(''' +apiVersion: v1 +clusters: +- cluster: {server: 'https://localhost:9443'} + name: test +contexts: +- context: {cluster: test, user: test} + name: test +current-context: test +kind: Config +preferences: {} +users: +- name: test + user: {token: testtoken} + ''') + return kubeconfig + + +@pytest.fixture +def requests_mock(): + return responses.RequestsMock(target='pykube.http.KubernetesHTTPAdapter._do_send') + + +def test_list_deployments(kubeconfig, monkeypatch, requests_mock): + config = KubeConfig.from_file(str(kubeconfig)) + api = HTTPClient(config) + + with requests_mock as rsps: + rsps.add(responses.GET, 'https://localhost:9443/apis/apps/v1/namespaces/default/deployments', + json={'items': []}) + + assert list(Deployment.objects(api)) == [] + assert len(rsps.calls) == 1 + # ensure that we passed the token specified in kubeconfig.. + assert rsps.calls[0].request.headers['Authorization'] == 'Bearer testtoken' + + +def test_list_and_update_deployments(kubeconfig, monkeypatch, requests_mock): + config = KubeConfig.from_file(str(kubeconfig)) + api = HTTPClient(config) + + with requests_mock as rsps: + rsps.add(responses.GET, 'https://localhost:9443/apis/apps/v1/namespaces/default/deployments', + json={'items': [{'metadata': {'name': 'deploy-1'}, 'spec': {'replicas': 3}}]}) + + deployments = list(Deployment.objects(api)) + assert len(deployments) == 1 + deploy = deployments[0] + assert deploy.name == 'deploy-1' + assert deploy.namespace == 'default' + assert deploy.replicas == 3 + + deploy.replicas = 2 + + rsps.add(responses.PATCH, 'https://localhost:9443/apis/apps/v1/namespaces/default/deployments/deploy-1', + json={'items': [{'metadata': {'name': 'deploy-1'}, 'spec': {'replicas': 2}}]}) + + deploy.update() + assert len(rsps.calls) == 2 + + assert json.loads(rsps.calls[-1].request.body) == {"metadata": {"name": "deploy-1"}, "spec": {"replicas": 2}} diff --git a/tests/test_http.py b/tests/test_http.py index 47e9113..9ad50f2 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -19,7 +19,7 @@ def test_http(monkeypatch): mock_send = MagicMock() mock_send.side_effect = Exception('MOCK HTTP') - monkeypatch.setattr('requests.adapters.HTTPAdapter.send', mock_send) + monkeypatch.setattr('pykube.http.KubernetesHTTPAdapter._do_send', mock_send) with pytest.raises(Exception): session.get('http://localhost:9090/test') From 54db8f4943c470263bbe68181a96a404d9b3a96d Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 13:02:42 +0100 Subject: [PATCH 10/22] test examples from README --- pykube/query.py | 4 +- pykube/rolling_updater.py | 173 -------------------------------------- tests/test_api.py | 60 +++++++++++-- 3 files changed, 56 insertions(+), 181 deletions(-) delete mode 100644 pykube/rolling_updater.py diff --git a/pykube/query.py b/pykube/query.py index 31d4b3b..520041a 100644 --- a/pykube/query.py +++ b/pykube/query.py @@ -186,9 +186,9 @@ def as_selector(value): elif op == "neq": s.append("{} != {}".format(label, v)) elif op == "in": - s.append("{} in ({})".format(label, ",".join(v))) + s.append("{} in ({})".format(label, ",".join(sorted(v)))) elif op == "notin": - s.append("{} notin ({})".format(label, ",".join(v))) + s.append("{} notin ({})".format(label, ",".join(sorted(v)))) else: raise ValueError("{} is not a valid comparison operator".format(op)) return ",".join(s) diff --git a/pykube/rolling_updater.py b/pykube/rolling_updater.py deleted file mode 100644 index 170d52f..0000000 --- a/pykube/rolling_updater.py +++ /dev/null @@ -1,173 +0,0 @@ -import logging -import math -import time - -from .objects import Pod -from .exceptions import KubernetesError - - -logger = logging.getLogger(__name__) - - -class RollingUpdater(object): - - def __init__(self, api, old_rc, new_rc, **kwargs): - self.api = api - self.old_rc = old_rc - self.new_rc = new_rc - self.update_period = kwargs.get("update_period", 10) - self.max_unavailable = kwargs.get("max_unavailable", 0) - self.max_surge = kwargs.get("max_surge", 1) - - def update(self): - desired = self.new_rc.replicas - original = self.old_rc.replicas - max_unavailable = extract_max_value(self.max_unavailable, "max_unavailable", desired) - max_surge = extract_max_value(self.max_surge, "max_surge", desired) - min_available = original - max_unavailable - if self.new_rc.exists(): - logger.info("ReplicationController {} already exists.".format(self.new_rc.name)) - return False - new_selector = self.new_rc.obj["spec"]["selector"] - old_selector = self.old_rc.obj["spec"]["selector"] - if new_selector == old_selector: - raise KubernetesError( - "error: {} must specify a matching key with non-equal value in Selector for {}".format( - self.new_rc.name, - self.old_rc.name - )) - new_labels = self.new_rc.obj["spec"]["template"]["metadata"]["labels"] - if new_selector != new_labels: - raise KubernetesError( - "The ReplicationController {} is invalid. spec.template.metadata.labels: Invalid value: {}: `selector` does not match template `labels` {}".format( - self.new_rc.name, - new_selector, - new_labels)) - - self.create_rc(self.new_rc) - logger.info("Created {}".format(self.new_rc.name)) - new_rc, old_rc = self.new_rc, self.old_rc - - logger.info( - "scaling up {} from {} to {}, scaling down {} from {} to 0 (keep {} pods available, don't exceed {} pods)".format( - new_rc.name, - new_rc.replicas, - desired, - old_rc.name, - old_rc.replicas, - min_available, - original + max_surge - ), - ) - - while new_rc.replicas != desired or old_rc.replicas != 0: - scaled_rc = self.scale_up( - new_rc, old_rc, - original, desired, - max_surge, max_unavailable, - ) - new_rc = scaled_rc - time.sleep(self.update_period) - scaled_rc = self.scale_down( - new_rc, old_rc, - desired, - min_available, max_surge, - ) - old_rc = scaled_rc - - logger.info("Update succeeded. Deleting {}".format(old_rc.name)) - self.cleanup(old_rc, new_rc) - - def scale_up(self, new_rc, old_rc, original, desired, max_surge, max_unavailable): - # if we're already at the desired, do nothing. - if new_rc.replicas == desired: - return new_rc - # scale up as far as we can based on the surge limit. - increment = (original + max_surge) - (old_rc.replicas + new_rc.replicas) - # if the old is already scaled down, go ahead and scale all the way up. - if old_rc.replicas == 0: - increment = desired - new_rc.replicas - # we can't scale up without violating the surge limit, so do nothing - if increment <= 0: - return new_rc - # increase the replica count, and deal with fenceposts - new_rc.replicas = min(desired, new_rc.replicas + increment) - # perform the scale up - logger.info("scaling {} up to {}".format(new_rc.name, new_rc.replicas)) - new_rc.scale() - return new_rc - - def scale_down(self, new_rc, old_rc, desired, min_available, max_surge): - # already scaled down; do nothing. - if old_rc.replicas == 0: - return old_rc - # block until there are any pods ready - _, new_available = self.poll_for_ready_pods(old_rc, new_rc) - # the old controller is considered as part of the total because we want - # to maintain minimum availability even with a volatile old controller. - # scale down as much as possible while maintaining minimum availability. - decrement = old_rc.replicas + new_available - min_available - # the decrement normally shouldn't drop below zero because the available - # count always start below the old replica count, but the old replica - # count can decrement due to externalities like pods death in the replica - # set. this will be considered a transient condition; do nothing and try - # again later with new readiness values. - # - # if the most we can scale is zero, it means we can't scale down without - # violating the minimum. do nothing and try again later when conditions - # may have changed. - if decrement <= 0: - return old_rc - # reduce the replica count, and deal with fenceposts - old_rc.replicas = max(0, old_rc.replicas - decrement) - # if the new is already fully scaled and available up to the desired size, - # go ahead and scale old all the way down - if new_rc.replicas == desired and new_available == desired: - old_rc.replicas = 0 - # perform scale down - logger.info("scaling {} down to {}".format(old_rc.name, old_rc.replicas)) - old_rc.scale() - return old_rc - - def cleanup(self, old_rc, new_rc): - old_rc.delete() - - def poll_for_ready_pods(self, old_rc, new_rc): - controllers = [old_rc, new_rc] - old_ready = 0 - new_ready = 0 - any_ready = False - - while True: - for controller in controllers: - pods = Pod.objects(self.api).filter( - namespace=controller.namespace, - selector=controller.obj["spec"]["selector"], - ) - for pod in pods: - if pod.ready: - if controller.name == old_rc.name: - old_ready += 1 - elif controller.name == new_rc.name: - new_ready += 1 - any_ready = True - if any_ready: - break - time.sleep(1) - - return old_ready, new_ready - - def create_rc(self, rc): - rc.replicas = 0 - rc.create() - - -def extract_max_value(field, name, value): - assert type(field) in {int, str}, "{} is not an int or str".format(type(field)) - if isinstance(field, int): - assert field >= 0, "{} must be >= 0".format(name) - return field - if isinstance(field, str): - v = int(field.replace("%", "")) - assert v >= 0, "{} must be >= 0".format(name) - return math.ceil(float(value) * (float(v) / 100.)) diff --git a/tests/test_api.py b/tests/test_api.py index fd867ee..0ddb075 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,9 @@ import json +import operator import pytest import responses +import pykube from pykube import KubeConfig, HTTPClient, Deployment @@ -31,10 +33,59 @@ def requests_mock(): return responses.RequestsMock(target='pykube.http.KubernetesHTTPAdapter._do_send') -def test_list_deployments(kubeconfig, monkeypatch, requests_mock): +@pytest.fixture +def api(kubeconfig): config = KubeConfig.from_file(str(kubeconfig)) - api = HTTPClient(config) + return HTTPClient(config) + + +def test_get_ready_pods(api, requests_mock): + # example from README + with requests_mock as rsps: + rsps.add(responses.GET, 'https://localhost:9443/api/v1/namespaces/gondor-system/pods', + json={'items': [ + {'metadata': {'name': 'pod-1'}, 'status': {}}, + {'metadata': {'name': 'pod-2'}, 'status': {'conditions': [{'type': 'Ready', 'status': 'True'}]}} + ]}) + pods = pykube.Pod.objects(api).filter(namespace="gondor-system") + ready_pods = list(filter(operator.attrgetter("ready"), pods)) + assert len(ready_pods) == 1 + assert ready_pods[0].name == 'pod-2' + + +def test_get_pod_by_name(api, requests_mock): + # example from README + with requests_mock as rsps: + rsps.add(responses.GET, 'https://localhost:9443/api/v1/namespaces/gondor-system/pods/my-pod', + json={'spec': {'containers': [{'image': 'hjacobs/kube-janitor'}]}}) + + pod = pykube.Pod.objects(api).filter(namespace="gondor-system").get(name="my-pod") + assert pod.obj["spec"]["containers"][0]["image"] == 'hjacobs/kube-janitor' + + +def test_selector_query(api, requests_mock): + # example from README + with requests_mock as rsps: + rsps.add(responses.GET, 'https://localhost:9443/api/v1/namespaces/gondor-system/pods?labelSelector=gondor.io%2Fname+in+%28api-web%2Capi-worker%29', + json={'items': [{'meta': {}}]}) + + pods = pykube.Pod.objects(api).filter( + namespace="gondor-system", + selector={"gondor.io/name__in": {"api-web", "api-worker"}}, + ) + assert len(list(pods)) == 1 + + rsps.add(responses.GET, 'https://localhost:9443/api/v1/namespaces/default/pods?fieldSelector=status.phase%3DPending', + json={'items': [{'meta': {}}]}) + + pending_pods = pykube.objects.Pod.objects(api).filter( + field_selector={"status.phase": "Pending"} + ) + assert len(list(pending_pods)) == 1 + + +def test_list_deployments(api, requests_mock): with requests_mock as rsps: rsps.add(responses.GET, 'https://localhost:9443/apis/apps/v1/namespaces/default/deployments', json={'items': []}) @@ -45,10 +96,7 @@ def test_list_deployments(kubeconfig, monkeypatch, requests_mock): assert rsps.calls[0].request.headers['Authorization'] == 'Bearer testtoken' -def test_list_and_update_deployments(kubeconfig, monkeypatch, requests_mock): - config = KubeConfig.from_file(str(kubeconfig)) - api = HTTPClient(config) - +def test_list_and_update_deployments(api, requests_mock): with requests_mock as rsps: rsps.add(responses.GET, 'https://localhost:9443/apis/apps/v1/namespaces/default/deployments', json={'items': [{'metadata': {'name': 'deploy-1'}, 'spec': {'replicas': 3}}]}) From 9832c03bd399286a1ffa13cc2244bf6ba657c6f0 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 13:06:45 +0100 Subject: [PATCH 11/22] test get_or_none --- tests/test_api.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 0ddb075..c961cd2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -58,10 +58,18 @@ def test_get_pod_by_name(api, requests_mock): with requests_mock as rsps: rsps.add(responses.GET, 'https://localhost:9443/api/v1/namespaces/gondor-system/pods/my-pod', json={'spec': {'containers': [{'image': 'hjacobs/kube-janitor'}]}}) + rsps.add(responses.GET, 'https://localhost:9443/api/v1/namespaces/gondor-system/pods/other-pod', + status=404) pod = pykube.Pod.objects(api).filter(namespace="gondor-system").get(name="my-pod") assert pod.obj["spec"]["containers"][0]["image"] == 'hjacobs/kube-janitor' + pod = pykube.Pod.objects(api).filter(namespace="gondor-system").get_or_none(name="my-pod") + assert pod.obj["spec"]["containers"][0]["image"] == 'hjacobs/kube-janitor' + + pod = pykube.Pod.objects(api).filter(namespace="gondor-system").get_or_none(name="other-pod") + assert pod is None + def test_selector_query(api, requests_mock): # example from README From 9b2e93d9047787b77d78c55439dfee3fe84f757b Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 13:17:25 +0100 Subject: [PATCH 12/22] use Deployment instead of ReplicationController --- Makefile | 3 +++ README.rst | 26 ++++++++++++++------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 365304c..cc8e51c 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ VERSION ?= $(shell git describe --tags --always --dirty) default: package +clean: + rm -fr build dist *egg-info + test: pipenv run flake8 pipenv run coverage run --source=pykube -m py.test diff --git a/README.rst b/README.rst index 770daa2..eaa29c4 100644 --- a/README.rst +++ b/README.rst @@ -99,21 +99,23 @@ Watch query: print(watch_event.type) # 'ADDED', 'DELETED', 'MODIFIED' print(watch_event.object) # pykube.Job object -Create a ReplicationController: +Create a Deployment: .. code:: python obj = { - "apiVersion": "v1", - "kind": "ReplicationController", + "apiVersion": "apps/v1", + "kind": "Deployment", "metadata": { - "name": "my-rc", + "name": "my-deploy", "namespace": "gondor-system" }, "spec": { "replicas": 3, "selector": { - "app": "nginx" + "matchLabels": { + "app": "nginx" + } }, "template": { "metadata": { @@ -135,27 +137,27 @@ Create a ReplicationController: } } } - pykube.ReplicationController(api, obj).create() + pykube.Deployment(api, obj).create() -Delete a ReplicationController: +Delete a Deployment: .. code:: python obj = { - "apiVersion": "v1", - "kind": "ReplicationController", + "apiVersion": "apps/v1", + "kind": "Deployment", "metadata": { - "name": "my-rc", + "name": "my-deploy", "namespace": "gondor-system" } } - pykube.ReplicationController(api, obj).delete() + pykube.Deployment(api, obj).delete() Check server version: .. code:: python - api = pykube.HTTPClient(pykube.KubeConfig.from_file("/Users//.kube/config")) + api = pykube.HTTPClient(pykube.KubeConfig.from_file("~/.kube/config")) api.version From 1328c9430c6d662931e4859d75b251a66e8fcb55 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 13:22:13 +0100 Subject: [PATCH 13/22] test create/delete --- tests/test_api.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index c961cd2..38cbb74 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -93,6 +93,62 @@ def test_selector_query(api, requests_mock): assert len(list(pending_pods)) == 1 +def test_create_delete_deployment(api, requests_mock): + # example from README + with requests_mock as rsps: + rsps.add(responses.POST, 'https://localhost:9443/apis/apps/v1/namespaces/gondor-system/deployments', + json={}) + + obj = { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "my-deploy", + "namespace": "gondor-system" + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx", + "ports": [ + {"containerPort": 80} + ] + } + ] + } + } + } + } + pykube.Deployment(api, obj).create() + + rsps.add(responses.DELETE, 'https://localhost:9443/apis/apps/v1/namespaces/gondor-system/deployments/my-deploy', + json={}) + + obj = { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "my-deploy", + "namespace": "gondor-system" + } + } + pykube.Deployment(api, obj).delete() + + def test_list_deployments(api, requests_mock): with requests_mock as rsps: rsps.add(responses.GET, 'https://localhost:9443/apis/apps/v1/namespaces/default/deployments', From 1b9362cc247f4ddf5ddbe2916696f2e834faf41e Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 13:41:05 +0100 Subject: [PATCH 14/22] pykube-ng user agent --- Makefile | 3 +++ pykube/__init__.py | 2 ++ pykube/http.py | 2 ++ setup.py | 11 ++++++++++- tests/test_http.py | 2 ++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cc8e51c..71186c2 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,6 @@ package: test upload: package pipenv run twine upload dist/pykube* + +version: + sed -i "s/__version__ = .*/__version__ = '${VERSION}'/" pykube/__init__.py diff --git a/pykube/__init__.py b/pykube/__init__.py index cee5de5..3e5bd31 100644 --- a/pykube/__init__.py +++ b/pykube/__init__.py @@ -2,6 +2,8 @@ Python client for Kubernetes """ +__version__ = 'v0.15.0-31-g1328c94-dirty' + from .config import KubeConfig # noqa from .exceptions import KubernetesError, PyKubeError, ObjectDoesNotExist # noqa from .http import HTTPClient # noqa diff --git a/pykube/http.py b/pykube/http.py index 7fbbd8e..51a41d0 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -24,6 +24,7 @@ from .exceptions import HTTPError from .utils import jsonpath_installed, jsonpath_parse +from . import __version__ _ipv4_re = re.compile(r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") @@ -160,6 +161,7 @@ def __init__(self, config): self.url = self.config.cluster["server"] session = requests.Session() + session.headers['User-Agent'] = f'pykube-ng/{__version__}' session.mount("https://", KubernetesHTTPAdapter(self.config)) session.mount("http://", KubernetesHTTPAdapter(self.config)) self.session = session diff --git a/setup.py b/setup.py index ebe5d45..b76becc 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,17 @@ import sys +from pathlib import Path from setuptools import setup, find_packages +def read_version(package): + with (Path(package) / '__init__.py').open('r') as fd: + for line in fd: + # do not use "exec" here and do manual parsing to not require deps + if line.startswith('__version__ = '): + return line.split()[-1].strip().strip('\'') + + with open("README.rst") as fp: long_description = fp.read() @@ -20,7 +29,7 @@ setup( name="pykube-ng", - version="0.17a1", + version=read_version('pykube'), description="Python client library for Kubernetes", long_description=long_description, long_description_content_type='text/x-rst', diff --git a/tests/test_http.py b/tests/test_http.py index 9ad50f2..88163af 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock +from pykube import __version__ from pykube.http import HTTPClient from pykube.config import KubeConfig @@ -26,3 +27,4 @@ def test_http(monkeypatch): mock_send.assert_called_once() assert mock_send.call_args[0][0].headers['Authorization'] == 'Basic YWRtOnNvbWVwYXNzd29yZA==' + assert mock_send.call_args[0][0].headers['User-Agent'] == f'pykube-ng/{__version__}' From 3bf2fd92ea3944514efc68f9295df179d2296192 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 13:43:45 +0100 Subject: [PATCH 15/22] v0.17a2 --- README.rst | 2 +- pykube/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index eaa29c4..7de7798 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,7 @@ Query for all ready pods in a custom namespace: import operator import pykube - api = pykube.HTTPClient(pykube.KubeConfig.from_file("/Users//.kube/config")) + api = pykube.HTTPClient(pykube.KubeConfig.from_file("~/.kube/config")) pods = pykube.Pod.objects(api).filter(namespace="gondor-system") ready_pods = filter(operator.attrgetter("ready"), pods) diff --git a/pykube/__init__.py b/pykube/__init__.py index 3e5bd31..6485f4f 100644 --- a/pykube/__init__.py +++ b/pykube/__init__.py @@ -2,7 +2,7 @@ Python client for Kubernetes """ -__version__ = 'v0.15.0-31-g1328c94-dirty' +__version__ = '0.17a2' from .config import KubeConfig # noqa from .exceptions import KubernetesError, PyKubeError, ObjectDoesNotExist # noqa From 1a0d30ca50048354b4d6486a7ad71dfef46b3a2a Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 13:55:00 +0100 Subject: [PATCH 16/22] test obj_merge --- tests/test_utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/test_utils.py diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..9b0c82a --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,9 @@ +from pykube.utils import obj_merge + + +def test_obj_merge(): + assert obj_merge({}, {}) == {} + assert obj_merge({'a': 1}, {}) == {'a': 1} + assert obj_merge({}, {'b': 2}) == {'b': 2} + assert obj_merge({'a': []}, {'a': []}) == {'a': []} + assert obj_merge({'a': [1, 2]}, {'a': [3, 4]}) == {'a': [1, 2]} From 566d8e2a2bfc207cc6ea508713a22c5682364c6f Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 14:05:21 +0100 Subject: [PATCH 17/22] test loading config from SA --- tests/test_config.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index d79b760..f5b9f0a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,6 +3,9 @@ """ import os +import pytest + +from pathlib import Path from pykube import config, exceptions @@ -13,6 +16,36 @@ DEFAULTUSER_CONFIG_FILE_PATH = os.path.sep.join(["tests", "test_config_default_user.yaml"]) +def test_from_service_account_no_file(tmpdir): + with pytest.raises(FileNotFoundError): + config.KubeConfig.from_service_account(path=str(tmpdir)) + + +def test_from_service_account_(tmpdir): + token_file = Path(tmpdir) / 'token' + ca_file = Path(tmpdir) / 'ca.crt' + + with token_file.open('w') as fd: + fd.write('mytok') + + with ca_file.open('w') as fd: + fd.write('myca') + + os.environ['KUBERNETES_SERVICE_HOST'] = '127.0.0.1' + os.environ['KUBERNETES_SERVICE_PORT'] = '9443' + + cfg = config.KubeConfig.from_service_account(path=str(tmpdir)) + + assert cfg.doc['clusters'][0]['cluster'] == {'server': 'https://127.0.0.1:9443', 'certificate-authority': str(ca_file)} + assert cfg.doc['users'][0]['user']['token'] == 'mytok' + + +def test_from_url(): + cfg = config.KubeConfig.from_url('http://localhost:8080') + assert cfg.doc['clusters'][0]['cluster'] == {'server': 'http://localhost:8080'} + assert 'users' not in cfg.doc + + class TestConfig(TestCase): def setUp(self): From fe34cf9fc4d6dd47b8e551d4e43fd3b5959d879d Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 14:17:52 +0100 Subject: [PATCH 18/22] remove HTTPie plugin (for now) --- README.rst | 14 -------------- pykube/contrib/__init__.py | 0 pykube/contrib/httpie_plugin.py | 32 -------------------------------- setup.py | 5 ----- 4 files changed, 51 deletions(-) delete mode 100644 pykube/contrib/__init__.py delete mode 100644 pykube/contrib/httpie_plugin.py diff --git a/README.rst b/README.rst index 7de7798..b56466e 100644 --- a/README.rst +++ b/README.rst @@ -161,20 +161,6 @@ Check server version: api.version -HTTPie ------- - -pykube can be used together with HTTPie for Kubernetes command line querying goodness. For example: - -.. code:: shell - - pip install httpie - http pykube://minikube/api/v1/services - -The above example will construct an HTTP request to the cluster behind the ``minikube`` context and -show you the response containing all services. - - Requirements ------------ diff --git a/pykube/contrib/__init__.py b/pykube/contrib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pykube/contrib/httpie_plugin.py b/pykube/contrib/httpie_plugin.py deleted file mode 100644 index b78ee66..0000000 --- a/pykube/contrib/httpie_plugin.py +++ /dev/null @@ -1,32 +0,0 @@ -import inspect - -from httpie.client import HTTPieHTTPAdapter -from httpie.compat import urlsplit -from httpie.plugins import TransportPlugin - -import pykube - -from pykube.http import KubernetesHTTPAdapterSendMixin - - -class PyKubeAdapter(KubernetesHTTPAdapterSendMixin, HTTPieHTTPAdapter): - - def send(self, request, **kwargs): - u = urlsplit(request.url) - context = u.netloc - config = pykube.KubeConfig.from_file("~/.kube/config", current_context=context) - request.url = config.cluster["server"] + u.path - kwargs["kube_config"] = config - return super(PyKubeAdapter, self).send(request, **kwargs) - - -class PyKubeTransportPlugin(TransportPlugin): - - name = "PyKube Transport" - description = "Authenticates against a Kubernetes cluster API" - prefix = "pykube://" - - def get_adapter(self): - # HACK work around not being given the ssl_version from httpie - ssl_version = inspect.stack()[1][0].f_locals.get("ssl_version") - return PyKubeAdapter(ssl_version=ssl_version) diff --git a/setup.py b/setup.py index b76becc..4285520 100644 --- a/setup.py +++ b/setup.py @@ -50,11 +50,6 @@ def read_version(package): ], zip_safe=False, packages=find_packages(), - entry_points={ - "httpie.plugins.transport.v1": [ - "httpie_pykube = pykube.contrib.httpie_plugin:PyKubeTransportPlugin" - ], - }, install_requires=install_requires, extras_require={ "gcp": [ From aadd6cddd446814f08da1d91a3a277d95dcdcea0 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 14:21:34 +0100 Subject: [PATCH 19/22] test get K8s version --- tests/test_api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 38cbb74..6275c62 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -39,6 +39,13 @@ def api(kubeconfig): return HTTPClient(config) +def test_api_version(api, requests_mock): + with requests_mock as rsps: + rsps.add(responses.GET, 'https://localhost:9443/version/', + json={'major': 1, 'minor': 13}) + assert api.version == (1, 13) + + def test_get_ready_pods(api, requests_mock): # example from README with requests_mock as rsps: From 84adb3976868a89a164cd93bf6aa58c983f5474d Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 14:26:41 +0100 Subject: [PATCH 20/22] test basic attributes --- tests/test_objects.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/test_objects.py diff --git a/tests/test_objects.py b/tests/test_objects.py new file mode 100644 index 0000000..034b4a9 --- /dev/null +++ b/tests/test_objects.py @@ -0,0 +1,10 @@ +import pykube + + +def test_api_object(): + pod = pykube.Pod(None, {'metadata': {'name': 'myname'}}) + assert repr(pod) == '' + assert str(pod) == 'myname' + assert pod.metadata == {'name': 'myname'} + assert pod.labels == {} + assert pod.annotations == {} From c3ff3e01e89958e15ec47fc2d78a423194345af7 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 14:33:59 +0100 Subject: [PATCH 21/22] remove Python 2 compatibility (six) --- pykube/http.py | 6 +++--- pykube/objects.py | 4 +--- pykube/query.py | 5 ++--- pykube/utils.py | 2 +- setup.py | 4 +--- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/pykube/http.py b/pykube/http.py index 51a41d0..7acbb7f 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -18,8 +18,8 @@ import requests.adapters -from six.moves import http_client -from six.moves.urllib.parse import urlparse +from http import HTTPStatus +from urllib.parse import urlparse from .exceptions import HTTPError from .utils import jsonpath_installed, jsonpath_parse @@ -132,7 +132,7 @@ def send(self, request, **kwargs): response = self._do_send(request, **kwargs) - _retry_status_codes = {http_client.UNAUTHORIZED} + _retry_status_codes = {HTTPStatus.UNAUTHORIZED} if response.status_code in _retry_status_codes and retry_func and _retry_attempt < 2: send_kwargs = { diff --git a/pykube/objects.py b/pykube/objects.py index 59fbde9..f977b63 100644 --- a/pykube/objects.py +++ b/pykube/objects.py @@ -2,9 +2,8 @@ import json import os.path as op from inspect import getmro -import six -from six.moves.urllib.parse import urlencode +from urllib.parse import urlencode from .exceptions import ObjectDoesNotExist from .mixins import ReplicatedMixin, ScalableMixin from .query import Query @@ -23,7 +22,6 @@ def __get__(self, obj, api_obj_class): return self -@six.python_2_unicode_compatible class APIObject(object): objects = ObjectManager() diff --git a/pykube/query.py b/pykube/query.py index 520041a..c4d1844 100644 --- a/pykube/query.py +++ b/pykube/query.py @@ -2,8 +2,7 @@ from collections import namedtuple -from six import string_types -from six.moves.urllib.parse import urlencode +from urllib.parse import urlencode from .exceptions import ObjectDoesNotExist @@ -168,7 +167,7 @@ def __iter__(self): def as_selector(value): - if isinstance(value, string_types): + if isinstance(value, str): return value s = [] for k, v in value.items(): diff --git a/pykube/utils.py b/pykube/utils.py index 2407c7c..42c15e1 100644 --- a/pykube/utils.py +++ b/pykube/utils.py @@ -6,7 +6,7 @@ except ImportError: jsonpath_installed = False -from six.moves import zip_longest +from itertools import zip_longest empty = object() diff --git a/setup.py b/setup.py index 4285520..9ed234b 100644 --- a/setup.py +++ b/setup.py @@ -17,9 +17,7 @@ def read_version(package): install_requires = [ "requests>=2.12", - "PyYAML", - "six>=1.10.0", - "tzlocal", + "PyYAML" ] if sys.version_info < (3,): From a568afb15cb31cf38b00e6c94a16bf83477f35a6 Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Sun, 3 Mar 2019 14:50:38 +0100 Subject: [PATCH 22/22] update README --- Makefile | 2 +- README.rst | 35 +++++++++++++---------------------- tox.ini | 3 +-- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 71186c2..c26085f 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ VERSION ?= $(shell git describe --tags --always --dirty) default: package clean: - rm -fr build dist *egg-info + rm -fr build dist *egg-info .tox/ .cache/ .pytest_cache/ test: pipenv run flake8 diff --git a/README.rst b/README.rst index b56466e..a9e2b55 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -pykube -====== +pykube-ng +========= .. image:: https://img.shields.io/travis/hjacobs/pykube.svg :target: https://travis-ci.org/hjacobs/pykube @@ -8,9 +8,6 @@ pykube :target: https://coveralls.io/github/hjacobs/pykube?branch=master :alt: Code Coverage -.. image:: https://img.shields.io/pypi/dm/pykube-ng.svg - :target: https://pypi.python.org/pypi/pykube-ng/ - .. image:: https://img.shields.io/pypi/v/pykube-ng.svg :target: https://pypi.python.org/pypi/pykube-ng/ @@ -20,26 +17,21 @@ pykube .. image:: https://img.shields.io/badge/license-apache-blue.svg :target: https://pypi.python.org/pypi/pykube-ng/ -**IMPORTANT**: This is a fork of `kelproject/pykube `_ which is no longer maintained (archived)! - -Python client library for Kubernetes - -.. image:: https://storage.googleapis.com/kel-assets/kel_full-02_200.jpg - :target: http://kelproject.com/ +Python client library for Kubernetes. -Kel is an open source Platform as a Service (PaaS) from Eldarion, Inc. that -makes it easy to manage web application deployment and hosting through the -entire lifecycle from development through testing to production. It adds -components and tools on top of Kubernetes that help developers manage their -application infrastructure. Kel builds on Eldarion's 7+ years experience running -one of the leading Python and Django PaaSes. +This is a fork of `kelproject/pykube `_ which is no longer maintained (archived). Here the original text of the pykube README: -For more information about Kel, see `kelproject.com`_, follow us on Twitter -`@projectkel`_, and join our `Slack team`_. + Kel is an open source Platform as a Service (PaaS) from Eldarion, Inc. that + makes it easy to manage web application deployment and hosting through the + entire lifecycle from development through testing to production. It adds + components and tools on top of Kubernetes that help developers manage their + application infrastructure. Kel builds on Eldarion's 7+ years experience running + one of the leading Python and Django PaaSes. + For more information about Kel, see `kelproject.com`_ or follow us on Twitter + `@projectkel`_. .. _kelproject.com: http://kelproject.com/ .. _@projectkel: https://twitter.com/projectkel -.. _Slack team: http://slack.kelproject.com/ Features -------- @@ -209,8 +201,7 @@ PRs are welcome. Please also have a look at `issues labeled with "help wanted"`_ Code of Conduct ---------------- -In order to foster a kind, inclusive, and harassment-free community, the Kel -Project follows the `Contributor Covenant Code of Conduct`_. +In order to foster a kind, inclusive, and harassment-free community, this project follows the `Contributor Covenant Code of Conduct`_. .. _Contributor Covenant Code of Conduct: http://contributor-covenant.org/version/1/4/ diff --git a/tox.ini b/tox.ini index 5ca52dc..1cfaf28 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,9 @@ [flake8] -ignore = E265,E501,W504 +ignore = E265, W504 max-line-length=160 [tox] envlist = - py27, py36 [testenv]