diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000000..ad1b1203dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Share about things that are not working as expected +labels: kind/bug + +--- + +**What happened (please include outputs or screenshots)**: + +**What you expected to happen**: + +**How to reproduce it (as minimally and precisely as possible)**: + +**Anything else we need to know?**: + +**Environment**: +- Kubernetes version (`kubectl version`): +- OS (e.g., MacOS 10.13.6): +- Python version (`python --version`) +- Python client version (`pip list | grep kubernetes`) diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000000..75bef6b7d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,10 @@ +--- +name: Documentation +about: Report any mistakes or missing information from the documentation or the examples +labels: kind/documentation + +--- + +**Link to the issue (please include a link to the specific documentation or example)**: + +**Description of the issue (please include outputs or screenshots if possible)**: diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000000..466b4f87ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,10 @@ +--- +name: Feature request +about: Suggest a new feature for the project +labels: kind/feature + +--- + +**What is the feature and why do you need it**: + +**Describe the solution you'd like to see**: diff --git a/.gitignore b/.gitignore index bdd5055d15..a5e2becbbb 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ target/ .idea/* *.iml .vscode + +# created by sphinx documentation build +doc/source/README.md +doc/_build diff --git a/.travis.yml b/.travis.yml index fce474b421..00a7ba50f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ # ref: https://docs.travis-ci.com/user/languages/python language: python dist: xenial -sudo: true services: - docker @@ -13,12 +12,10 @@ matrix: env: TOXENV=py27-functional - python: 2.7 env: TOXENV=update-pycodestyle - - python: 2.7 + - python: 3.7 env: TOXENV=docs - python: 2.7 env: TOXENV=coverage,codecov - - python: 3.4 - env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 - python: 3.5 @@ -31,6 +28,10 @@ matrix: env: TOXENV=py37 - python: 3.7 env: TOXENV=py37-functional + - python: 3.8 + env: TOXENV=py38 + - python: 3.8 + env: TOXENV=py38-functional install: - pip install tox diff --git a/CHANGELOG.md b/CHANGELOG.md index a7fff70fed..b1e119d3cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Introduce RuntimeClass to NodeV1alpha1Api and NodeV1beta1Api [kubernetes/kubernetes#74433](https://github.com/kubernetes/kubernetes/pull/74433) - Graduate PriorityClass API to GA SchedulingV1Api [kubernetes/kubernetes#73555](https://github.com/kubernetes/kubernetes/pull/73555) - Introduce CSINodeInfo and CSIDriver to StorageV1beta1Api [kubernetes/kubernetes#74283](https://github.com/kubernetes/kubernetes/pull/74283) +- The alpha Initializers feature, `admissionregistration.k8s.io/v1alpha1` API version, `Initializers` admission plugin, and use of the `metadata.initializers` API field have been removed. Discontinue use of the alpha feature and delete any existing `InitializerConfiguration` API objects before upgrading. The `metadata.initializers` field will be removed in a future release. The parameter `include_uninitialized` has been removed. [kubernetes/kubernetes#72972](https://github.com/kubernetes/kubernetes/pull/72972) # v9.0.0 **Bug Fix:** diff --git a/OWNERS b/OWNERS index dd6cbaeb28..605d99b551 100644 --- a/OWNERS +++ b/OWNERS @@ -1,8 +1,11 @@ # See the OWNERS docs at https://go.k8s.io/owners approvers: - - mbohlool + - roycaihw + - yliaog +emeritus_approvers: - caesarxuchao - lavalamp - - yliaog - - roycaihw + - mbohlool +reviewers: + - micw523 diff --git a/README.md b/README.md index 8af2972797..8a4f010f10 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ From [PyPi](https://pypi.python.org/pypi/kubernetes/) directly: pip install kubernetes ``` -## Example +## Examples list all pods: @@ -68,7 +68,7 @@ More examples can be found in [examples](examples/) folder. To run examples, run python -m examples.example1 ``` -(replace example1 with the example base filename) +(replace example1 with one of the filenames in the examples folder) ## Documentation @@ -77,24 +77,20 @@ All APIs and Models' documentation can be found at the [Generated client's READM ## Compatibility `client-python` follows [semver](http://semver.org/), so until the major version of -client-python gets increased, your code will continue to work with explicitly +client-python gets increased, your code will continue to work with explicitly supported versions of Kubernetes clusters. #### Compatibility matrix -| | Kubernetes 1.5 | Kubernetes 1.6 | Kubernetes 1.7 | Kubernetes 1.8 | Kubernetes 1.9 | Kubernetes 1.10 | Kubernetes 1.11 | Kubernetes 1.12 | Kubernetes 1.13 | Kubernetes 1.14 | -|--------------------|----------------|----------------|----------------|----------------|----------------|-----------------|-----------------|-----------------|-----------------|-----------------| -| client-python 1.0 | ✓ | - | - |- |- |- |- |- |- |- | -| client-python 2.0 | + | ✓ | - |- |- |- |- |- |- |- | -| client-python 3.0 | + | + | ✓ |- |- |- |- |- |- |- | -| client-python 4.0 | + | + | + |✓ |- |- |- |- |- |- | -| client-python 5.0 | + | + | + |+ |✓ |- |- |- |- |- | -| client-python 6.0 | + | + | + |+ |+ |✓ |- |- |- |- | -| client-python 7.0 | + | + | + |+ |+ |+ |✓ |- |- |- | -| client-python 8.0 | + | + | + |+ |+ |+ |+ |✓ |- |- | -| client-python 9.0 | + | + | + |+ |+ |+ |+ |+ |✓ |- | -| client-python 10.0 | + | + | + |+ |+ |+ |+ |+ |+ |✓ | -| client-python HEAD | + | + | + |+ |+ |+ |+ |+ |+ |✓ | +| | Kubernetes 1.9 | Kubernetes 1.10 | Kubernetes 1.11 | Kubernetes 1.12 | Kubernetes 1.13 | Kubernetes 1.14 | +|--------------------|----------------|-----------------|-----------------|-----------------|-----------------|-----------------| +| client-python 5.0 |✓ |- |- |- |- |- | +| client-python 6.0 |+ |✓ |- |- |- |- | +| client-python 7.0 |+ |+ |✓ |- |- |- | +| client-python 8.0 |+ |+ |+ |✓ |- |- | +| client-python 9.0 |+ |+ |+ |+ |✓ |- | +| client-python 10.0 |+ |+ |+ |+ |+ |✓ | +| client-python HEAD |+ |+ |+ |+ |+ |✓ | Key: @@ -110,14 +106,6 @@ between client-python versions. | Client version | Canonical source for OpenAPI spec | Maintenance status | |-----------------|--------------------------------------|-------------------------------| -| 1.0 Alpha/Beta | Kubernetes main repo, 1.5 branch | ✗ | -| 1.0.x | Kubernetes main repo, 1.5 branch | ✗ | -| 2.0 Alpha/Beta | Kubernetes main repo, 1.6 branch | ✗ | -| 2.0.x | Kubernetes main repo, 1.6 branch | ✗ | -| 3.0 Alpha/Beta | Kubernetes main repo, 1.7 branch | ✗ | -| 3.0 | Kubernetes main repo, 1.7 branch | ✗ | -| 4.0 Alpha/Beta | Kubernetes main repo, 1.8 branch | ✗ | -| 4.0 | Kubernetes main repo, 1.8 branch | ✗ | | 5.0 Alpha/Beta | Kubernetes main repo, 1.9 branch | ✗ | | 5.0 | Kubernetes main repo, 1.9 branch | ✗ | | 6.0 Alpha/Beta | Kubernetes main repo, 1.10 branch | ✗ | @@ -142,18 +130,12 @@ Note: There would be no maintenance for alpha/beta releases except the latest on ## Community, Support, Discussion -If you have any problem on using the package or any suggestions, please start with reaching the [Kubernetes clients slack channel](https://kubernetes.slack.com/messages/C76GB48RK/), or filing an [issue](https://github.com/kubernetes-client/python/issues) to let us know. You can also reach the maintainers of this project at [SIG API Machinery](https://github.com/kubernetes/community/tree/master/sig-api-machinery). +If you have any problem on using the package or any suggestions, please start with reaching the [Kubernetes clients slack channel](https://kubernetes.slack.com/messages/C76GB48RK/), or filing an [issue](https://github.com/kubernetes-client/python/issues) to let us know. You can also reach the maintainers of this project at [SIG API Machinery](https://github.com/kubernetes/community/tree/master/sig-api-machinery), where this project falls under. ### Code of Conduct Participation in the Kubernetes community is governed by the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). -## Kubernetes Incubator - -This is a [Kubernetes Incubator project](https://github.com/kubernetes/community/blob/master/incubator.md). - -* [SIG: sig-api-machinery](https://github.com/kubernetes/community/tree/master/sig-api-machinery) - ## Troubleshooting ### SSLError on macOS @@ -184,9 +166,9 @@ Specifically check `ipaddress` and `urllib3` package versions to make sure they Starting from 4.0 release, we do not support directly calling exec or attach calls. you should use stream module to call them. so instead of `resp = api.connect_get_namespaced_pod_exec(name, ...` you should call `resp = stream(api.connect_get_namespaced_pod_exec, name, ...`. -See more at [exec example](examples/exec.py). Using Stream will overwrite the requests protocol in _core_v1_api.CoreV1Api()_ This will cause a failure in non-exec/attach calls. If you reuse your api client object, you will need to recreate it between api calls that use _stream_ and other api calls. +See more at [exec example](examples/pod_exec.py). diff --git a/RELEASE.md b/RELEASE.md index 824f13a4e9..0b0eed35e3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,7 +3,11 @@ The Kubernetes Python client is released on an as-needed basis. The process is as follows: 1. An issue is proposing a new release with a changelog since the last release +1. Update the support matrix in README.md removing the oldest version and adding the + proposed release. 1. All [OWNERS](OWNERS) must LGTM this release -1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` +1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes + the tag with `git push $VERSION` 1. The release issue is closed -1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] kubernetes-python-client $VERSION is released` +1. An announcement email is sent to `kubernetes-dev@googlegroups.com` + with the subject `[ANNOUNCE] kubernetes-python-client $VERSION is released` diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000000..d5b5edf760 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,21 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -c source +SPHINXBUILD = sphinx-build +SPHINXPROJ = kubernetes-python +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +html: + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo "\nDocs rendered successfully, open _/build/html/index.html to view" diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000000..5b635c577c --- /dev/null +++ b/doc/README.md @@ -0,0 +1,11 @@ +Building the documentation +========================== + +Install the test requirements with: + +``` +$ pip install -r ../test-requirements.txt +``` + +Use `make html` to build the docs in html format. + diff --git a/doc/requirements-docs.txt b/doc/requirements-docs.txt new file mode 100644 index 0000000000..eb69200af2 --- /dev/null +++ b/doc/requirements-docs.txt @@ -0,0 +1,2 @@ +recommonmark +sphinx_markdown_tables diff --git a/doc/source/conf.py b/doc/source/conf.py index a5f0a1fc66..2d1d6acc97 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,22 +13,37 @@ # limitations under the License. import os +import re +import shutil import sys -from recommonmark.parser import CommonMarkParser +from recommonmark.transform import AutoStructify + +# Work around https://github.com/readthedocs/recommonmark/issues/152 +new_readme = [] + +with open("../../README.md", "r") as r: + lines = r.readlines() + for l in lines: + nl = re.sub("\[!\[[\w\s]+\]\(", "[![](", l) + new_readme.append(nl) + +with open("README.md", "w") as n: + n.writelines(new_readme) + +# apparently index.rst can't search for markdown not in the same directory +shutil.copy("../../CONTRIBUTING.md", ".") sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- -source_parsers = { - '.md': CommonMarkParser, -} - source_suffix = ['.rst', '.md'] # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + 'sphinx_markdown_tables', + 'recommonmark', 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', ] @@ -80,3 +95,10 @@ # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'http://docs.python.org/': None} +def setup(app): + app.add_config_value('recommonmark_config', { + 'auto_toc_tree_section': 'Contents', + 'enable_eval_rst': True, + }, True) + app.add_transform(AutoStructify) + diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst deleted file mode 100644 index 2a47898895..0000000000 --- a/doc/source/contributing.rst +++ /dev/null @@ -1,4 +0,0 @@ -============ -Contributing -============ -.. include:: ../../CONTRIBUTING.md diff --git a/doc/source/index.rst b/doc/source/index.rst index fc6b82629e..83a649b8ef 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,11 +11,12 @@ Contents: .. toctree:: :maxdepth: 2 - readme + README installation usage + examples modules - contributing + contributing Indices and tables ================== diff --git a/doc/source/readme.rst b/doc/source/readme.rst deleted file mode 100644 index 77f974f645..0000000000 --- a/doc/source/readme.rst +++ /dev/null @@ -1,4 +0,0 @@ -====== -Readme -====== -.. include:: ../../README.md diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 6f67af9302..114306a8cf 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -2,6 +2,19 @@ Usage ======== -To use kubernetes-python-client in a project:: +The directory ``examples`` contains a few examples on how to use the client. - import kubernetes + +Deployments +----------- + +Here is a simple usage of creating a deployment from a yaml file: + + +.. literalinclude:: ../../examples/create_deployment.py + + +The following example demostrates how to create, update and delete deployments +without the need to read a file from the disk: + +.. literalinclude:: ../../examples/deployment_examples.py diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..0c4d513e8d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,17 @@ +# Python Client Examples + +This directory contains various examples of how to use the Python client. +Please read the description at the top of each example for more information +about what the script does and any prequisites. Most scripts also include +comments throughout the code. + +## Setup + +These scripts require Python 2.7 or 3.5+ and the Kubernetes client which can be +installed following the directions +[here](https://github.com/kubernetes-client/python#installation). + +## Contributions + +If you find a problem please file an +[issue](https://github.com/kubernetes-client/python/issues). diff --git a/examples/__init__.py b/examples/__init__.py index 13d0123d14..a010399d95 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Empty init file to make examples folder a python module. +# Empty init file to make examples folder a python module diff --git a/examples/example3.py b/examples/api_discovery.py similarity index 91% rename from examples/example3.py rename to examples/api_discovery.py index 25c0d9a807..9c91fe429c 100644 --- a/examples/example3.py +++ b/examples/api_discovery.py @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Reads the list of available API versions and prints them. Similar to running +`kubectl api-versions`. +""" + from kubernetes import client, config @@ -22,7 +27,7 @@ def main(): config.load_kube_config() print("Supported APIs (* is preferred version):") - print("%-20s %s" % + print("%-40s %s" % ("core", ",".join(client.CoreApi().get_api_versions().versions))) for api in client.ApisApi().get_api_versions().groups: versions = [] diff --git a/examples/create_deployment_from_yaml.py b/examples/create_deployment_from_yaml.py deleted file mode 100644 index dcb7ac14d7..0000000000 --- a/examples/create_deployment_from_yaml.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2018 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from os import path - -from kubernetes import client, config, utils - - -def main(): - # Configs can be set in Configuration class directly or using helper - # utility. If no argument provided, the config will be loaded from - # default location. - config.load_kube_config() - k8s_client = client.ApiClient() - utils.create_from_yaml(k8s_client, "nginx-deployment.yaml") - k8s_api = client.ExtensionsV1beta1Api(k8s_client) - deps = k8s_api.read_namespaced_deployment("nginx-deployment", "default") - print("Deployment {0} created".format(deps.metadata.name)) - - -if __name__ == '__main__': - main() diff --git a/examples/create_thirdparty_resource.md b/examples/create_thirdparty_resource.md deleted file mode 100644 index ec5975ad0a..0000000000 --- a/examples/create_thirdparty_resource.md +++ /dev/null @@ -1,40 +0,0 @@ -## Creating a Third Party Resource - -``` -from __future__ import print_function - -from pprint import pprint - -import kubernetes -from kubernetes import config -from kubernetes.rest import ApiException - -config.load_kube_config() -api_instance = kubernetes.ThirdPartyResources() - -namespace = 'default' -resource = 'repos' -fqdn = 'git.k8s.com' - -body = {} -body['apiVersion'] = "git.k8s.com/v1" -body['kind'] = "RePo" -body['metadata'] = {} -body['metadata']['name'] = "blog-repo" -body['repo'] = "github.com/user/my-blog" -body['username'] = "username" -body['password'] = "password" -body['branch'] = "branch" - - - -try: - # Create a Resource - api_response = api_instance.apis_fqdn_v1_namespaces_namespace_resource_post( - namespace, fqdn, resource, body) - pprint(api_response) -except ApiException as e: - print( - "Exception when calling DefaultApi->apis_fqdn_v1_namespaces_namespace_resource_post: %s\n" % - e) -``` \ No newline at end of file diff --git a/examples/custom_object.py b/examples/custom_object.py new file mode 100644 index 0000000000..0c2b36aefb --- /dev/null +++ b/examples/custom_object.py @@ -0,0 +1,94 @@ +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Uses a Custom Resource Definition (CRD) to create a custom object, in this case +a CronTab. This example use an example CRD from this tutorial: +https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ + +The following yaml manifest has to be applied first: + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: crontabs.stable.example.com +spec: + group: stable.example.com + versions: + - name: v1 + served: true + storage: true + scope: Namespaced + names: + plural: crontabs + singular: crontab + kind: CronTab + shortNames: + - ct +""" + +from pprint import pprint + +from kubernetes import client, config + + +def main(): + config.load_kube_config() + + api = client.CustomObjectsApi() + + # it's my custom resource defined as Dict + my_resource = { + "apiVersion": "stable.example.com/v1", + "kind": "CronTab", + "metadata": {"name": "my-new-cron-object"}, + "cronSpec": "* * * * */5", + "image": "my-awesome-cron-image", + } + + # create the resource + api.create_namespaced_custom_object( + group="stable.example.com", + version="v1", + namespace="default", + plural="crontabs", + body=my_resource, + ) + print("Resource created") + + # get the resource and print out data + resource = api.get_namespaced_custom_object( + group="stable.example.com", + version="v1", + name="my-new-cron-object", + namespace="default", + plural="crontabs", + ) + print("Resource details:") + pprint(resource) + + # delete it + api.delete_namespaced_custom_object( + group="stable.example.com", + version="v1", + name="my-new-cron-object", + namespace="default", + plural="crontabs", + body=client.V1DeleteOptions(), + ) + print("Resource deleted") + + +if __name__ == "__main__": + main() diff --git a/examples/create_deployment.py b/examples/deployment_create.py similarity index 86% rename from examples/create_deployment.py rename to examples/deployment_create.py index 0ce1e2fa1d..ba13440ff8 100644 --- a/examples/create_deployment.py +++ b/examples/deployment_create.py @@ -27,10 +27,10 @@ def main(): with open(path.join(path.dirname(__file__), "nginx-deployment.yaml")) as f: dep = yaml.safe_load(f) - k8s_beta = client.ExtensionsV1beta1Api() - resp = k8s_beta.create_namespaced_deployment( + k8s_apps_v1 = client.AppsV1Api() + resp = k8s_apps_v1.create_namespaced_deployment( body=dep, namespace="default") - print("Deployment created. status='%s'" % str(resp.status)) + print("Deployment created. status='%s'" % resp.metadata.name) if __name__ == '__main__': diff --git a/examples/deployment_examples.py b/examples/deployment_crud.py similarity index 86% rename from examples/deployment_examples.py rename to examples/deployment_crud.py index 29f55d34d3..a25bb518aa 100644 --- a/examples/deployment_examples.py +++ b/examples/deployment_crud.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from os import path - -import yaml +""" +Creates, updates, and deletes a deployment using AppsV1Api. +""" from kubernetes import client, config @@ -25,19 +25,20 @@ def create_deployment_object(): # Configureate Pod template container container = client.V1Container( name="nginx", - image="nginx:1.7.9", + image="nginx:1.15.4", ports=[client.V1ContainerPort(container_port=80)]) # Create and configurate a spec section template = client.V1PodTemplateSpec( metadata=client.V1ObjectMeta(labels={"app": "nginx"}), spec=client.V1PodSpec(containers=[container])) # Create the specification of deployment - spec = client.ExtensionsV1beta1DeploymentSpec( + spec = client.V1DeploymentSpec( replicas=3, - template=template) + template=template, + selector={'matchLabels': {'app': 'nginx'}}) # Instantiate the deployment object - deployment = client.ExtensionsV1beta1Deployment( - api_version="extensions/v1beta1", + deployment = client.V1Deployment( + api_version="apps/v1", kind="Deployment", metadata=client.V1ObjectMeta(name=DEPLOYMENT_NAME), spec=spec) @@ -55,7 +56,7 @@ def create_deployment(api_instance, deployment): def update_deployment(api_instance, deployment): # Update container image - deployment.spec.template.spec.containers[0].image = "nginx:1.9.1" + deployment.spec.template.spec.containers[0].image = "nginx:1.16.0" # Update the deployment api_response = api_instance.patch_namespaced_deployment( name=DEPLOYMENT_NAME, @@ -80,16 +81,16 @@ def main(): # utility. If no argument provided, the config will be loaded from # default location. config.load_kube_config() - extensions_v1beta1 = client.ExtensionsV1beta1Api() + apps_v1 = client.AppsV1Api() # Create a deployment object with client-python API. The deployment we # created is same as the `nginx-deployment.yaml` in the /examples folder. deployment = create_deployment_object() - create_deployment(extensions_v1beta1, deployment) + create_deployment(apps_v1, deployment) - update_deployment(extensions_v1beta1, deployment) + update_deployment(apps_v1, deployment) - delete_deployment(extensions_v1beta1) + delete_deployment(apps_v1) if __name__ == '__main__': diff --git a/examples/exec.py b/examples/exec.py deleted file mode 100644 index d1f9e9e301..0000000000 --- a/examples/exec.py +++ /dev/null @@ -1,97 +0,0 @@ -import time - -from kubernetes import config -from kubernetes.client import Configuration -from kubernetes.client.apis import core_v1_api -from kubernetes.client.rest import ApiException -from kubernetes.stream import stream - -config.load_kube_config() -c = Configuration() -c.assert_hostname = False -Configuration.set_default(c) -api = core_v1_api.CoreV1Api() -name = 'busybox-test' - -resp = None -try: - resp = api.read_namespaced_pod(name=name, - namespace='default') -except ApiException as e: - if e.status != 404: - print("Unknown error: %s" % e) - exit(1) - -if not resp: - print("Pod %s does not exist. Creating it..." % name) - 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 = api.create_namespaced_pod(body=pod_manifest, - namespace='default') - while True: - resp = api.read_namespaced_pod(name=name, - namespace='default') - if resp.status.phase != 'Pending': - break - time.sleep(1) - print("Done.") - - -# calling exec and wait for response. -exec_command = [ - '/bin/sh', - '-c', - 'echo This message goes to stderr >&2; echo This message goes to stdout'] -resp = stream(api.connect_get_namespaced_pod_exec, name, 'default', - command=exec_command, - stderr=True, stdin=False, - stdout=True, tty=False) -print("Response: " + resp) - -# Calling exec interactively. -exec_command = ['/bin/sh'] -resp = stream(api.connect_get_namespaced_pod_exec, name, 'default', - command=exec_command, - stderr=True, stdin=True, - stdout=True, tty=False, - _preload_content=False) -commands = [ - "echo test1", - "echo \"This message goes to stderr\" >&2", -] -while resp.is_open(): - resp.update(timeout=1) - if resp.peek_stdout(): - print("STDOUT: %s" % resp.read_stdout()) - if resp.peek_stderr(): - print("STDERR: %s" % resp.read_stderr()) - if commands: - c = commands.pop(0) - print("Running command... %s\n" % c) - resp.write_stdin(c + "\n") - else: - break - -resp.write_stdin("date\n") -sdate = resp.readline_stdout(timeout=3) -print("Server date command returns: %s" % sdate) -resp.write_stdin("whoami\n") -user = resp.readline_stdout(timeout=3) -print("Server user is: %s" % user) -resp.close() diff --git a/examples/in_cluster_config.py b/examples/in_cluster_config.py index 86b8704e2c..55f9eb792e 100644 --- a/examples/in_cluster_config.py +++ b/examples/in_cluster_config.py @@ -12,47 +12,46 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Simple example to show loading config from the cluster -# -# It works only from a pod. You can start an image with Python -# (for example python:latest), exec into the pod, install the library, -# then try out this example. -# -# If you get 403 errors from API server you will have to configure -# RBAC to add the permission to list pods. -# -# --- -# kind: ClusterRole -# apiVersion: rbac.authorization.k8s.io/v1 -# metadata: -# name: pods-list -# rules: -# - apiGroups: [""] -# resources: ["pods"] -# verbs: ["list"] -# --- -# kind: ClusterRoleBinding -# apiVersion: rbac.authorization.k8s.io/v1 -# metadata: -# name: pods-list -# subjects: -# - kind: ServiceAccount -# name: default -# namespace: default -# roleRef: -# kind: ClusterRole -# name: pods-list -# apiGroup: rbac.authorization.k8s.io -# --- -# -# Doc: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ +""" +Shows how to load a Kubernetes config from within a cluster. This script +must be run within a pod. You can start a pod with a Python image (for +example, `python:latest`), exec into the pod, install the library, then run +this example. + +If you get 403 errors from the API server you will have to configure RBAC to +add permission to list pods by applying the following manifest: + +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: pods-list +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["list"] + +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: pods-list +subjects: +- kind: ServiceAccount + name: default + namespace: default +roleRef: + kind: ClusterRole + name: pods-list + apiGroup: rbac.authorization.k8s.io + +Documentation: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ +""" from kubernetes import client, config def main(): - - # it works only if this script is run by K8s as a POD config.load_incluster_config() v1 = client.CoreV1Api() diff --git a/examples/ingress_create.py b/examples/ingress_create.py new file mode 100644 index 0000000000..fa5739c8f5 --- /dev/null +++ b/examples/ingress_create.py @@ -0,0 +1,115 @@ +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Creates deployment, service, and ingress objects. The ingress allows external +network access to the cluster. +""" + +from kubernetes import client, config + + +def create_deployment(apps_v1_api): + container = client.V1Container( + name="deployment", + image="gcr.io/google-appengine/fluentd-logger", + image_pull_policy="Never", + ports=[client.V1ContainerPort(container_port=5678)], + ) + # Template + template = client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta(labels={"app": "deployment"}), + spec=client.V1PodSpec(containers=[container])) + # Spec + spec = client.V1DeploymentSpec( + replicas=1, + template=template) + # Deployment + deployment = client.V1Deployment( + api_version="apps/v1", + kind="Deployment", + metadata=client.V1ObjectMeta(name="deployment"), + spec=spec) + # Creation of the Deployment in specified namespace + # (Can replace "default" with a namespace you may have created) + apps_v1_api.create_namespaced_deployment( + namespace="default", body=deployment + ) + + +def create_service(): + core_v1_api = client.CoreV1Api() + body = client.V1Service( + api_version="v1", + kind="Service", + metadata=client.V1ObjectMeta( + name="service-example" + ), + spec=client.V1ServiceSpec( + selector={"app": "deployment"}, + ports=[client.V1ServicePort( + port=5678, + target_port=5678 + )] + ) + ) + # Creation of the Deployment in specified namespace + # (Can replace "default" with a namespace you may have created) + core_v1_api.create_namespaced_service(namespace="default", body=body) + + +def create_ingress(networking_v1_beta1_api): + body = client.NetworkingV1beta1Ingress( + api_version="networking.k8s.io/v1beta1", + kind="Ingress", + metadata=client.V1ObjectMeta(name="ingress-example", annotations={ + "nginx.ingress.kubernetes.io/rewrite-target": "/" + }), + spec=client.NetworkingV1beta1IngressSpec( + rules=[client.NetworkingV1beta1IngressRule( + host="example.com", + http=client.NetworkingV1beta1HTTPIngressRuleValue( + paths=[client.NetworkingV1beta1HTTPIngressPath( + path="/", + backend=client.NetworkingV1beta1IngressBackend( + service_port=5678, + service_name="service-example") + + )] + ) + ) + ] + ) + ) + # Creation of the Deployment in specified namespace + # (Can replace "default" with a namespace you may have created) + networking_v1_beta1_api.create_namespaced_ingress( + namespace="default", + body=body + ) + + +def main(): + # Fetching and loading local Kubernetes Information + config.load_kube_config() + apps_v1_api = client.AppsV1Api() + networking_v1_beta1_api = client.NetworkingV1beta1Api() + + create_deployment(apps_v1_api) + create_service() + create_ingress(networking_v1_beta1_api) + + +if __name__ == "__main__": + main() diff --git a/examples/job_crud.py b/examples/job_crud.py new file mode 100644 index 0000000000..b18b152d4d --- /dev/null +++ b/examples/job_crud.py @@ -0,0 +1,97 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Creates, updates, and deletes a job object. +""" + +from os import path + +import yaml + +from kubernetes import client, config + +JOB_NAME = "pi" + + +def create_job_object(): + # Configureate Pod template container + container = client.V1Container( + name="pi", + image="perl", + command=["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]) + # Create and configurate a spec section + template = client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta(labels={"app": "pi"}), + spec=client.V1PodSpec(restart_policy="Never", containers=[container])) + # Create the specification of deployment + spec = client.V1JobSpec( + template=template, + backoff_limit=4) + # Instantiate the job object + job = client.V1Job( + api_version="batch/v1", + kind="Job", + metadata=client.V1ObjectMeta(name=JOB_NAME), + spec=spec) + + return job + + +def create_job(api_instance, job): + api_response = api_instance.create_namespaced_job( + body=job, + namespace="default") + print("Job created. status='%s'" % str(api_response.status)) + + +def update_job(api_instance, job): + # Update container image + job.spec.template.spec.containers[0].image = "perl" + api_response = api_instance.patch_namespaced_job( + name=JOB_NAME, + namespace="default", + body=job) + print("Job updated. status='%s'" % str(api_response.status)) + + +def delete_job(api_instance): + api_response = api_instance.delete_namespaced_job( + name=JOB_NAME, + namespace="default", + body=client.V1DeleteOptions( + propagation_policy='Foreground', + grace_period_seconds=5)) + print("Job deleted. status='%s'" % str(api_response.status)) + + +def main(): + # Configs can be set in Configuration class directly or using helper + # utility. If no argument provided, the config will be loaded from + # default location. + config.load_kube_config() + batch_v1 = client.BatchV1Api() + # Create a job object with client-python API. The job we + # created is same as the `pi-job.yaml` in the /examples folder. + job = create_job_object() + + create_job(batch_v1, job) + + update_job(batch_v1, job) + + delete_job(batch_v1) + + +if __name__ == '__main__': + main() diff --git a/examples/multiple_clusters.py b/examples/multiple_clusters.py index 68a5d2ff8c..94b0458cd8 100644 --- a/examples/multiple_clusters.py +++ b/examples/multiple_clusters.py @@ -12,14 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Allows you to pick a context and then lists all pods in the chosen context. + +Please install the pick library before running this example. +""" + from kubernetes import client, config -# install pick using "pip install pick". It is not included -# as a dependency because it only used in examples -from pick import pick +from kubernetes.client import configuration +from pick import pick # install pick using `pip install pick` def main(): - contexts, active_context = config.list_kube_config_contexts() if not contexts: print("Cannot find any context in kube-config file.") diff --git a/examples/nginx-deployment.yaml b/examples/nginx-deployment.yaml index d05940d29b..5dd80da371 100644 --- a/examples/nginx-deployment.yaml +++ b/examples/nginx-deployment.yaml @@ -1,9 +1,14 @@ -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment + labels: + app: nginx spec: replicas: 3 + selector: + matchLabels: + app: nginx template: metadata: labels: @@ -11,7 +16,6 @@ spec: spec: containers: - name: nginx - image: nginx:1.7.9 + image: nginx:1.15.4 ports: - containerPort: 80 - diff --git a/examples/manage_node_labels.py b/examples/node_labels.py similarity index 82% rename from examples/manage_node_labels.py rename to examples/node_labels.py index e350ad372d..22ac3197ad 100644 --- a/examples/manage_node_labels.py +++ b/examples/node_labels.py @@ -12,19 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Changes the labels of the "minikube" node. Adds the label "foo" with value +"bar" and will overwrite the "foo" label if it already exists. Removes the +label "baz". +""" + from pprint import pprint from kubernetes import client, config def main(): - """ - Change labels of the "minikube" node: - - Add label "foo" with value "bar". This will overwrite the "foo" label - if it already exists. - - Remove the label "baz" from the node. - """ - config.load_kube_config() api_instance = client.CoreV1Api() diff --git a/examples/notebooks/create_deployment.ipynb b/examples/notebooks/create_deployment.ipynb index 0fa6c05105..b4e1229dd7 100644 --- a/examples/notebooks/create_deployment.ipynb +++ b/examples/notebooks/create_deployment.ipynb @@ -47,7 +47,7 @@ "outputs": [], "source": [ "config.load_kube_config()\n", - "extension = client.ExtensionsV1beta1Api()" + "apps_api = client.AppsV1beta1Api()" ] }, { @@ -70,7 +70,7 @@ }, "outputs": [], "source": [ - "deployment = client.ExtensionsV1beta1Deployment()" + "deployment = client.AppsV1beta1Deployment()" ] }, { @@ -93,7 +93,7 @@ }, "outputs": [], "source": [ - "deployment.api_version = \"extensions/v1beta1\"\n", + "deployment.api_version = \"apps/v1beta1\"\n", "deployment.kind = \"Deployment\"\n", "deployment.metadata = client.V1ObjectMeta(name=\"nginx-deployment\")" ] @@ -118,7 +118,7 @@ }, "outputs": [], "source": [ - "spec = client.ExtensionsV1beta1DeploymentSpec()\n", + "spec = client.AppsV1beta1DeploymentSpec()\n", "spec.replicas = 3" ] }, @@ -207,7 +207,7 @@ }, "outputs": [], "source": [ - "extension.create_namespaced_deployment(namespace=\"default\", body=deployment)" + "apps_api.create_namespaced_deployment(namespace=\"default\", body=deployment)" ] }, { @@ -253,7 +253,7 @@ }, "outputs": [], "source": [ - "extension.replace_namespaced_deployment(name=\"nginx-deployment\", namespace=\"default\", body=deployment)" + "apps_api.replace_namespaced_deployment(name=\"nginx-deployment\", namespace=\"default\", body=deployment)" ] }, { @@ -277,10 +277,10 @@ }, "outputs": [], "source": [ - "rollback = client.ExtensionsV1beta1DeploymentRollback()\n", - "rollback.api_version = \"extensions/v1beta1\"\n", + "rollback = client.AppsV1beta1DeploymentRollback()\n", + "rollback.api_version = \"apps/v1beta1\"\n", "rollback.kind = \"DeploymentRollback\"\n", - "rollback.rollback_to = client.ExtensionsV1beta1RollbackConfig()\n", + "rollback.rollback_to = client.AppsV1beta1RollbackConfig()\n", "rollback.rollback_to.revision = 0\n", "rollback.name = \"nginx-deployment\"" ] diff --git a/examples/notebooks/intro_notebook.ipynb b/examples/notebooks/intro_notebook.ipynb index 53bf63185b..b4e3b8e8e9 100644 --- a/examples/notebooks/intro_notebook.ipynb +++ b/examples/notebooks/intro_notebook.ipynb @@ -89,9 +89,9 @@ }, "outputs": [], "source": [ - "api_instance = client.ExtensionsV1beta1Api()\n", - "dep = client.ExtensionsV1beta1Deployment()\n", - "spec = client.ExtensionsV1beta1DeploymentSpec()" + "api_instance = client.AppsV1beta1Api()\n", + "dep = client.AppsV1beta1Deployment()\n", + "spec = client.AppsV1beta1DeploymentSpec()" ] }, { diff --git a/examples/notebooks/test.ipynb b/examples/notebooks/test.ipynb deleted file mode 100644 index 9f7aa6b830..0000000000 --- a/examples/notebooks/test.ipynb +++ /dev/null @@ -1,104 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "from kubernetes import client, config" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "config.load_incluster_config()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "v1=client.CoreV1Api()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "default\n", - "kube-system\n", - "kubeless\n" - ] - } - ], - "source": [ - "for ns in v1.list_namespace().items:\n", - " print ns.metadata.name" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 2", - "language": "python", - "name": "python2" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.12" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} - diff --git a/examples/notebooks/watch_notebook.ipynb b/examples/notebooks/watch_notebook.ipynb deleted file mode 100644 index e7ec43c67d..0000000000 --- a/examples/notebooks/watch_notebook.ipynb +++ /dev/null @@ -1,129 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "How to watch changes to an object\n", - "==================\n", - "\n", - "In this notebook, we learn how kubernetes API resource Watch endpoint is used to observe resource changes. It can be used to get information about changes to any kubernetes object." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "from kubernetes import client, config, watch" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load config from default location." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "config.load_kube_config()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "### Create API instance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "api_instance = client.CoreV1Api()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "### Run a Watch on the Pods endpoint. \n", - "Watch would be executed and produce output about changes to any Pod. After running the cell below, You can test this by running the Pod notebook [create_pod.ipynb](create_pod.ipynb) and observing the additional output here. You can stop the cell from running by restarting the kernel." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "w = watch.Watch()\n", - "for event in w.stream(api_instance.list_pod_for_all_namespaces):\n", - " print(\"Event: %s %s %s\" % (event['type'],event['object'].kind, event['object'].metadata.name))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 2", - "language": "python", - "name": "python2" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/example1.py b/examples/out_of_cluster_config.py similarity index 93% rename from examples/example1.py rename to examples/out_of_cluster_config.py index 214fd190f7..f391be236d 100644 --- a/examples/example1.py +++ b/examples/out_of_cluster_config.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Shows how to load a Kubernetes config from outside of the cluster. +""" + from kubernetes import client, config diff --git a/examples/pi-job.yaml b/examples/pi-job.yaml new file mode 100644 index 0000000000..ee1d89fdd8 --- /dev/null +++ b/examples/pi-job.yaml @@ -0,0 +1,13 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: pi +spec: + template: + spec: + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + backoffLimit: 4 diff --git a/examples/example4.py b/examples/pick_kube_config_context.py similarity index 84% rename from examples/example4.py rename to examples/pick_kube_config_context.py index 5c05495853..962639669b 100644 --- a/examples/example4.py +++ b/examples/pick_kube_config_context.py @@ -12,11 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Allows you to pick a context and then lists all pods in the chosen context. + +Please install the pick library before running this example. +""" + from kubernetes import client, config from kubernetes.client import configuration -# install pick using "pip install pick". It is not included -# as a dependency because it only used in examples -from pick import pick +from pick import pick # install pick using `pip install pick` def main(): @@ -32,7 +36,7 @@ def main(): # utility config.load_kube_config(context=option) - print("Active host is %s" % configuration.host) + print("Active host is %s" % configuration.Configuration().host) v1 = client.CoreV1Api() print("Listing pods with their IPs:") diff --git a/examples/pod_config_list.py b/examples/pod_config_list.py new file mode 100644 index 0000000000..09bbde9b69 --- /dev/null +++ b/examples/pod_config_list.py @@ -0,0 +1,54 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Allows you to pick a context and then lists all pods in the chosen context. A +context includes a cluster, a user, and a namespace. + +Please install the pick library before running this example. +""" + +from kubernetes import client, config +from kubernetes.client import configuration +from pick import pick # install pick using `pip install pick` + + +def main(): + contexts, active_context = config.list_kube_config_contexts() + if not contexts: + print("Cannot find any context in kube-config file.") + return + contexts = [context['name'] for context in contexts] + active_index = contexts.index(active_context['name']) + option, _ = pick(contexts, title="Pick the context to load", + default_index=active_index) + # Configs can be set in Configuration class directly or using helper + # utility + config.load_kube_config(context=option) + + print("Active host is %s" % configuration.Configuration().host) + + v1 = client.CoreV1Api() + print("Listing pods with their IPs:") + ret = v1.list_pod_for_all_namespaces(watch=False) + for item in ret.items: + print( + "%s\t%s\t%s" % + (item.status.pod_ip, + item.metadata.namespace, + item.metadata.name)) + + +if __name__ == '__main__': + main() diff --git a/examples/pod_exec.py b/examples/pod_exec.py new file mode 100644 index 0000000000..98b717f4a6 --- /dev/null +++ b/examples/pod_exec.py @@ -0,0 +1,129 @@ +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Shows the functionality of exec using a Busybox container. +""" + +import time + +from kubernetes import config +from kubernetes.client import Configuration +from kubernetes.client.apis import core_v1_api +from kubernetes.client.rest import ApiException +from kubernetes.stream import stream + + +def exec_commands(api_instance): + name = 'busybox-test' + resp = None + try: + resp = api_instance.read_namespaced_pod(name=name, + namespace='default') + except ApiException as e: + if e.status != 404: + print("Unknown error: %s" % e) + exit(1) + + if not resp: + print("Pod %s does not exist. Creating it..." % name) + 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 = api_instance.create_namespaced_pod(body=pod_manifest, + namespace='default') + while True: + resp = api_instance.read_namespaced_pod(name=name, + namespace='default') + if resp.status.phase != 'Pending': + break + time.sleep(1) + print("Done.") + + # Calling exec and waiting for response + exec_command = [ + '/bin/sh', + '-c', + 'echo This message goes to stderr; echo This message goes to stdout'] + resp = stream(api_instance.connect_get_namespaced_pod_exec, + name, + 'default', + command=exec_command, + stderr=True, stdin=False, + stdout=True, tty=False) + print("Response: " + resp) + + # Calling exec interactively + exec_command = ['/bin/sh'] + resp = stream(api_instance.connect_get_namespaced_pod_exec, + name, + 'default', + command=exec_command, + stderr=True, stdin=True, + stdout=True, tty=False, + _preload_content=False) + commands = [ + "echo This message goes to stdout", + "echo \"This message goes to stderr\" >&2", + ] + + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + print("STDOUT: %s" % resp.read_stdout()) + if resp.peek_stderr(): + print("STDERR: %s" % resp.read_stderr()) + if commands: + c = commands.pop(0) + print("Running command... %s\n" % c) + resp.write_stdin(c + "\n") + else: + break + + resp.write_stdin("date\n") + sdate = resp.readline_stdout(timeout=3) + print("Server date command returns: %s" % sdate) + resp.write_stdin("whoami\n") + user = resp.readline_stdout(timeout=3) + print("Server user is: %s" % user) + resp.close() + + +def main(): + config.load_kube_config() + c = Configuration() + c.assert_hostname = False + Configuration.set_default(c) + core_v1 = core_v1_api.CoreV1Api() + + exec_commands(core_v1) + + +if __name__ == '__main__': + main() diff --git a/examples/example2.py b/examples/pod_namespace_watch.py similarity index 63% rename from examples/example2.py rename to examples/pod_namespace_watch.py index 003d5c9601..f09768cf7d 100644 --- a/examples/example2.py +++ b/examples/pod_namespace_watch.py @@ -12,6 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +Uses watch to print the stream of events from list namespaces and list pods. +The script will wait for 10 events related to namespaces to occur within +the `timeout_seconds` threshold and then move on to wait for another 10 events +related to pods to occur within the `timeout_seconds` threshold. +""" + from kubernetes import client, config, watch @@ -29,8 +36,18 @@ def main(): count -= 1 if not count: w.stop() + print("Finished namespace stream.") - print("Ended.") + for event in w.stream(v1.list_pod_for_all_namespaces, timeout_seconds=10): + print("Event: %s %s %s" % ( + event['type'], + event['object'].kind, + event['object'].metadata.name) + ) + count -= 1 + if not count: + w.stop() + print("Finished pod stream.") if __name__ == '__main__': diff --git a/examples/remote_cluster.py b/examples/remote_cluster.py index 8cf39efec5..b72b39b4e9 100644 --- a/examples/remote_cluster.py +++ b/examples/remote_cluster.py @@ -23,7 +23,7 @@ def main(): # Define the barer token we are going to use to authenticate. # See here to create the token: # https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/ - aToken = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + aToken = "" # Create a configuration object aConfiguration = client.Configuration() diff --git a/kubernetes/base b/kubernetes/base index 474e9fb322..9f73cc68c1 160000 --- a/kubernetes/base +++ b/kubernetes/base @@ -1 +1 @@ -Subproject commit 474e9fb32293fa05098e920967bb0e0645182d5b +Subproject commit 9f73cc68c1a93725f89842a7cd8c595204c1b901 diff --git a/kubernetes/e2e_test/test_extensions.py b/kubernetes/e2e_test/test_apps.py similarity index 84% rename from kubernetes/e2e_test/test_extensions.py rename to kubernetes/e2e_test/test_apps.py index b659910317..1908374a3e 100644 --- a/kubernetes/e2e_test/test_extensions.py +++ b/kubernetes/e2e_test/test_apps.py @@ -17,12 +17,12 @@ import yaml from kubernetes.client import api_client -from kubernetes.client.apis import extensions_v1beta1_api +from kubernetes.client.apis import apps_v1_api from kubernetes.client.models import v1_delete_options from kubernetes.e2e_test import base -class TestClientExtensions(unittest.TestCase): +class TestClientApps(unittest.TestCase): @classmethod def setUpClass(cls): @@ -30,14 +30,17 @@ def setUpClass(cls): def test_create_deployment(self): client = api_client.ApiClient(configuration=self.config) - api = extensions_v1beta1_api.ExtensionsV1beta1Api(client) + api = apps_v1_api.AppsV1Api(client) name = 'nginx-deployment-' + str(uuid.uuid4()) - deployment = '''apiVersion: extensions/v1beta1 + deployment = '''apiVersion: apps/v1 kind: Deployment metadata: name: %s spec: replicas: 3 + selector: + matchLabels: + app: nginx template: metadata: labels: @@ -45,7 +48,7 @@ def test_create_deployment(self): spec: containers: - name: nginx - image: nginx:1.7.9 + image: nginx:1.15.4 ports: - containerPort: 80 ''' @@ -60,16 +63,19 @@ def test_create_deployment(self): def test_create_daemonset(self): client = api_client.ApiClient(configuration=self.config) - api = extensions_v1beta1_api.ExtensionsV1beta1Api(client) + api = apps_v1_api.AppsV1Api(client) name = 'nginx-app-' + str(uuid.uuid4()) daemonset = { - 'apiVersion': 'extensions/v1beta1', + 'apiVersion': 'apps/v1', 'kind': 'DaemonSet', 'metadata': { 'labels': {'app': 'nginx'}, 'name': '%s' % name, }, 'spec': { + 'selector': { + 'matchLabels': {'app': 'nginx'}, + }, 'template': { 'metadata': { 'labels': {'app': 'nginx'}, @@ -77,7 +83,7 @@ def test_create_daemonset(self): 'spec': { 'containers': [ {'name': 'nginx-app', - 'image': 'nginx:1.10'}, + 'image': 'nginx:1.15.4'}, ], }, }, @@ -91,4 +97,4 @@ def test_create_daemonset(self): self.assertIsNotNone(resp) options = v1_delete_options.V1DeleteOptions() - resp = api.delete_namespaced_daemon_set(name, 'default', body=options) \ No newline at end of file + resp = api.delete_namespaced_daemon_set(name, 'default', body=options) diff --git a/kubernetes/e2e_test/test_client.py b/kubernetes/e2e_test/test_client.py index 57000b9827..d7a450c6a3 100644 --- a/kubernetes/e2e_test/test_client.py +++ b/kubernetes/e2e_test/test_client.py @@ -29,6 +29,26 @@ def short_uuid(): return id[-12:] +def manifest_with_command(name, command): + return { + 'apiVersion': 'v1', + 'kind': 'Pod', + 'metadata': { + 'name': name + }, + 'spec': { + 'containers': [{ + 'image': 'busybox', + 'name': 'sleep', + "args": [ + "/bin/sh", + "-c", + command + ] + }] + } + } + class TestClient(unittest.TestCase): @classmethod @@ -40,25 +60,7 @@ def test_pod_apis(self): api = core_v1_api.CoreV1Api(client) 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" - ] - }] - } - } - + pod_manifest = manifest_with_command(name, "while true;do date;sleep 5; done") resp = api.create_namespaced_pod(body=pod_manifest, namespace='default') self.assertEqual(name, resp.metadata.name) @@ -117,6 +119,45 @@ def test_pod_apis(self): resp = api.delete_namespaced_pod(name=name, body={}, namespace='default') + def test_exit_code(self): + client = api_client.ApiClient(configuration=self.config) + api = core_v1_api.CoreV1Api(client) + + name = 'busybox-test-' + short_uuid() + pod_manifest = manifest_with_command(name, "while true;do date;sleep 5; done") + resp = api.create_namespaced_pod(body=pod_manifest, + namespace='default') + self.assertEqual(name, resp.metadata.name) + self.assertTrue(resp.status.phase) + + while True: + resp = api.read_namespaced_pod(name=name, + namespace='default') + self.assertEqual(name, resp.metadata.name) + self.assertTrue(resp.status.phase) + if resp.status.phase == 'Running': + break + time.sleep(1) + + commands_expected_values = ( + (["false", 1]), + (["/bin/sh", "-c", "sleep 1; exit 3"], 3), + (["true", 0]), + (["/bin/sh", "-c", "ls /"], 0) + ) + for command, value in commands_expected_values: + client = stream(api.connect_get_namespaced_pod_exec, name, 'default', + command=command, + stderr=True, stdin=False, + stdout=True, tty=False, + _preload_content=False) + + self.assertIsNone(client.returncode) + client.run_forever(timeout=10) + self.assertEqual(client.returncode, value) + + resp = api.delete_namespaced_pod(name=name, body={}, + namespace='default') def test_service_apis(self): client = api_client.ApiClient(configuration=self.config) diff --git a/kubernetes/e2e_test/test_utils.py b/kubernetes/e2e_test/test_utils.py index b5684a7986..ab752dff79 100644 --- a/kubernetes/e2e_test/test_utils.py +++ b/kubernetes/e2e_test/test_utils.py @@ -14,7 +14,9 @@ import unittest +import yaml from kubernetes import utils, client +from kubernetes.client.rest import ApiException from kubernetes.e2e_test import base @@ -39,32 +41,39 @@ def tearDownClass(cls): def test_create_apps_deployment_from_yaml(self): """ - Should be able to create an apps/v1beta1 deployment. + Should be able to create an apps/v1 deployment. """ k8s_client = client.api_client.ApiClient(configuration=self.config) utils.create_from_yaml( k8s_client, self.path_prefix + "apps-deployment.yaml") - app_api = client.AppsV1beta1Api(k8s_client) + app_api = client.AppsV1Api(k8s_client) dep = app_api.read_namespaced_deployment(name="nginx-app", namespace="default") self.assertIsNotNone(dep) - app_api.delete_namespaced_deployment( - name="nginx-app", namespace="default", - body={}) + while True: + try: + app_api.delete_namespaced_deployment( + name="nginx-app", namespace="default", + body={}) + break + except ApiException: + continue - def test_create_extensions_deployment_from_yaml(self): - """ - Should be able to create an extensions/v1beta1 deployment. - """ + def test_create_apps_deployment_from_yaml_obj(self): k8s_client = client.api_client.ApiClient(configuration=self.config) - utils.create_from_yaml( - k8s_client, self.path_prefix + "extensions-deployment.yaml") - ext_api = client.ExtensionsV1beta1Api(k8s_client) - dep = ext_api.read_namespaced_deployment(name="nginx-deployment", + with open(self.path_prefix + "apps-deployment.yaml") as f: + yml_obj = yaml.safe_load(f) + + yml_obj["metadata"]["name"] = "nginx-app-3" + + utils.create_from_dict(k8s_client, yml_obj) + + app_api = client.AppsV1Api(k8s_client) + dep = app_api.read_namespaced_deployment(name="nginx-app-3", namespace="default") self.assertIsNotNone(dep) - ext_api.delete_namespaced_deployment( - name="nginx-deployment", namespace="default", + app_api.delete_namespaced_deployment( + name="nginx-app-3", namespace="default", body={}) def test_create_pod_from_yaml(self): @@ -123,6 +132,20 @@ def test_create_rbac_role_from_yaml(self): rbac_api.delete_namespaced_role( name="pod-reader", namespace="default", body={}) + def test_create_rbac_role_from_yaml_with_verbose_enabled(self): + """ + Should be able to create an rbac role with verbose enabled. + """ + k8s_client = client.api_client.ApiClient(configuration=self.config) + utils.create_from_yaml( + k8s_client, self.path_prefix + "rbac-role.yaml", verbose=True) + rbac_api = client.RbacAuthorizationV1Api(k8s_client) + rbac_role = rbac_api.read_namespaced_role( + name="pod-reader", namespace="default") + self.assertIsNotNone(rbac_role) + rbac_api.delete_namespaced_role( + name="pod-reader", namespace="default", body={}) + def test_create_deployment_non_default_namespace_from_yaml(self): """ Should be able to create a namespace "dep", @@ -134,7 +157,7 @@ def test_create_deployment_non_default_namespace_from_yaml(self): utils.create_from_yaml( k8s_client, self.path_prefix + "dep-deployment.yaml") core_api = client.CoreV1Api(k8s_client) - ext_api = client.ExtensionsV1beta1Api(k8s_client) + ext_api = client.AppsV1Api(k8s_client) nmsp = core_api.read_namespace(name="dep") self.assertIsNotNone(nmsp) dep = ext_api.read_namespaced_deployment(name="nginx-deployment", @@ -186,7 +209,7 @@ def test_create_general_list_from_yaml(self): utils.create_from_yaml( k8s_client, self.path_prefix + "list.yaml") core_api = client.CoreV1Api(k8s_client) - ext_api = client.ExtensionsV1beta1Api(k8s_client) + ext_api = client.AppsV1Api(k8s_client) svc = core_api.read_namespaced_service(name="list-service-test", namespace="default") self.assertIsNotNone(svc) @@ -266,7 +289,7 @@ def test_create_from_list_in_multi_resource_yaml(self): utils.create_from_yaml( k8s_client, self.path_prefix + "multi-resource-with-list.yaml") core_api = client.CoreV1Api(k8s_client) - app_api = client.AppsV1beta1Api(k8s_client) + app_api = client.AppsV1Api(k8s_client) pod_0 = core_api.read_namespaced_pod( name="mock-pod-0", namespace="default") self.assertIsNotNone(pod_0) @@ -317,7 +340,7 @@ def test_create_from_multi_resource_yaml_with_conflict(self): def test_create_from_multi_resource_yaml_with_multi_conflicts(self): """ - Should create an extensions/v1beta1 deployment + Should create an apps/v1 deployment and fail to create the same deployment twice. Should raise an exception that contains two error messages. """ @@ -327,14 +350,14 @@ def test_create_from_multi_resource_yaml_with_multi_conflicts(self): k8s_client, self.path_prefix + "triple-nginx.yaml") exp_error = ('Error from server (Conflict): {"kind":"Status",' '"apiVersion":"v1","metadata":{},"status":"Failure",' - '"message":"deployments.extensions \\"triple-nginx\\" ' + '"message":"deployments.apps \\"triple-nginx\\" ' 'already exists","reason":"AlreadyExists",' - '"details":{"name":"triple-nginx","group":"extensions",' + '"details":{"name":"triple-nginx","group":"apps",' '"kind":"deployments"},"code":409}\n' ) exp_error += exp_error self.assertEqual(exp_error, str(cm.exception)) - ext_api = client.ExtensionsV1beta1Api(k8s_client) + ext_api = client.AppsV1Api(k8s_client) dep = ext_api.read_namespaced_deployment(name="triple-nginx", namespace="default") self.assertIsNotNone(dep) @@ -342,14 +365,16 @@ def test_create_from_multi_resource_yaml_with_multi_conflicts(self): name="triple-nginx", namespace="default", body={}) - def test_create_namespaces_apps_deployment_from_yaml(self): + def test_create_namespaced_apps_deployment_from_yaml(self): """ - Should be able to create an apps/v1beta1 deployment. + Should be able to create an apps/v1beta1 deployment + in a test namespace. """ k8s_client = client.api_client.ApiClient(configuration=self.config) utils.create_from_yaml( - k8s_client, self.path_prefix + "apps-deployment.yaml", namespace=self.test_namespace) - app_api = client.AppsV1beta1Api(k8s_client) + k8s_client, self.path_prefix + "apps-deployment.yaml", + namespace=self.test_namespace) + app_api = client.AppsV1Api(k8s_client) dep = app_api.read_namespaced_deployment(name="nginx-app", namespace=self.test_namespace) self.assertIsNotNone(dep) @@ -357,16 +382,17 @@ def test_create_namespaces_apps_deployment_from_yaml(self): name="nginx-app", namespace=self.test_namespace, body={}) - def test_create_from_list_in_multi_resource_yaml(self): + def test_create_from_list_in_multi_resource_yaml_namespaced(self): """ Should be able to create the items in the PodList and a deployment - specified in the multi-resource file + specified in the multi-resource file in a test namespace """ k8s_client = client.api_client.ApiClient(configuration=self.config) utils.create_from_yaml( - k8s_client, self.path_prefix + "multi-resource-with-list.yaml", namespace=self.test_namespace) + k8s_client, self.path_prefix + "multi-resource-with-list.yaml", + namespace=self.test_namespace) core_api = client.CoreV1Api(k8s_client) - app_api = client.AppsV1beta1Api(k8s_client) + app_api = client.AppsV1Api(k8s_client) pod_0 = core_api.read_namespaced_pod( name="mock-pod-0", namespace=self.test_namespace) self.assertIsNotNone(pod_0) diff --git a/kubernetes/e2e_test/test_yaml/extensions-deployment.yaml b/kubernetes/e2e_test/test_yaml/apps-deployment-2.yaml similarity index 57% rename from kubernetes/e2e_test/test_yaml/extensions-deployment.yaml rename to kubernetes/e2e_test/test_yaml/apps-deployment-2.yaml index d05940d29b..b2acb92f82 100644 --- a/kubernetes/e2e_test/test_yaml/extensions-deployment.yaml +++ b/kubernetes/e2e_test/test_yaml/apps-deployment-2.yaml @@ -1,9 +1,14 @@ -apiVersion: extensions/v1beta1 +apiVersion: apps/v1beta1 kind: Deployment metadata: - name: nginx-deployment + name: nginx-app-2 + labels: + app: nginx spec: replicas: 3 + selector: + matchLabels: + app: nginx template: metadata: labels: @@ -11,7 +16,6 @@ spec: spec: containers: - name: nginx - image: nginx:1.7.9 + image: nginx:1.15.4 ports: - containerPort: 80 - diff --git a/kubernetes/e2e_test/test_yaml/apps-deployment.yaml b/kubernetes/e2e_test/test_yaml/apps-deployment.yaml index a2ffa6b996..eb33bbac55 100644 --- a/kubernetes/e2e_test/test_yaml/apps-deployment.yaml +++ b/kubernetes/e2e_test/test_yaml/apps-deployment.yaml @@ -1,4 +1,4 @@ -apiVersion: apps/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: nginx-app diff --git a/kubernetes/e2e_test/test_yaml/dep-deployment.yaml b/kubernetes/e2e_test/test_yaml/dep-deployment.yaml index be9b281f20..57170d92a4 100644 --- a/kubernetes/e2e_test/test_yaml/dep-deployment.yaml +++ b/kubernetes/e2e_test/test_yaml/dep-deployment.yaml @@ -1,10 +1,13 @@ -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment namespace: dep spec: replicas: 3 + selector: + matchLabels: + app: nginx template: metadata: labels: @@ -12,7 +15,6 @@ spec: spec: containers: - name: nginx - image: nginx:1.7.9 + image: nginx:1.15.4 ports: - containerPort: 80 - diff --git a/kubernetes/e2e_test/test_yaml/list.yaml b/kubernetes/e2e_test/test_yaml/list.yaml index 3416ec429d..15aab9ad5c 100644 --- a/kubernetes/e2e_test/test_yaml/list.yaml +++ b/kubernetes/e2e_test/test_yaml/list.yaml @@ -11,7 +11,7 @@ items: port: 80 selector: app: list-deployment-test -- apiVersion: extensions/v1beta1 +- apiVersion: apps/v1 kind: Deployment metadata: name: list-deployment-test @@ -19,6 +19,9 @@ items: app: list-deployment-test spec: replicas: 1 + selector: + matchLabels: + app: list-deployment-test template: metadata: labels: @@ -26,4 +29,4 @@ items: spec: containers: - name: nginx - image: nginx \ No newline at end of file + image: nginx:1.15.4 \ No newline at end of file diff --git a/kubernetes/e2e_test/test_yaml/multi-resource-with-list.yaml b/kubernetes/e2e_test/test_yaml/multi-resource-with-list.yaml index b3228b8cde..996e4b6961 100644 --- a/kubernetes/e2e_test/test_yaml/multi-resource-with-list.yaml +++ b/kubernetes/e2e_test/test_yaml/multi-resource-with-list.yaml @@ -24,7 +24,7 @@ items: image: busybox command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600'] --- -apiVersion: apps/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: mock diff --git a/kubernetes/e2e_test/test_yaml/triple-nginx.yaml b/kubernetes/e2e_test/test_yaml/triple-nginx.yaml index 9a4fda66cf..a071c88b50 100644 --- a/kubernetes/e2e_test/test_yaml/triple-nginx.yaml +++ b/kubernetes/e2e_test/test_yaml/triple-nginx.yaml @@ -1,9 +1,12 @@ -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: triple-nginx spec: replicas: 3 + selector: + matchLabels: + app: nginx template: metadata: labels: @@ -11,16 +14,19 @@ spec: spec: containers: - name: nginx - image: nginx:1.7.9 + image: nginx:1.15.4 ports: - containerPort: 80 --- -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: triple-nginx spec: replicas: 3 + selector: + matchLabels: + app: nginx template: metadata: labels: @@ -28,16 +34,19 @@ spec: spec: containers: - name: nginx - image: nginx:1.7.9 + image: nginx:1.15.4 ports: - containerPort: 80 --- -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: triple-nginx spec: replicas: 3 + selector: + matchLabels: + app: nginx template: metadata: labels: @@ -45,6 +54,6 @@ spec: spec: containers: - name: nginx - image: nginx:1.7.9 + image: nginx:1.15.4 ports: - containerPort: 80 \ No newline at end of file diff --git a/kubernetes/test/test_quantity.py b/kubernetes/test/test_quantity.py new file mode 100644 index 0000000000..35bef5661d --- /dev/null +++ b/kubernetes/test/test_quantity.py @@ -0,0 +1,112 @@ +# coding: utf-8 +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import unittest +from kubernetes.utils import parse_quantity +from decimal import Decimal + + +class TestQuantity(unittest.TestCase): + def test_parse(self): + self.assertIsInstance(parse_quantity(2.2), Decimal) + # input, expected output + tests = [ + (0, 0), + (2, 2), + (2, Decimal("2")), + (2., 2), + (Decimal("2.2"), Decimal("2.2")), + (2., Decimal(2)), + (Decimal("2."), 2), + ("123", 123), + ("2", 2), + ("2n", Decimal("2") * Decimal(1000)**-3), + ("2u", Decimal("0.000002")), + ("2m", Decimal("0.002")), + ("0m", Decimal("0")), + ("0M", Decimal("0")), + ("223k", 223000), + ("002M", 2 * 1000**2), + ("2M", 2 * 1000**2), + ("4123G", 4123 * 1000**3), + ("2T", 2 * 1000**4), + ("2P", 2 * 1000**5), + ("2E", 2 * 1000**6), + + ("223Ki", 223 * 1024), + ("002Mi", 2 * 1024**2), + ("2Mi", 2 * 1024**2), + ("2Gi", 2 * 1024**3), + ("4123Gi", 4123 * 1024**3), + ("2Ti", 2 * 1024**4), + ("2Pi", 2 * 1024**5), + ("2Ei", 2 * 1024**6), + + ("2.34n", Decimal("2.34") * Decimal(1000)**-3), + ("2.34u", Decimal("2.34") * Decimal(1000)**-2), + ("2.34m", Decimal("2.34") * Decimal(1000)**-1), + ("2.34Ki", Decimal("2.34") * 1024), + ("2.34", Decimal("2.34")), + (".34", Decimal("0.34")), + ("34.", 34), + (".34M", Decimal("0.34") * 1000**2), + + ("2e2K", Decimal("2e2") * 1000), + ("2e2Ki", Decimal("2e2") * 1024), + ("2e-2Ki", Decimal("2e-2") * 1024), + ("2.34E1", Decimal("2.34E1")), + (".34e-2", Decimal("0.34e-2")), + ] + + for inp, out in tests: + self.assertEqual(parse_quantity(inp), out) + if isinstance(inp, (int, float, Decimal)): + self.assertEqual(parse_quantity(-1 * inp), -out) + else: + self.assertEqual(parse_quantity("-" + inp), -out) + self.assertEqual(parse_quantity("+" + inp), out) + + def test_parse_invalid(self): + self.assertRaises(ValueError, parse_quantity, []) + self.assertRaises(ValueError, parse_quantity, "") + self.assertRaises(ValueError, parse_quantity, "-") + self.assertRaises(ValueError, parse_quantity, "i") + self.assertRaises(ValueError, parse_quantity, "2i") + self.assertRaises(ValueError, parse_quantity, "2mm") + self.assertRaises(ValueError, parse_quantity, "2mmKi") + self.assertRaises(ValueError, parse_quantity, "2KKi") + self.assertRaises(ValueError, parse_quantity, "2e") + self.assertRaises(ValueError, parse_quantity, "2.2i") + self.assertRaises(ValueError, parse_quantity, "bla") + self.assertRaises(ValueError, parse_quantity, "Ki") + self.assertRaises(ValueError, parse_quantity, "M") + self.assertRaises(ValueError, parse_quantity, "2ki") + self.assertRaises(ValueError, parse_quantity, "2Ki ") + self.assertRaises(ValueError, parse_quantity, "20Ki ") + self.assertRaises(ValueError, parse_quantity, "20B") + self.assertRaises(ValueError, parse_quantity, "20Bi") + self.assertRaises(ValueError, parse_quantity, "20.2Bi") + self.assertRaises(ValueError, parse_quantity, "2MiKi") + self.assertRaises(ValueError, parse_quantity, "2MK") + self.assertRaises(ValueError, parse_quantity, "2MKi") + self.assertRaises(ValueError, parse_quantity, "234df") + self.assertRaises(ValueError, parse_quantity, "df234") + self.assertRaises(ValueError, parse_quantity, tuple()) + + +if __name__ == '__main__': + unittest.main() diff --git a/kubernetes/utils/__init__.py b/kubernetes/utils/__init__.py index 2b8597c2fb..8add80bcfe 100644 --- a/kubernetes/utils/__init__.py +++ b/kubernetes/utils/__init__.py @@ -14,4 +14,6 @@ from __future__ import absolute_import -from .create_from_yaml import FailToCreateError, create_from_yaml +from .create_from_yaml import (FailToCreateError, create_from_dict, + create_from_yaml) +from .quantity import parse_quantity diff --git a/kubernetes/utils/create_from_yaml.py b/kubernetes/utils/create_from_yaml.py index 3180671ae2..80efa085f9 100644 --- a/kubernetes/utils/create_from_yaml.py +++ b/kubernetes/utils/create_from_yaml.py @@ -41,14 +41,6 @@ def create_from_yaml( the yaml file already contains a namespace definition this parameter has no effect. - Returns: - An k8s api object or list of apis objects created from YAML. - When a single object is generated, return type is dependent - on output_list. - - Throws a FailToCreateError exception if creation of any object - fails with helpful messages from the server. - Available parameters for creating : :param async_req bool :param bool include_uninitialized: If true, partially initialized @@ -59,47 +51,81 @@ def create_from_yaml( directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed - """ + Raises: + FailToCreateError which holds list of `client.rest.ApiException` + instances for each object that failed to create. + """ with open(path.abspath(yaml_file)) as f: yml_document_all = yaml.safe_load_all(f) - api_exceptions = [] - # Load all documents from a single YAML file + + failures = [] for yml_document in yml_document_all: - # If it is a list type, will need to iterate its items - if "List" in yml_document["kind"]: - # Could be "List" or "Pod/Service/...List" - # This is a list type. iterate within its items - kind = yml_document["kind"].replace("List", "") - for yml_object in yml_document["items"]: - # Mitigate cases when server returns a xxxList object - # See kubernetes-client/python#586 - if kind is not "": - yml_object["apiVersion"] = yml_document["apiVersion"] - yml_object["kind"] = kind - try: - create_from_yaml_single_item( - k8s_client, yml_object, verbose, namespace, **kwargs) - except client.rest.ApiException as api_exception: - api_exceptions.append(api_exception) - else: - # This is a single object. Call the single item method - try: - create_from_yaml_single_item( - k8s_client, yml_document, verbose, namespace, **kwargs) - except client.rest.ApiException as api_exception: - api_exceptions.append(api_exception) + try: + create_from_dict(k8s_client, yml_document, verbose, + namespace=namespace, + **kwargs) + except FailToCreateError as failure: + failures.extend(failure.api_exceptions) + if failures: + raise FailToCreateError(failures) + + +def create_from_dict(k8s_client, data, verbose=False, namespace='default', + **kwargs): + """ + Perform an action from a dictionary containing valid kubernetes + API object (i.e. List, Service, etc). + + Input: + k8s_client: an ApiClient object, initialized with the client args. + data: a dictionary holding valid kubernetes objects + verbose: If True, print confirmation from the create action. + Default is False. + namespace: string. Contains the namespace to create all + resources inside. The namespace must preexist otherwise + the resource creation will fail. If the API object in + the yaml file already contains a namespace definition + this parameter has no effect. + + Raises: + FailToCreateError which holds list of `client.rest.ApiException` + instances for each object that failed to create. + """ + # If it is a list type, will need to iterate its items + api_exceptions = [] + + if "List" in data["kind"]: + # Could be "List" or "Pod/Service/...List" + # This is a list type. iterate within its items + kind = data["kind"].replace("List", "") + for yml_object in data["items"]: + # Mitigate cases when server returns a xxxList object + # See kubernetes-client/python#586 + if kind is not "": + yml_object["apiVersion"] = data["apiVersion"] + yml_object["kind"] = kind + try: + create_from_yaml_single_item( + k8s_client, yml_object, verbose, namespace=namespace, + **kwargs) + except client.rest.ApiException as api_exception: + api_exceptions.append(api_exception) + else: + # This is a single object. Call the single item method + try: + create_from_yaml_single_item( + k8s_client, data, verbose, namespace=namespace, **kwargs) + except client.rest.ApiException as api_exception: + api_exceptions.append(api_exception) + # In case we have exceptions waiting for us, raise them if api_exceptions: raise FailToCreateError(api_exceptions) def create_from_yaml_single_item( - k8s_client, - yml_object, - verbose=False, - namespace="default", - **kwargs): + k8s_client, yml_object, verbose=False, **kwargs): group, _, version = yml_object["apiVersion"].partition("/") if version == "": version = group @@ -116,19 +142,24 @@ def create_from_yaml_single_item( kind = yml_object["kind"] kind = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', kind) kind = re.sub('([a-z0-9])([A-Z])', r'\1_\2', kind).lower() - # Decide which namespace we are going to put the object in, - # if any - if "namespace" in yml_object["metadata"]: - namespace = yml_object["metadata"]["namespace"] # Expect the user to create namespaced objects more often if hasattr(k8s_api, "create_namespaced_{0}".format(kind)): + # Decide which namespace we are going to put the object in, + # if any + if "namespace" in yml_object["metadata"]: + namespace = yml_object["metadata"]["namespace"] + kwargs['namespace'] = namespace resp = getattr(k8s_api, "create_namespaced_{0}".format(kind))( - body=yml_object, namespace=namespace, **kwargs) + body=yml_object, **kwargs) else: + kwargs.pop('namespace', None) resp = getattr(k8s_api, "create_{0}".format(kind))( body=yml_object, **kwargs) if verbose: - print("{0} created. status='{1}'".format(kind, str(resp.status))) + msg = "{0} created.".format(kind) + if hasattr(resp, 'status'): + msg += " status='{0}'".format(str(resp.status)) + print(msg) class FailToCreateError(Exception): diff --git a/kubernetes/utils/quantity.py b/kubernetes/utils/quantity.py new file mode 100644 index 0000000000..df373ae468 --- /dev/null +++ b/kubernetes/utils/quantity.py @@ -0,0 +1,75 @@ +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from decimal import Decimal, InvalidOperation + + +def parse_quantity(quantity): + """ + Parse kubernetes canonical form quantity like 200Mi to a decimal number. + Supported SI suffixes: + base1024: Ki | Mi | Gi | Ti | Pi | Ei + base1000: n | u | m | "" | k | M | G | T | P | E + + See https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go + + Input: + quanity: string. kubernetes canonical form quantity + + Returns: + Decimal + + Raises: + ValueError on invalid or unknown input + """ + if isinstance(quantity, (int, float, Decimal)): + return Decimal(quantity) + + exponents = {"n": -3, "u": -2, "m": -1, "K": 1, "k": 1, "M": 2, + "G": 3, "T": 4, "P": 5, "E": 6} + + quantity = str(quantity) + number = quantity + suffix = None + if len(quantity) >= 2 and quantity[-1] == "i": + if quantity[-2] in exponents: + number = quantity[:-2] + suffix = quantity[-2:] + elif len(quantity) >= 1 and quantity[-1] in exponents: + number = quantity[:-1] + suffix = quantity[-1:] + + try: + number = Decimal(number) + except InvalidOperation: + raise ValueError("Invalid number format: {}".format(number)) + + if suffix is None: + return number + + if suffix.endswith("i"): + base = 1024 + elif len(suffix) == 1: + base = 1000 + else: + raise ValueError("{} has unknown suffix".format(quantity)) + + # handly SI inconsistency + if suffix == "ki": + raise ValueError("{} has unknown suffix".format(quantity)) + + if suffix[0] not in exponents: + raise ValueError("{} has unknown suffix".format(quantity)) + + exponent = Decimal(exponents[suffix[0]]) + return number * (base ** exponent) diff --git a/requirements.txt b/requirements.txt index 56079f48e3..74e38b7cd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ ipaddress>=1.0.17;python_version=="2.7" # PSF websocket-client>=0.32.0,!=0.40.0,!=0.41.*,!=0.42.* # LGPLv2+ requests # Apache-2.0 requests-oauthlib # ISC -urllib3>=1.23 # MIT +urllib3>=1.24.2 # MIT diff --git a/scripts/kube-init.sh b/scripts/kube-init.sh index 32cbc0472a..b43890c566 100755 --- a/scripts/kube-init.sh +++ b/scripts/kube-init.sh @@ -108,7 +108,8 @@ done # Shut down CI if minikube did not start and show logs if [ $MINIKUBE_OK == "false" ]; then sudo minikube logs - die $LINENO "minikube did not start" + echo "minikube did not start (line: ${LINENO})" + exit 1 fi echo "Dump Kubernetes Objects..." diff --git a/scripts/update-client.sh b/scripts/update-client.sh index 118d95b221..506038429a 100755 --- a/scripts/update-client.sh +++ b/scripts/update-client.sh @@ -21,11 +21,6 @@ set -o errexit set -o nounset set -o pipefail -if ! which mvn > /dev/null 2>&1; then - echo "Maven is not installed." - exit -fi - SCRIPT_ROOT=$(dirname "${BASH_SOURCE}") CLIENT_ROOT="${SCRIPT_ROOT}/../kubernetes" CLIENT_VERSION=$(python "${SCRIPT_ROOT}/constants.py" CLIENT_VERSION) diff --git a/setup.py b/setup.py index ec50eb2de6..9cdfcb6115 100644 --- a/setup.py +++ b/setup.py @@ -62,9 +62,7 @@ 'kubernetes.stream', 'kubernetes.client.models', 'kubernetes.utils'], include_package_data=True, - long_description="""\ - Python client for kubernetes http://kubernetes.io/ - """, + long_description="Python client for kubernetes http://kubernetes.io/", classifiers=[ "Development Status :: %s" % DEVELOPMENT_STATUS, "Topic :: Utilities", @@ -76,9 +74,9 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], ) diff --git a/test-requirements.txt b/test-requirements.txt index 0bb8dc53c4..5e6aac0889 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,13 +1,15 @@ coverage>=4.0.3 nose>=1.3.7 pytest +pytest-cov pluggy>=0.3.1 py>=1.4.31 randomize>=0.13 mock>=2.0.0 -sphinx>=1.2.1,!=1.3b1,<1.4 # BSD +sphinx>=1.4 # BSD recommonmark +sphinx_markdown_tables codecov>=1.4.0 pycodestyle autopep8 -isort \ No newline at end of file +isort diff --git a/tox.ini b/tox.ini index a271d7854c..0930582f4b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [tox] -envlist = py27, py34, py35, py36, py37 +envlist = + py27, py3{5,6,7,8} + py27-functional, py3{5,6,7,8}-functional [testenv] passenv = TOXENV CI TRAVIS TRAVIS_* @@ -9,7 +11,8 @@ deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = python -V - py.test -vvv -s --ignore=kubernetes/e2e_test + !functional: pytest -vvv -s --ignore=kubernetes/e2e_test + functional: {toxinidir}/scripts/kube-init.sh pytest -vvv -s [] [testenv:docs] commands = @@ -19,30 +22,10 @@ commands = commands = {toxinidir}/scripts/update-pycodestyle.sh -[testenv:py27-functional] -commands = - python -V - {toxinidir}/scripts/kube-init.sh py.test -vvv -s [] - -[testenv:py35-functional] -commands = - python -V - {toxinidir}/scripts/kube-init.sh py.test -vvv -s [] - -[testenv:py36-functional] -commands = - python -V - {toxinidir}/scripts/kube-init.sh py.test -vvv -s [] - -[testenv:py37-functional] -commands = - python -V - {toxinidir}/scripts/kube-init.sh py.test -vvv -s [] - [testenv:coverage] commands = python -V - nosetests --with-coverage --cover-package=kubernetes.config,kubernetes.watch --cover-tests + pytest --cov=kubernetes/watch --cov=kubernetes/config kubernetes/watch kubernetes/config [testenv:codecov] commands =